diff --git a/Cargo.lock b/Cargo.lock index 9430380944e573b5b9f7fdfc1e83df0d760d5957..ac851b942881ef148a64c66c143a0b04129c31a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -388,7 +388,6 @@ dependencies = [ "streaming_diff", "task", "telemetry", - "telemetry_events", "terminal", "terminal_view", "text", @@ -407,6 +406,37 @@ dependencies = [ "zed_actions", ] +[[package]] +name = "agent_ui_v2" +version = "0.1.0" +dependencies = [ + "agent", + "agent_servers", + "agent_settings", + "agent_ui", + "anyhow", + "assistant_text_thread", + "chrono", + "db", + "editor", + "feature_flags", + "fs", + "fuzzy", + "gpui", + "menu", + "project", + "prompt_store", + "serde", + "serde_json", + "settings", + "text", + "time", + "time_format", + "ui", + "util", + "workspace", +] + [[package]] name = "ahash" version = "0.7.8" @@ -835,7 +865,6 @@ dependencies = [ "fs", "futures 0.3.31", "fuzzy", - "globset", "gpui", "html_to_markdown", "http_client", @@ -894,7 +923,7 @@ dependencies = [ "settings", "smallvec", "smol", - "telemetry_events", + "telemetry", "text", "ui", "unindent", @@ -2770,9 +2799,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.41" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "jobserver", @@ -3113,9 +3142,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "b042e5d8a74ae91bb0961acd039822472ec99f8ab0948cbf6d1369588f8be586" dependencies = [ "cc", ] @@ -5111,7 +5140,6 @@ dependencies = [ "cloud_llm_client", "collections", "copilot", - "credentials_provider", "ctor", "db", "edit_prediction_context", @@ -5132,6 +5160,7 @@ dependencies = [ "postage", "pretty_assertions", "project", + "pulldown-cmark 0.12.2", "rand 0.9.2", "regex", "release_channel", @@ -5186,7 +5215,6 @@ dependencies = [ "pretty_assertions", "project", "prompt_store", - "pulldown-cmark 0.12.2", "release_channel", "reqwest_client", "serde", @@ -5258,9 +5286,11 @@ dependencies = [ "feature_flags", "fs", "futures 0.3.31", + "git", "gpui", "indoc", "language", + "log", "lsp", "markdown", "menu", @@ -5274,8 +5304,8 @@ dependencies = [ "telemetry", "text", "theme", + "time", "ui", - "ui_input", "util", "workspace", "zed_actions", @@ -6102,9 +6132,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fixedbitset" @@ -7056,6 +7086,7 @@ dependencies = [ "picker", "pretty_assertions", "project", + "rand 0.9.2", "recent_projects", "remote", "schemars", @@ -8811,6 +8842,7 @@ dependencies = [ "cloud_api_types", "cloud_llm_client", "collections", + "credentials_provider", "futures 0.3.31", "gpui", "http_client", @@ -8826,9 +8858,9 @@ dependencies = [ "serde_json", "settings", "smol", - "telemetry_events", "thiserror 2.0.17", "util", + "zed_env_vars", ] [[package]] @@ -8887,7 +8919,6 @@ dependencies = [ "util", "vercel", "x_ai", - "zed_env_vars", ] [[package]] @@ -14789,6 +14820,8 @@ dependencies = [ "assets", "bm25", "client", + "copilot", + "edit_prediction", "editor", "feature_flags", "fs", @@ -14797,6 +14830,7 @@ dependencies = [ "gpui", "heck 0.5.0", "language", + "language_models", "log", "menu", "node_runtime", @@ -18115,6 +18149,7 @@ dependencies = [ "menu", "multi_buffer", "nvim-rs", + "outline_panel", "parking_lot", "perf", "picker", @@ -20067,6 +20102,7 @@ dependencies = [ "component", "dap", "db", + "feature_flags", "fs", "futures 0.3.31", "gpui", @@ -20483,6 +20519,7 @@ dependencies = [ "activity_indicator", "agent_settings", "agent_ui", + "agent_ui_v2", "anyhow", "ashpd 0.11.0", "askpass", @@ -20794,16 +20831,16 @@ dependencies = [ [[package]] name = "zed_html" -version = "0.2.3" +version = "0.3.0" dependencies = [ "zed_extension_api 0.7.0", ] [[package]] name = "zed_proto" -version = "0.2.3" +version = "0.3.0" dependencies = [ - "zed_extension_api 0.1.0", + "zed_extension_api 0.7.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 523dce229e6b58d98f0ef36070fb068a7b743367..f3a5fefc7168c5296d032ae89ec5817673d9c333 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "crates/agent_servers", "crates/agent_settings", "crates/agent_ui", + "crates/agent_ui_v2", "crates/ai_onboarding", "crates/anthropic", "crates/askpass", @@ -242,6 +243,7 @@ action_log = { path = "crates/action_log" } agent = { path = "crates/agent" } activity_indicator = { path = "crates/activity_indicator" } agent_ui = { path = "crates/agent_ui" } +agent_ui_v2 = { path = "crates/agent_ui_v2" } agent_settings = { path = "crates/agent_settings" } agent_servers = { path = "crates/agent_servers" } ai_onboarding = { path = "crates/ai_onboarding" } diff --git a/assets/icons/zed_agent_two.svg b/assets/icons/zed_agent_two.svg new file mode 100644 index 0000000000000000000000000000000000000000..c352be84d2f1bea6da1f6a5be70b9420f019b6d6 --- /dev/null +++ b/assets/icons/zed_agent_two.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 0bcbb455b502642237347cf9fc36b91eab83f20b..342c4b0b7cb9608c13bed2899dd67b3ac0378db5 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -44,15 +44,15 @@ "f11": "zed::ToggleFullScreen", "ctrl-alt-z": "edit_prediction::RatePredictions", "ctrl-alt-shift-i": "edit_prediction::ToggleMenu", - "ctrl-alt-l": "lsp_tool::ToggleMenu" - } + "ctrl-alt-l": "lsp_tool::ToggleMenu", + }, }, { "context": "Picker || menu", "bindings": { "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } + "down": "menu::SelectNext", + }, }, { "context": "Editor", @@ -63,7 +63,6 @@ "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", { "ignore_newlines": false, "ignore_brackets": false }], @@ -125,8 +124,8 @@ "shift-f10": "editor::OpenContextMenu", "ctrl-alt-shift-e": "editor::ToggleEditPrediction", "f9": "editor::ToggleBreakpoint", - "shift-f9": "editor::EditLogBreakpoint" - } + "shift-f9": "editor::EditLogBreakpoint", + }, }, { "context": "Editor && mode == full", @@ -145,44 +144,44 @@ "ctrl-alt-e": "editor::SelectEnclosingSymbol", "ctrl-shift-backspace": "editor::GoToPreviousChange", "ctrl-shift-alt-backspace": "editor::GoToNextChange", - "alt-enter": "editor::OpenSelectionsInMultibuffer" - } + "alt-enter": "editor::OpenSelectionsInMultibuffer", + }, }, { "context": "Editor && mode == full && edit_prediction", "bindings": { "alt-]": "editor::NextEditPrediction", - "alt-[": "editor::PreviousEditPrediction" - } + "alt-[": "editor::PreviousEditPrediction", + }, }, { "context": "Editor && !edit_prediction", "bindings": { - "alt-\\": "editor::ShowEditPrediction" - } + "alt-\\": "editor::ShowEditPrediction", + }, }, { "context": "Editor && mode == auto_height", "bindings": { "ctrl-enter": "editor::Newline", "shift-enter": "editor::Newline", - "ctrl-shift-enter": "editor::NewlineBelow" - } + "ctrl-shift-enter": "editor::NewlineBelow", + }, }, { "context": "Markdown", "bindings": { "copy": "markdown::Copy", "ctrl-insert": "markdown::Copy", - "ctrl-c": "markdown::Copy" - } + "ctrl-c": "markdown::Copy", + }, }, { "context": "Editor && jupyter && !ContextEditor", "bindings": { "ctrl-shift-enter": "repl::Run", - "ctrl-alt-enter": "repl::RunInPlace" - } + "ctrl-alt-enter": "repl::RunInPlace", + }, }, { "context": "Editor && !agent_diff", @@ -190,8 +189,8 @@ "ctrl-k ctrl-r": "git::Restore", "ctrl-alt-y": "git::ToggleStaged", "alt-y": "git::StageAndNext", - "alt-shift-y": "git::UnstageAndNext" - } + "alt-shift-y": "git::UnstageAndNext", + }, }, { "context": "Editor && editor_agent_diff", @@ -200,8 +199,8 @@ "ctrl-n": "agent::Reject", "ctrl-shift-y": "agent::KeepAll", "ctrl-shift-n": "agent::RejectAll", - "shift-ctrl-r": "agent::OpenAgentDiff" - } + "shift-ctrl-r": "agent::OpenAgentDiff", + }, }, { "context": "AgentDiff", @@ -209,8 +208,8 @@ "ctrl-y": "agent::Keep", "ctrl-n": "agent::Reject", "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } + "ctrl-shift-n": "agent::RejectAll", + }, }, { "context": "ContextEditor > Editor", @@ -226,8 +225,8 @@ "ctrl-k c": "assistant::CopyCode", "ctrl-g": "search::SelectNextMatch", "ctrl-shift-g": "search::SelectPreviousMatch", - "ctrl-k l": "agent::OpenRulesLibrary" - } + "ctrl-k l": "agent::OpenRulesLibrary", + }, }, { "context": "AgentPanel", @@ -251,37 +250,37 @@ "alt-enter": "agent::ContinueWithBurnMode", "ctrl-y": "agent::AllowOnce", "ctrl-alt-y": "agent::AllowAlways", - "ctrl-alt-z": "agent::RejectOnce" - } + "ctrl-alt-z": "agent::RejectOnce", + }, }, { "context": "AgentPanel > NavigationMenu", "bindings": { - "shift-backspace": "agent::DeleteRecentlyOpenThread" - } + "shift-backspace": "agent::DeleteRecentlyOpenThread", + }, }, { "context": "AgentPanel > Markdown", "bindings": { "copy": "markdown::CopyAsMarkdown", "ctrl-insert": "markdown::CopyAsMarkdown", - "ctrl-c": "markdown::CopyAsMarkdown" - } + "ctrl-c": "markdown::CopyAsMarkdown", + }, }, { "context": "AgentPanel && text_thread", "bindings": { "ctrl-n": "agent::NewTextThread", - "ctrl-alt-t": "agent::NewThread" - } + "ctrl-alt-t": "agent::NewThread", + }, }, { "context": "AgentPanel && acp_thread", "use_key_equivalents": true, "bindings": { "ctrl-n": "agent::NewExternalAgentThread", - "ctrl-alt-t": "agent::NewThread" - } + "ctrl-alt-t": "agent::NewThread", + }, }, { "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", @@ -291,8 +290,8 @@ "ctrl-i": "agent::ToggleProfileSelector", "shift-ctrl-r": "agent::OpenAgentDiff", "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } + "ctrl-shift-n": "agent::RejectAll", + }, }, { "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", @@ -302,30 +301,30 @@ "ctrl-i": "agent::ToggleProfileSelector", "shift-ctrl-r": "agent::OpenAgentDiff", "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } + "ctrl-shift-n": "agent::RejectAll", + }, }, { "context": "EditMessageEditor > Editor", "bindings": { "escape": "menu::Cancel", "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } + "alt-enter": "editor::Newline", + }, }, { "context": "AgentFeedbackMessageEditor > Editor", "bindings": { "escape": "menu::Cancel", "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } + "alt-enter": "editor::Newline", + }, }, { "context": "AcpThread > ModeSelector", "bindings": { - "ctrl-enter": "menu::Confirm" - } + "ctrl-enter": "menu::Confirm", + }, }, { "context": "AcpThread > Editor && !use_modifier_to_send", @@ -334,8 +333,8 @@ "enter": "agent::Chat", "shift-ctrl-r": "agent::OpenAgentDiff", "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } + "ctrl-shift-n": "agent::RejectAll", + }, }, { "context": "AcpThread > Editor && use_modifier_to_send", @@ -345,14 +344,14 @@ "shift-ctrl-r": "agent::OpenAgentDiff", "ctrl-shift-y": "agent::KeepAll", "ctrl-shift-n": "agent::RejectAll", - "shift-tab": "agent::CycleModeSelector" - } + "shift-tab": "agent::CycleModeSelector", + }, }, { "context": "ThreadHistory", "bindings": { - "backspace": "agent::RemoveSelectedThread" - } + "backspace": "agent::RemoveSelectedThread", + }, }, { "context": "RulesLibrary", @@ -360,8 +359,8 @@ "new": "rules_library::NewRule", "ctrl-n": "rules_library::NewRule", "ctrl-shift-s": "rules_library::ToggleDefaultRule", - "ctrl-w": "workspace::CloseWindow" - } + "ctrl-w": "workspace::CloseWindow", + }, }, { "context": "BufferSearchBar", @@ -374,22 +373,22 @@ "find": "search::FocusSearch", "ctrl-f": "search::FocusSearch", "ctrl-h": "search::ToggleReplace", - "ctrl-l": "search::ToggleSelection" - } + "ctrl-l": "search::ToggleSelection", + }, }, { "context": "BufferSearchBar && in_replace > Editor", "bindings": { "enter": "search::ReplaceNext", - "ctrl-enter": "search::ReplaceAll" - } + "ctrl-enter": "search::ReplaceAll", + }, }, { "context": "BufferSearchBar && !in_replace > Editor", "bindings": { "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } + "down": "search::NextHistoryQuery", + }, }, { "context": "ProjectSearchBar", @@ -400,22 +399,22 @@ "ctrl-shift-f": "search::FocusSearch", "ctrl-shift-h": "search::ToggleReplace", "alt-ctrl-g": "search::ToggleRegex", - "alt-ctrl-x": "search::ToggleRegex" - } + "alt-ctrl-x": "search::ToggleRegex", + }, }, { "context": "ProjectSearchBar > Editor", "bindings": { "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } + "down": "search::NextHistoryQuery", + }, }, { "context": "ProjectSearchBar && in_replace > Editor", "bindings": { "enter": "search::ReplaceNext", - "ctrl-alt-enter": "search::ReplaceAll" - } + "ctrl-alt-enter": "search::ReplaceAll", + }, }, { "context": "ProjectSearchView", @@ -423,8 +422,8 @@ "escape": "project_search::ToggleFocus", "ctrl-shift-h": "search::ToggleReplace", "alt-ctrl-g": "search::ToggleRegex", - "alt-ctrl-x": "search::ToggleRegex" - } + "alt-ctrl-x": "search::ToggleRegex", + }, }, { "context": "Pane", @@ -473,8 +472,8 @@ "ctrl-alt-shift-r": "search::ToggleRegex", "ctrl-alt-shift-x": "search::ToggleRegex", "alt-r": "search::ToggleRegex", - "ctrl-k shift-enter": "pane::TogglePinTab" - } + "ctrl-k shift-enter": "pane::TogglePinTab", + }, }, // Bindings from VS Code { @@ -501,6 +500,7 @@ "ctrl-k ctrl-i": "editor::Hover", "ctrl-k ctrl-b": "editor::BlameHover", "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }], + "ctrl-k ctrl-c": ["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", @@ -537,31 +537,31 @@ "ctrl-\\": "pane::SplitRight", "ctrl-alt-shift-c": "editor::DisplayCursorNames", "alt-.": "editor::GoToHunk", - "alt-,": "editor::GoToPreviousHunk" - } + "alt-,": "editor::GoToPreviousHunk", + }, }, { "context": "Editor && extension == md", "use_key_equivalents": true, "bindings": { "ctrl-k v": "markdown::OpenPreviewToTheSide", - "ctrl-shift-v": "markdown::OpenPreview" - } + "ctrl-shift-v": "markdown::OpenPreview", + }, }, { "context": "Editor && extension == svg", "use_key_equivalents": true, "bindings": { "ctrl-k v": "svg::OpenPreviewToTheSide", - "ctrl-shift-v": "svg::OpenPreview" - } + "ctrl-shift-v": "svg::OpenPreview", + }, }, { "context": "Editor && mode == full", "bindings": { "ctrl-shift-o": "outline::Toggle", - "ctrl-g": "go_to_line::Toggle" - } + "ctrl-g": "go_to_line::Toggle", + }, }, { "context": "Workspace", @@ -655,28 +655,28 @@ // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }], "f5": "debugger::Rerun", "ctrl-f4": "workspace::CloseActiveDock", - "ctrl-w": "workspace::CloseActiveDock" - } + "ctrl-w": "workspace::CloseActiveDock", + }, }, { "context": "Workspace && debugger_running", "bindings": { - "f5": "zed::NoAction" - } + "f5": "zed::NoAction", + }, }, { "context": "Workspace && debugger_stopped", "bindings": { - "f5": "debugger::Continue" - } + "f5": "debugger::Continue", + }, }, { "context": "ApplicationMenu", "bindings": { "f10": "menu::Cancel", "left": "app_menu::ActivateMenuLeft", - "right": "app_menu::ActivateMenuRight" - } + "right": "app_menu::ActivateMenuRight", + }, }, // Bindings from Sublime Text { @@ -694,8 +694,8 @@ "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart", "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart", "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd", - "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd" - } + "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd", + }, }, // Bindings from Atom { @@ -704,37 +704,37 @@ "ctrl-k up": "pane::SplitUp", "ctrl-k down": "pane::SplitDown", "ctrl-k left": "pane::SplitLeft", - "ctrl-k right": "pane::SplitRight" - } + "ctrl-k right": "pane::SplitRight", + }, }, // Bindings that should be unified with bindings for more general actions { "context": "Editor && renaming", "bindings": { - "enter": "editor::ConfirmRename" - } + "enter": "editor::ConfirmRename", + }, }, { "context": "Editor && showing_completions", "bindings": { "enter": "editor::ConfirmCompletion", "shift-enter": "editor::ConfirmCompletionReplace", - "tab": "editor::ComposeCompletion" - } + "tab": "editor::ComposeCompletion", + }, }, { "context": "Editor && in_snippet && has_next_tabstop && !showing_completions", "use_key_equivalents": true, "bindings": { - "tab": "editor::NextSnippetTabstop" - } + "tab": "editor::NextSnippetTabstop", + }, }, { "context": "Editor && in_snippet && has_previous_tabstop && !showing_completions", "use_key_equivalents": true, "bindings": { - "shift-tab": "editor::PreviousSnippetTabstop" - } + "shift-tab": "editor::PreviousSnippetTabstop", + }, }, // Bindings for accepting edit predictions // @@ -746,22 +746,22 @@ "alt-tab": "editor::AcceptEditPrediction", "alt-l": "editor::AcceptEditPrediction", "tab": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } + "alt-right": "editor::AcceptPartialEditPrediction", + }, }, { "context": "Editor && edit_prediction_conflict", "bindings": { "alt-tab": "editor::AcceptEditPrediction", "alt-l": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } + "alt-right": "editor::AcceptPartialEditPrediction", + }, }, { "context": "Editor && showing_code_actions", "bindings": { - "enter": "editor::ConfirmCodeAction" - } + "enter": "editor::ConfirmCodeAction", + }, }, { "context": "Editor && (showing_code_actions || showing_completions)", @@ -771,29 +771,29 @@ "ctrl-n": "editor::ContextMenuNext", "down": "editor::ContextMenuNext", "pageup": "editor::ContextMenuFirst", - "pagedown": "editor::ContextMenuLast" - } + "pagedown": "editor::ContextMenuLast", + }, }, { "context": "Editor && showing_signature_help && !showing_completions", "bindings": { "up": "editor::SignatureHelpPrevious", - "down": "editor::SignatureHelpNext" - } + "down": "editor::SignatureHelpNext", + }, }, // Custom bindings { "bindings": { "ctrl-alt-shift-f": "workspace::FollowNextCollaborator", // Only available in debug builds: opens an element inspector for development. - "ctrl-alt-i": "dev::ToggleInspector" - } + "ctrl-alt-i": "dev::ToggleInspector", + }, }, { "context": "!Terminal", "bindings": { - "ctrl-shift-c": "collab_panel::ToggleFocus" - } + "ctrl-shift-c": "collab_panel::ToggleFocus", + }, }, { "context": "!ContextEditor > Editor && mode == full", @@ -805,8 +805,8 @@ "ctrl-f8": "editor::GoToHunk", "ctrl-shift-f8": "editor::GoToPreviousHunk", "ctrl-enter": "assistant::InlineAssist", - "ctrl-:": "editor::ToggleInlayHints" - } + "ctrl-:": "editor::ToggleInlayHints", + }, }, { "context": "PromptEditor", @@ -814,8 +814,8 @@ "ctrl-[": "agent::CyclePreviousInlineAssist", "ctrl-]": "agent::CycleNextInlineAssist", "ctrl-shift-enter": "inline_assistant::ThumbsUpResult", - "ctrl-shift-backspace": "inline_assistant::ThumbsDownResult" - } + "ctrl-shift-backspace": "inline_assistant::ThumbsDownResult", + }, }, { "context": "Prompt", @@ -823,14 +823,14 @@ "left": "menu::SelectPrevious", "right": "menu::SelectNext", "h": "menu::SelectPrevious", - "l": "menu::SelectNext" - } + "l": "menu::SelectNext", + }, }, { "context": "ProjectSearchBar && !in_replace", "bindings": { - "ctrl-enter": "project_search::SearchInNew" - } + "ctrl-enter": "project_search::SearchInNew", + }, }, { "context": "OutlinePanel && not_editing", @@ -847,8 +847,8 @@ "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrevious", "alt-enter": "editor::OpenExcerpts", - "ctrl-alt-enter": "editor::OpenExcerptsSplit" - } + "ctrl-alt-enter": "editor::OpenExcerptsSplit", + }, }, { "context": "ProjectPanel", @@ -886,14 +886,14 @@ "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrevious", - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "ProjectPanel && not_editing", "bindings": { - "space": "project_panel::Open" - } + "space": "project_panel::Open", + }, }, { "context": "GitPanel && ChangesList", @@ -914,15 +914,15 @@ "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 }] - } + "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }], + }, }, { "context": "GitPanel && CommitEditor", "use_key_equivalents": true, "bindings": { - "escape": "git::Cancel" - } + "escape": "git::Cancel", + }, }, { "context": "GitCommit > Editor", @@ -931,8 +931,8 @@ "enter": "editor::Newline", "ctrl-enter": "git::Commit", "ctrl-shift-enter": "git::Amend", - "alt-l": "git::GenerateCommitMessage" - } + "alt-l": "git::GenerateCommitMessage", + }, }, { "context": "GitPanel", @@ -948,8 +948,8 @@ "ctrl-space": "git::StageAll", "ctrl-shift-space": "git::UnstageAll", "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend" - } + "ctrl-shift-enter": "git::Amend", + }, }, { "context": "GitDiff > Editor", @@ -957,14 +957,14 @@ "ctrl-enter": "git::Commit", "ctrl-shift-enter": "git::Amend", "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll" - } + "ctrl-shift-space": "git::UnstageAll", + }, }, { "context": "AskPass > Editor", "bindings": { - "enter": "menu::Confirm" - } + "enter": "menu::Confirm", + }, }, { "context": "CommitEditor > Editor", @@ -976,16 +976,16 @@ "ctrl-enter": "git::Commit", "ctrl-shift-enter": "git::Amend", "alt-up": "git_panel::FocusChanges", - "alt-l": "git::GenerateCommitMessage" - } + "alt-l": "git::GenerateCommitMessage", + }, }, { "context": "DebugPanel", "bindings": { "ctrl-t": "debugger::ToggleThreadPicker", "ctrl-i": "debugger::ToggleSessionPicker", - "shift-alt-escape": "debugger::ToggleExpandItem" - } + "shift-alt-escape": "debugger::ToggleExpandItem", + }, }, { "context": "VariableList", @@ -997,8 +997,8 @@ "ctrl-alt-c": "variable_list::CopyVariableName", "delete": "variable_list::RemoveWatch", "backspace": "variable_list::RemoveWatch", - "alt-enter": "variable_list::AddWatch" - } + "alt-enter": "variable_list::AddWatch", + }, }, { "context": "BreakpointList", @@ -1006,35 +1006,35 @@ "space": "debugger::ToggleEnableBreakpoint", "backspace": "debugger::UnsetBreakpoint", "left": "debugger::PreviousBreakpointProperty", - "right": "debugger::NextBreakpointProperty" - } + "right": "debugger::NextBreakpointProperty", + }, }, { "context": "CollabPanel && not_editing", "bindings": { "ctrl-backspace": "collab_panel::Remove", - "space": "menu::Confirm" - } + "space": "menu::Confirm", + }, }, { "context": "CollabPanel", "bindings": { "alt-up": "collab_panel::MoveChannelUp", "alt-down": "collab_panel::MoveChannelDown", - "alt-enter": "collab_panel::OpenSelectedChannelNotes" - } + "alt-enter": "collab_panel::OpenSelectedChannelNotes", + }, }, { "context": "(CollabPanel && editing) > Editor", "bindings": { - "space": "collab_panel::InsertSpace" - } + "space": "collab_panel::InsertSpace", + }, }, { "context": "ChannelModal", "bindings": { - "tab": "channel_modal::ToggleMode" - } + "tab": "channel_modal::ToggleMode", + }, }, { "context": "Picker > Editor", @@ -1043,29 +1043,29 @@ "up": "menu::SelectPrevious", "down": "menu::SelectNext", "tab": "picker::ConfirmCompletion", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }] - } + "alt-enter": ["picker::ConfirmInput", { "secondary": false }], + }, }, { "context": "ChannelModal > Picker > Editor", "bindings": { - "tab": "channel_modal::ToggleMode" - } + "tab": "channel_modal::ToggleMode", + }, }, { "context": "ToolchainSelector", "use_key_equivalents": true, "bindings": { - "ctrl-shift-a": "toolchain::AddToolchain" - } + "ctrl-shift-a": "toolchain::AddToolchain", + }, }, { "context": "FileFinder || (FileFinder > Picker > Editor)", "bindings": { "ctrl-p": "file_finder::Toggle", "ctrl-shift-a": "file_finder::ToggleSplitMenu", - "ctrl-shift-i": "file_finder::ToggleFilterMenu" - } + "ctrl-shift-i": "file_finder::ToggleFilterMenu", + }, }, { "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)", @@ -1074,8 +1074,8 @@ "ctrl-j": "pane::SplitDown", "ctrl-k": "pane::SplitUp", "ctrl-h": "pane::SplitLeft", - "ctrl-l": "pane::SplitRight" - } + "ctrl-l": "pane::SplitRight", + }, }, { "context": "TabSwitcher", @@ -1083,15 +1083,15 @@ "ctrl-shift-tab": "menu::SelectPrevious", "ctrl-up": "menu::SelectPrevious", "ctrl-down": "menu::SelectNext", - "ctrl-backspace": "tab_switcher::CloseSelectedItem" - } + "ctrl-backspace": "tab_switcher::CloseSelectedItem", + }, }, { "context": "StashList || (StashList > Picker > Editor)", "bindings": { "ctrl-shift-backspace": "stash_picker::DropStashItem", - "ctrl-shift-v": "stash_picker::ShowStashItem" - } + "ctrl-shift-v": "stash_picker::ShowStashItem", + }, }, { "context": "Terminal", @@ -1136,58 +1136,58 @@ "ctrl-shift-r": "terminal::RerunTask", "ctrl-alt-r": "terminal::RerunTask", "alt-t": "terminal::RerunTask", - "ctrl-shift-5": "pane::SplitRight" - } + "ctrl-shift-5": "pane::SplitRight", + }, }, { "context": "ZedPredictModal", "bindings": { - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "ConfigureContextServerModal > Editor", "bindings": { "escape": "menu::Cancel", "enter": "editor::Newline", - "ctrl-enter": "menu::Confirm" - } + "ctrl-enter": "menu::Confirm", + }, }, { "context": "ContextServerToolsModal", "use_key_equivalents": true, "bindings": { - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "OnboardingAiConfigurationModal", "use_key_equivalents": true, "bindings": { - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "Diagnostics", "use_key_equivalents": true, "bindings": { - "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh" - } + "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh", + }, }, { "context": "DebugConsole > Editor", "use_key_equivalents": true, "bindings": { "enter": "menu::Confirm", - "alt-enter": "console::WatchExpression" - } + "alt-enter": "console::WatchExpression", + }, }, { "context": "RunModal", "bindings": { "ctrl-tab": "pane::ActivateNextItem", - "ctrl-shift-tab": "pane::ActivatePreviousItem" - } + "ctrl-shift-tab": "pane::ActivatePreviousItem", + }, }, { "context": "MarkdownPreview", @@ -1197,8 +1197,8 @@ "up": "markdown::ScrollUp", "down": "markdown::ScrollDown", "alt-up": "markdown::ScrollUpByItem", - "alt-down": "markdown::ScrollDownByItem" - } + "alt-down": "markdown::ScrollDownByItem", + }, }, { "context": "KeymapEditor", @@ -1212,8 +1212,8 @@ "alt-enter": "keymap_editor::CreateBinding", "ctrl-c": "keymap_editor::CopyAction", "ctrl-shift-c": "keymap_editor::CopyContext", - "ctrl-t": "keymap_editor::ShowMatchingKeybinds" - } + "ctrl-t": "keymap_editor::ShowMatchingKeybinds", + }, }, { "context": "KeystrokeInput", @@ -1221,24 +1221,24 @@ "bindings": { "enter": "keystroke_input::StartRecording", "escape escape escape": "keystroke_input::StopRecording", - "delete": "keystroke_input::ClearKeystrokes" - } + "delete": "keystroke_input::ClearKeystrokes", + }, }, { "context": "KeybindEditorModal", "use_key_equivalents": true, "bindings": { "ctrl-enter": "menu::Confirm", - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "KeybindEditorModal > Editor", "use_key_equivalents": true, "bindings": { "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } + "down": "menu::SelectNext", + }, }, { "context": "Onboarding", @@ -1250,8 +1250,8 @@ "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }], "ctrl-enter": "onboarding::Finish", "alt-shift-l": "onboarding::SignIn", - "alt-shift-a": "onboarding::OpenAccount" - } + "alt-shift-a": "onboarding::OpenAccount", + }, }, { "context": "Welcome", @@ -1260,23 +1260,23 @@ "ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }], "ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }], "ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }], - "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }] - } + "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }], + }, }, { "context": "InvalidBuffer", "use_key_equivalents": true, "bindings": { - "ctrl-shift-enter": "workspace::OpenWithSystem" - } + "ctrl-shift-enter": "workspace::OpenWithSystem", + }, }, { "context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)", "use_key_equivalents": true, "bindings": { "ctrl-shift-space": "git::WorktreeFromDefaultOnWindow", - "ctrl-space": "git::WorktreeFromDefault" - } + "ctrl-space": "git::WorktreeFromDefault", + }, }, { "context": "SettingsWindow", @@ -1301,16 +1301,16 @@ "ctrl-9": ["settings_editor::FocusFile", 8], "ctrl-0": ["settings_editor::FocusFile", 9], "ctrl-pageup": "settings_editor::FocusPreviousFile", - "ctrl-pagedown": "settings_editor::FocusNextFile" - } + "ctrl-pagedown": "settings_editor::FocusNextFile", + }, }, { "context": "StashDiff > Editor", "bindings": { "ctrl-space": "git::ApplyCurrentStash", "ctrl-shift-space": "git::PopCurrentStash", - "ctrl-shift-backspace": "git::DropCurrentStash" - } + "ctrl-shift-backspace": "git::DropCurrentStash", + }, }, { "context": "SettingsWindow > NavigationMenu", @@ -1325,22 +1325,22 @@ "pageup": "settings_editor::FocusPreviousRootNavEntry", "pagedown": "settings_editor::FocusNextRootNavEntry", "home": "settings_editor::FocusFirstNavEntry", - "end": "settings_editor::FocusLastNavEntry" - } + "end": "settings_editor::FocusLastNavEntry", + }, }, { "context": "EditPredictionContext > Editor", "bindings": { "alt-left": "dev::EditPredictionContextGoBack", - "alt-right": "dev::EditPredictionContextGoForward" - } + "alt-right": "dev::EditPredictionContextGoForward", + }, }, { "context": "GitBranchSelector || (GitBranchSelector > Picker > Editor)", "use_key_equivalents": true, "bindings": { "ctrl-shift-backspace": "branch_picker::DeleteBranch", - "ctrl-shift-i": "branch_picker::FilterRemotes" - } - } + "ctrl-shift-i": "branch_picker::FilterRemotes", + }, + }, ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 65ac280ba7f782cef417aef220dacd7f32f9e6ff..50fc0c7222b76c9e5218c47a481442534debe2b0 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -50,8 +50,8 @@ "ctrl-cmd-z": "edit_prediction::RatePredictions", "ctrl-cmd-i": "edit_prediction::ToggleMenu", "ctrl-cmd-l": "lsp_tool::ToggleMenu", - "ctrl-cmd-c": "editor::DisplayCursorNames" - } + "ctrl-cmd-c": "editor::DisplayCursorNames", + }, }, { "context": "Editor", @@ -148,8 +148,8 @@ "shift-f9": "editor::EditLogBreakpoint", "ctrl-f12": "editor::GoToDeclaration", "alt-ctrl-f12": "editor::GoToDeclarationSplit", - "ctrl-cmd-e": "editor::ToggleEditPrediction" - } + "ctrl-cmd-e": "editor::ToggleEditPrediction", + }, }, { "context": "Editor && mode == full", @@ -167,8 +167,8 @@ "cmd->": "agent::AddSelectionToThread", "cmd-<": "assistant::InsertIntoEditor", "cmd-alt-e": "editor::SelectEnclosingSymbol", - "alt-enter": "editor::OpenSelectionsInMultibuffer" - } + "alt-enter": "editor::OpenSelectionsInMultibuffer", + }, }, { "context": "Editor && multibuffer", @@ -177,23 +177,23 @@ "cmd-up": "editor::MoveToStartOfExcerpt", "cmd-down": "editor::MoveToStartOfNextExcerpt", "cmd-shift-up": "editor::SelectToStartOfExcerpt", - "cmd-shift-down": "editor::SelectToStartOfNextExcerpt" - } + "cmd-shift-down": "editor::SelectToStartOfNextExcerpt", + }, }, { "context": "Editor && mode == full && edit_prediction", "use_key_equivalents": true, "bindings": { "alt-tab": "editor::NextEditPrediction", - "alt-shift-tab": "editor::PreviousEditPrediction" - } + "alt-shift-tab": "editor::PreviousEditPrediction", + }, }, { "context": "Editor && !edit_prediction", "use_key_equivalents": true, "bindings": { - "alt-tab": "editor::ShowEditPrediction" - } + "alt-tab": "editor::ShowEditPrediction", + }, }, { "context": "Editor && mode == auto_height", @@ -201,23 +201,23 @@ "bindings": { "ctrl-enter": "editor::Newline", "shift-enter": "editor::Newline", - "ctrl-shift-enter": "editor::NewlineBelow" - } + "ctrl-shift-enter": "editor::NewlineBelow", + }, }, { "context": "Markdown", "use_key_equivalents": true, "bindings": { - "cmd-c": "markdown::Copy" - } + "cmd-c": "markdown::Copy", + }, }, { "context": "Editor && jupyter && !ContextEditor", "use_key_equivalents": true, "bindings": { "ctrl-shift-enter": "repl::Run", - "ctrl-alt-enter": "repl::RunInPlace" - } + "ctrl-alt-enter": "repl::RunInPlace", + }, }, { "context": "Editor && !agent_diff && !AgentPanel", @@ -226,8 +226,8 @@ "cmd-alt-z": "git::Restore", "cmd-alt-y": "git::ToggleStaged", "cmd-y": "git::StageAndNext", - "cmd-shift-y": "git::UnstageAndNext" - } + "cmd-shift-y": "git::UnstageAndNext", + }, }, { "context": "AgentDiff", @@ -236,8 +236,8 @@ "cmd-y": "agent::Keep", "cmd-n": "agent::Reject", "cmd-shift-y": "agent::KeepAll", - "cmd-shift-n": "agent::RejectAll" - } + "cmd-shift-n": "agent::RejectAll", + }, }, { "context": "Editor && editor_agent_diff", @@ -247,8 +247,8 @@ "cmd-n": "agent::Reject", "cmd-shift-y": "agent::KeepAll", "cmd-shift-n": "agent::RejectAll", - "shift-ctrl-r": "agent::OpenAgentDiff" - } + "shift-ctrl-r": "agent::OpenAgentDiff", + }, }, { "context": "ContextEditor > Editor", @@ -264,8 +264,8 @@ "cmd-k c": "assistant::CopyCode", "cmd-g": "search::SelectNextMatch", "cmd-shift-g": "search::SelectPreviousMatch", - "cmd-k l": "agent::OpenRulesLibrary" - } + "cmd-k l": "agent::OpenRulesLibrary", + }, }, { "context": "AgentPanel", @@ -290,37 +290,37 @@ "alt-enter": "agent::ContinueWithBurnMode", "cmd-y": "agent::AllowOnce", "cmd-alt-y": "agent::AllowAlways", - "cmd-alt-z": "agent::RejectOnce" - } + "cmd-alt-z": "agent::RejectOnce", + }, }, { "context": "AgentPanel > NavigationMenu", "bindings": { - "shift-backspace": "agent::DeleteRecentlyOpenThread" - } + "shift-backspace": "agent::DeleteRecentlyOpenThread", + }, }, { "context": "AgentPanel > Markdown", "use_key_equivalents": true, "bindings": { - "cmd-c": "markdown::CopyAsMarkdown" - } + "cmd-c": "markdown::CopyAsMarkdown", + }, }, { "context": "AgentPanel && text_thread", "use_key_equivalents": true, "bindings": { "cmd-n": "agent::NewTextThread", - "cmd-alt-n": "agent::NewExternalAgentThread" - } + "cmd-alt-n": "agent::NewExternalAgentThread", + }, }, { "context": "AgentPanel && acp_thread", "use_key_equivalents": true, "bindings": { "cmd-n": "agent::NewExternalAgentThread", - "cmd-alt-t": "agent::NewThread" - } + "cmd-alt-t": "agent::NewThread", + }, }, { "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", @@ -331,8 +331,8 @@ "cmd-i": "agent::ToggleProfileSelector", "shift-ctrl-r": "agent::OpenAgentDiff", "cmd-shift-y": "agent::KeepAll", - "cmd-shift-n": "agent::RejectAll" - } + "cmd-shift-n": "agent::RejectAll", + }, }, { "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", @@ -343,8 +343,8 @@ "cmd-i": "agent::ToggleProfileSelector", "shift-ctrl-r": "agent::OpenAgentDiff", "cmd-shift-y": "agent::KeepAll", - "cmd-shift-n": "agent::RejectAll" - } + "cmd-shift-n": "agent::RejectAll", + }, }, { "context": "EditMessageEditor > Editor", @@ -352,8 +352,8 @@ "bindings": { "escape": "menu::Cancel", "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } + "alt-enter": "editor::Newline", + }, }, { "context": "AgentFeedbackMessageEditor > Editor", @@ -361,20 +361,20 @@ "bindings": { "escape": "menu::Cancel", "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } + "alt-enter": "editor::Newline", + }, }, { "context": "AgentConfiguration", "bindings": { - "ctrl--": "pane::GoBack" - } + "ctrl--": "pane::GoBack", + }, }, { "context": "AcpThread > ModeSelector", "bindings": { - "cmd-enter": "menu::Confirm" - } + "cmd-enter": "menu::Confirm", + }, }, { "context": "AcpThread > Editor && !use_modifier_to_send", @@ -384,8 +384,8 @@ "shift-ctrl-r": "agent::OpenAgentDiff", "cmd-shift-y": "agent::KeepAll", "cmd-shift-n": "agent::RejectAll", - "shift-tab": "agent::CycleModeSelector" - } + "shift-tab": "agent::CycleModeSelector", + }, }, { "context": "AcpThread > Editor && use_modifier_to_send", @@ -395,20 +395,20 @@ "shift-ctrl-r": "agent::OpenAgentDiff", "cmd-shift-y": "agent::KeepAll", "cmd-shift-n": "agent::RejectAll", - "shift-tab": "agent::CycleModeSelector" - } + "shift-tab": "agent::CycleModeSelector", + }, }, { "context": "ThreadHistory", "bindings": { - "ctrl--": "pane::GoBack" - } + "ctrl--": "pane::GoBack", + }, }, { "context": "ThreadHistory > Editor", "bindings": { - "shift-backspace": "agent::RemoveSelectedThread" - } + "shift-backspace": "agent::RemoveSelectedThread", + }, }, { "context": "RulesLibrary", @@ -416,8 +416,8 @@ "bindings": { "cmd-n": "rules_library::NewRule", "cmd-shift-s": "rules_library::ToggleDefaultRule", - "cmd-w": "workspace::CloseWindow" - } + "cmd-w": "workspace::CloseWindow", + }, }, { "context": "BufferSearchBar", @@ -431,24 +431,24 @@ "cmd-f": "search::FocusSearch", "cmd-alt-f": "search::ToggleReplace", "cmd-alt-l": "search::ToggleSelection", - "cmd-shift-o": "outline::Toggle" - } + "cmd-shift-o": "outline::Toggle", + }, }, { "context": "BufferSearchBar && in_replace > Editor", "use_key_equivalents": true, "bindings": { "enter": "search::ReplaceNext", - "cmd-enter": "search::ReplaceAll" - } + "cmd-enter": "search::ReplaceAll", + }, }, { "context": "BufferSearchBar && !in_replace > Editor", "use_key_equivalents": true, "bindings": { "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } + "down": "search::NextHistoryQuery", + }, }, { "context": "ProjectSearchBar", @@ -460,24 +460,24 @@ "cmd-shift-f": "search::FocusSearch", "cmd-shift-h": "search::ToggleReplace", "alt-cmd-g": "search::ToggleRegex", - "alt-cmd-x": "search::ToggleRegex" - } + "alt-cmd-x": "search::ToggleRegex", + }, }, { "context": "ProjectSearchBar > Editor", "use_key_equivalents": true, "bindings": { "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } + "down": "search::NextHistoryQuery", + }, }, { "context": "ProjectSearchBar && in_replace > Editor", "use_key_equivalents": true, "bindings": { "enter": "search::ReplaceNext", - "cmd-enter": "search::ReplaceAll" - } + "cmd-enter": "search::ReplaceAll", + }, }, { "context": "ProjectSearchView", @@ -488,8 +488,8 @@ "shift-enter": "project_search::ToggleAllSearchResults", "cmd-shift-h": "search::ToggleReplace", "alt-cmd-g": "search::ToggleRegex", - "alt-cmd-x": "search::ToggleRegex" - } + "alt-cmd-x": "search::ToggleRegex", + }, }, { "context": "Pane", @@ -519,8 +519,8 @@ "alt-cmd-w": "search::ToggleWholeWord", "alt-cmd-f": "project_search::ToggleFilters", "alt-cmd-x": "search::ToggleRegex", - "cmd-k shift-enter": "pane::TogglePinTab" - } + "cmd-k shift-enter": "pane::TogglePinTab", + }, }, // Bindings from VS Code { @@ -590,24 +590,24 @@ "cmd-.": "editor::ToggleCodeActions", "cmd-k r": "editor::RevealInFileManager", "cmd-k p": "editor::CopyPath", - "cmd-\\": "pane::SplitRight" - } + "cmd-\\": "pane::SplitRight", + }, }, { "context": "Editor && extension == md", "use_key_equivalents": true, "bindings": { "cmd-k v": "markdown::OpenPreviewToTheSide", - "cmd-shift-v": "markdown::OpenPreview" - } + "cmd-shift-v": "markdown::OpenPreview", + }, }, { "context": "Editor && extension == svg", "use_key_equivalents": true, "bindings": { "cmd-k v": "svg::OpenPreviewToTheSide", - "cmd-shift-v": "svg::OpenPreview" - } + "cmd-shift-v": "svg::OpenPreview", + }, }, { "context": "Editor && mode == full", @@ -616,8 +616,8 @@ "cmd-shift-o": "outline::Toggle", "ctrl-g": "go_to_line::Toggle", "cmd-shift-backspace": "editor::GoToPreviousChange", - "cmd-shift-alt-backspace": "editor::GoToNextChange" - } + "cmd-shift-alt-backspace": "editor::GoToNextChange", + }, }, { "context": "Pane", @@ -635,8 +635,8 @@ "ctrl-0": "pane::ActivateLastItem", "ctrl--": "pane::GoBack", "ctrl-_": "pane::GoForward", - "cmd-shift-f": "pane::DeploySearch" - } + "cmd-shift-f": "pane::DeploySearch", + }, }, { "context": "Workspace", @@ -707,8 +707,8 @@ "cmd-k shift-down": "workspace::SwapPaneDown", "cmd-shift-x": "zed::Extensions", "f5": "debugger::Rerun", - "cmd-w": "workspace::CloseActiveDock" - } + "cmd-w": "workspace::CloseActiveDock", + }, }, { "context": "Workspace && !Terminal", @@ -719,27 +719,27 @@ // All task parameters are captured and unchanged between reruns by default. // Use the `"reevaluate_context"` parameter to control this. "cmd-alt-r": ["task::Rerun", { "reevaluate_context": false }], - "ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }] + "ctrl-alt-shift-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" }], - } + }, }, { "context": "Workspace && debugger_running", "use_key_equivalents": true, "bindings": { "f5": "zed::NoAction", - "f11": "debugger::StepInto" - } + "f11": "debugger::StepInto", + }, }, { "context": "Workspace && debugger_stopped", "use_key_equivalents": true, "bindings": { - "f5": "debugger::Continue" - } + "f5": "debugger::Continue", + }, }, // Bindings from Sublime Text { @@ -760,8 +760,8 @@ "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart", "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart", "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd", - "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd" - } + "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd", + }, }, // Bindings from Atom { @@ -771,16 +771,16 @@ "cmd-k up": "pane::SplitUp", "cmd-k down": "pane::SplitDown", "cmd-k left": "pane::SplitLeft", - "cmd-k right": "pane::SplitRight" - } + "cmd-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" - } + "enter": "editor::ConfirmRename", + }, }, { "context": "Editor && showing_completions", @@ -788,45 +788,45 @@ "bindings": { "enter": "editor::ConfirmCompletion", "shift-enter": "editor::ConfirmCompletionReplace", - "tab": "editor::ComposeCompletion" - } + "tab": "editor::ComposeCompletion", + }, }, { "context": "Editor && in_snippet && has_next_tabstop && !showing_completions", "use_key_equivalents": true, "bindings": { - "tab": "editor::NextSnippetTabstop" - } + "tab": "editor::NextSnippetTabstop", + }, }, { "context": "Editor && in_snippet && has_previous_tabstop && !showing_completions", "use_key_equivalents": true, "bindings": { - "shift-tab": "editor::PreviousSnippetTabstop" - } + "shift-tab": "editor::PreviousSnippetTabstop", + }, }, { "context": "Editor && edit_prediction", "bindings": { "alt-tab": "editor::AcceptEditPrediction", "tab": "editor::AcceptEditPrediction", - "ctrl-cmd-right": "editor::AcceptPartialEditPrediction" - } + "ctrl-cmd-right": "editor::AcceptPartialEditPrediction", + }, }, { "context": "Editor && edit_prediction_conflict", "use_key_equivalents": true, "bindings": { "alt-tab": "editor::AcceptEditPrediction", - "ctrl-cmd-right": "editor::AcceptPartialEditPrediction" - } + "ctrl-cmd-right": "editor::AcceptPartialEditPrediction", + }, }, { "context": "Editor && showing_code_actions", "use_key_equivalents": true, "bindings": { - "enter": "editor::ConfirmCodeAction" - } + "enter": "editor::ConfirmCodeAction", + }, }, { "context": "Editor && (showing_code_actions || showing_completions)", @@ -837,15 +837,15 @@ "down": "editor::ContextMenuNext", "ctrl-n": "editor::ContextMenuNext", "pageup": "editor::ContextMenuFirst", - "pagedown": "editor::ContextMenuLast" - } + "pagedown": "editor::ContextMenuLast", + }, }, { "context": "Editor && showing_signature_help && !showing_completions", "bindings": { "up": "editor::SignatureHelpPrevious", - "down": "editor::SignatureHelpNext" - } + "down": "editor::SignatureHelpNext", + }, }, // Custom bindings { @@ -855,8 +855,8 @@ // TODO: Move this to a dock open action "cmd-shift-c": "collab_panel::ToggleFocus", // Only available in debug builds: opens an element inspector for development. - "cmd-alt-i": "dev::ToggleInspector" - } + "cmd-alt-i": "dev::ToggleInspector", + }, }, { "context": "!ContextEditor > Editor && mode == full", @@ -869,8 +869,8 @@ "cmd-f8": "editor::GoToHunk", "cmd-shift-f8": "editor::GoToPreviousHunk", "ctrl-enter": "assistant::InlineAssist", - "ctrl-:": "editor::ToggleInlayHints" - } + "ctrl-:": "editor::ToggleInlayHints", + }, }, { "context": "PromptEditor", @@ -880,8 +880,8 @@ "ctrl-[": "agent::CyclePreviousInlineAssist", "ctrl-]": "agent::CycleNextInlineAssist", "cmd-shift-enter": "inline_assistant::ThumbsUpResult", - "cmd-shift-backspace": "inline_assistant::ThumbsDownResult" - } + "cmd-shift-backspace": "inline_assistant::ThumbsDownResult", + }, }, { "context": "Prompt", @@ -890,15 +890,15 @@ "left": "menu::SelectPrevious", "right": "menu::SelectNext", "h": "menu::SelectPrevious", - "l": "menu::SelectNext" - } + "l": "menu::SelectNext", + }, }, { "context": "ProjectSearchBar && !in_replace", "use_key_equivalents": true, "bindings": { - "cmd-enter": "project_search::SearchInNew" - } + "cmd-enter": "project_search::SearchInNew", + }, }, { "context": "OutlinePanel && not_editing", @@ -914,8 +914,8 @@ "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrevious", "alt-enter": "editor::OpenExcerpts", - "cmd-alt-enter": "editor::OpenExcerptsSplit" - } + "cmd-alt-enter": "editor::OpenExcerptsSplit", + }, }, { "context": "ProjectPanel", @@ -945,15 +945,15 @@ "cmd-alt-shift-f": "project_panel::NewSearchInDirectory", "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrevious", - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "ProjectPanel && not_editing", "use_key_equivalents": true, "bindings": { - "space": "project_panel::Open" - } + "space": "project_panel::Open", + }, }, { "context": "VariableList", @@ -966,8 +966,8 @@ "cmd-alt-c": "variable_list::CopyVariableName", "delete": "variable_list::RemoveWatch", "backspace": "variable_list::RemoveWatch", - "alt-enter": "variable_list::AddWatch" - } + "alt-enter": "variable_list::AddWatch", + }, }, { "context": "GitPanel && ChangesList", @@ -990,15 +990,15 @@ "backspace": ["git::RestoreFile", { "skip_prompt": false }], "delete": ["git::RestoreFile", { "skip_prompt": false }], "cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }], - "cmd-delete": ["git::RestoreFile", { "skip_prompt": true }] - } + "cmd-delete": ["git::RestoreFile", { "skip_prompt": true }], + }, }, { "context": "GitPanel && CommitEditor", "use_key_equivalents": true, "bindings": { - "escape": "git::Cancel" - } + "escape": "git::Cancel", + }, }, { "context": "GitDiff > Editor", @@ -1007,8 +1007,8 @@ "cmd-enter": "git::Commit", "cmd-shift-enter": "git::Amend", "cmd-ctrl-y": "git::StageAll", - "cmd-ctrl-shift-y": "git::UnstageAll" - } + "cmd-ctrl-shift-y": "git::UnstageAll", + }, }, { "context": "CommitEditor > Editor", @@ -1021,8 +1021,8 @@ "shift-tab": "git_panel::FocusChanges", "alt-up": "git_panel::FocusChanges", "shift-escape": "git::ExpandCommitEditor", - "alt-tab": "git::GenerateCommitMessage" - } + "alt-tab": "git::GenerateCommitMessage", + }, }, { "context": "GitPanel", @@ -1039,8 +1039,8 @@ "cmd-ctrl-y": "git::StageAll", "cmd-ctrl-shift-y": "git::UnstageAll", "cmd-enter": "git::Commit", - "cmd-shift-enter": "git::Amend" - } + "cmd-shift-enter": "git::Amend", + }, }, { "context": "GitCommit > Editor", @@ -1050,16 +1050,16 @@ "escape": "menu::Cancel", "cmd-enter": "git::Commit", "cmd-shift-enter": "git::Amend", - "alt-tab": "git::GenerateCommitMessage" - } + "alt-tab": "git::GenerateCommitMessage", + }, }, { "context": "DebugPanel", "bindings": { "cmd-t": "debugger::ToggleThreadPicker", "cmd-i": "debugger::ToggleSessionPicker", - "shift-alt-escape": "debugger::ToggleExpandItem" - } + "shift-alt-escape": "debugger::ToggleExpandItem", + }, }, { "context": "BreakpointList", @@ -1067,16 +1067,16 @@ "space": "debugger::ToggleEnableBreakpoint", "backspace": "debugger::UnsetBreakpoint", "left": "debugger::PreviousBreakpointProperty", - "right": "debugger::NextBreakpointProperty" - } + "right": "debugger::NextBreakpointProperty", + }, }, { "context": "CollabPanel && not_editing", "use_key_equivalents": true, "bindings": { "ctrl-backspace": "collab_panel::Remove", - "space": "menu::Confirm" - } + "space": "menu::Confirm", + }, }, { "context": "CollabPanel", @@ -1084,22 +1084,22 @@ "bindings": { "alt-up": "collab_panel::MoveChannelUp", "alt-down": "collab_panel::MoveChannelDown", - "alt-enter": "collab_panel::OpenSelectedChannelNotes" - } + "alt-enter": "collab_panel::OpenSelectedChannelNotes", + }, }, { "context": "(CollabPanel && editing) > Editor", "use_key_equivalents": true, "bindings": { - "space": "collab_panel::InsertSpace" - } + "space": "collab_panel::InsertSpace", + }, }, { "context": "ChannelModal", "use_key_equivalents": true, "bindings": { - "tab": "channel_modal::ToggleMode" - } + "tab": "channel_modal::ToggleMode", + }, }, { "context": "Picker > Editor", @@ -1110,30 +1110,30 @@ "down": "menu::SelectNext", "tab": "picker::ConfirmCompletion", "alt-enter": ["picker::ConfirmInput", { "secondary": false }], - "cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }] - } + "cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }], + }, }, { "context": "ChannelModal > Picker > Editor", "use_key_equivalents": true, "bindings": { - "tab": "channel_modal::ToggleMode" - } + "tab": "channel_modal::ToggleMode", + }, }, { "context": "ToolchainSelector", "use_key_equivalents": true, "bindings": { - "cmd-shift-a": "toolchain::AddToolchain" - } + "cmd-shift-a": "toolchain::AddToolchain", + }, }, { "context": "FileFinder || (FileFinder > Picker > Editor)", "use_key_equivalents": true, "bindings": { "cmd-shift-a": "file_finder::ToggleSplitMenu", - "cmd-shift-i": "file_finder::ToggleFilterMenu" - } + "cmd-shift-i": "file_finder::ToggleFilterMenu", + }, }, { "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)", @@ -1143,8 +1143,8 @@ "cmd-j": "pane::SplitDown", "cmd-k": "pane::SplitUp", "cmd-h": "pane::SplitLeft", - "cmd-l": "pane::SplitRight" - } + "cmd-l": "pane::SplitRight", + }, }, { "context": "TabSwitcher", @@ -1153,16 +1153,16 @@ "ctrl-shift-tab": "menu::SelectPrevious", "ctrl-up": "menu::SelectPrevious", "ctrl-down": "menu::SelectNext", - "ctrl-backspace": "tab_switcher::CloseSelectedItem" - } + "ctrl-backspace": "tab_switcher::CloseSelectedItem", + }, }, { "context": "StashList || (StashList > Picker > Editor)", "use_key_equivalents": true, "bindings": { "ctrl-shift-backspace": "stash_picker::DropStashItem", - "ctrl-shift-v": "stash_picker::ShowStashItem" - } + "ctrl-shift-v": "stash_picker::ShowStashItem", + }, }, { "context": "Terminal", @@ -1217,8 +1217,8 @@ "ctrl-alt-left": "pane::SplitLeft", "ctrl-alt-right": "pane::SplitRight", "cmd-d": "pane::SplitRight", - "cmd-alt-r": "terminal::RerunTask" - } + "cmd-alt-r": "terminal::RerunTask", + }, }, { "context": "RatePredictionsModal", @@ -1228,8 +1228,8 @@ "cmd-shift-backspace": "zeta::ThumbsDownActivePrediction", "shift-down": "zeta::NextEdit", "shift-up": "zeta::PreviousEdit", - "right": "zeta::PreviewPrediction" - } + "right": "zeta::PreviewPrediction", + }, }, { "context": "RatePredictionsModal > Editor", @@ -1237,15 +1237,15 @@ "bindings": { "escape": "zeta::FocusPredictions", "cmd-shift-enter": "zeta::ThumbsUpActivePrediction", - "cmd-shift-backspace": "zeta::ThumbsDownActivePrediction" - } + "cmd-shift-backspace": "zeta::ThumbsDownActivePrediction", + }, }, { "context": "ZedPredictModal", "use_key_equivalents": true, "bindings": { - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "ConfigureContextServerModal > Editor", @@ -1253,45 +1253,45 @@ "bindings": { "escape": "menu::Cancel", "enter": "editor::Newline", - "cmd-enter": "menu::Confirm" - } + "cmd-enter": "menu::Confirm", + }, }, { "context": "ContextServerToolsModal", "use_key_equivalents": true, "bindings": { - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "OnboardingAiConfigurationModal", "use_key_equivalents": true, "bindings": { - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "Diagnostics", "use_key_equivalents": true, "bindings": { - "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh" - } + "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh", + }, }, { "context": "DebugConsole > Editor", "use_key_equivalents": true, "bindings": { "enter": "menu::Confirm", - "alt-enter": "console::WatchExpression" - } + "alt-enter": "console::WatchExpression", + }, }, { "context": "RunModal", "use_key_equivalents": true, "bindings": { "ctrl-tab": "pane::ActivateNextItem", - "ctrl-shift-tab": "pane::ActivatePreviousItem" - } + "ctrl-shift-tab": "pane::ActivatePreviousItem", + }, }, { "context": "MarkdownPreview", @@ -1301,8 +1301,8 @@ "up": "markdown::ScrollUp", "down": "markdown::ScrollDown", "alt-up": "markdown::ScrollUpByItem", - "alt-down": "markdown::ScrollDownByItem" - } + "alt-down": "markdown::ScrollDownByItem", + }, }, { "context": "KeymapEditor", @@ -1315,8 +1315,8 @@ "alt-enter": "keymap_editor::CreateBinding", "cmd-c": "keymap_editor::CopyAction", "cmd-shift-c": "keymap_editor::CopyContext", - "cmd-t": "keymap_editor::ShowMatchingKeybinds" - } + "cmd-t": "keymap_editor::ShowMatchingKeybinds", + }, }, { "context": "KeystrokeInput", @@ -1324,24 +1324,24 @@ "bindings": { "enter": "keystroke_input::StartRecording", "escape escape escape": "keystroke_input::StopRecording", - "delete": "keystroke_input::ClearKeystrokes" - } + "delete": "keystroke_input::ClearKeystrokes", + }, }, { "context": "KeybindEditorModal", "use_key_equivalents": true, "bindings": { "cmd-enter": "menu::Confirm", - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "KeybindEditorModal > Editor", "use_key_equivalents": true, "bindings": { "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } + "down": "menu::SelectNext", + }, }, { "context": "Onboarding", @@ -1353,8 +1353,8 @@ "cmd-0": ["zed::ResetUiFontSize", { "persist": false }], "cmd-enter": "onboarding::Finish", "alt-tab": "onboarding::SignIn", - "alt-shift-a": "onboarding::OpenAccount" - } + "alt-shift-a": "onboarding::OpenAccount", + }, }, { "context": "Welcome", @@ -1363,23 +1363,23 @@ "cmd-=": ["zed::IncreaseUiFontSize", { "persist": false }], "cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }], "cmd--": ["zed::DecreaseUiFontSize", { "persist": false }], - "cmd-0": ["zed::ResetUiFontSize", { "persist": false }] - } + "cmd-0": ["zed::ResetUiFontSize", { "persist": false }], + }, }, { "context": "InvalidBuffer", "use_key_equivalents": true, "bindings": { - "ctrl-shift-enter": "workspace::OpenWithSystem" - } + "ctrl-shift-enter": "workspace::OpenWithSystem", + }, }, { "context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)", "use_key_equivalents": true, "bindings": { "ctrl-shift-space": "git::WorktreeFromDefaultOnWindow", - "ctrl-space": "git::WorktreeFromDefault" - } + "ctrl-space": "git::WorktreeFromDefault", + }, }, { "context": "SettingsWindow", @@ -1404,8 +1404,8 @@ "ctrl-9": ["settings_editor::FocusFile", 8], "ctrl-0": ["settings_editor::FocusFile", 9], "cmd-{": "settings_editor::FocusPreviousFile", - "cmd-}": "settings_editor::FocusNextFile" - } + "cmd-}": "settings_editor::FocusNextFile", + }, }, { "context": "StashDiff > Editor", @@ -1413,8 +1413,8 @@ "bindings": { "ctrl-space": "git::ApplyCurrentStash", "ctrl-shift-space": "git::PopCurrentStash", - "ctrl-shift-backspace": "git::DropCurrentStash" - } + "ctrl-shift-backspace": "git::DropCurrentStash", + }, }, { "context": "SettingsWindow > NavigationMenu", @@ -1429,22 +1429,22 @@ "pageup": "settings_editor::FocusPreviousRootNavEntry", "pagedown": "settings_editor::FocusNextRootNavEntry", "home": "settings_editor::FocusFirstNavEntry", - "end": "settings_editor::FocusLastNavEntry" - } + "end": "settings_editor::FocusLastNavEntry", + }, }, { "context": "EditPredictionContext > Editor", "bindings": { "alt-left": "dev::EditPredictionContextGoBack", - "alt-right": "dev::EditPredictionContextGoForward" - } + "alt-right": "dev::EditPredictionContextGoForward", + }, }, { "context": "GitBranchSelector || (GitBranchSelector > Picker > Editor)", "use_key_equivalents": true, "bindings": { "cmd-shift-backspace": "branch_picker::DeleteBranch", - "cmd-shift-i": "branch_picker::FilterRemotes" - } - } + "cmd-shift-i": "branch_picker::FilterRemotes", + }, + }, ] diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 51943ab35587e633a25eb9420c45dff21048330a..61793d2158d35ed25f71da3606534d64b523de9f 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -42,16 +42,16 @@ "f11": "zed::ToggleFullScreen", "ctrl-shift-i": "edit_prediction::ToggleMenu", "shift-alt-l": "lsp_tool::ToggleMenu", - "ctrl-shift-alt-c": "editor::DisplayCursorNames" - } + "ctrl-shift-alt-c": "editor::DisplayCursorNames", + }, }, { "context": "Picker || menu", "use_key_equivalents": true, "bindings": { "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } + "down": "menu::SelectNext", + }, }, { "context": "Editor", @@ -63,7 +63,6 @@ "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", { "ignore_newlines": false, "ignore_brackets": false }], @@ -120,8 +119,8 @@ "shift-f10": "editor::OpenContextMenu", "ctrl-alt-e": "editor::ToggleEditPrediction", "f9": "editor::ToggleBreakpoint", - "shift-f9": "editor::EditLogBreakpoint" - } + "shift-f9": "editor::EditLogBreakpoint", + }, }, { "context": "Editor && mode == full", @@ -140,23 +139,23 @@ "shift-alt-e": "editor::SelectEnclosingSymbol", "ctrl-shift-backspace": "editor::GoToPreviousChange", "ctrl-shift-alt-backspace": "editor::GoToNextChange", - "alt-enter": "editor::OpenSelectionsInMultibuffer" - } + "alt-enter": "editor::OpenSelectionsInMultibuffer", + }, }, { "context": "Editor && mode == full && edit_prediction", "use_key_equivalents": true, "bindings": { "alt-]": "editor::NextEditPrediction", - "alt-[": "editor::PreviousEditPrediction" - } + "alt-[": "editor::PreviousEditPrediction", + }, }, { "context": "Editor && !edit_prediction", "use_key_equivalents": true, "bindings": { - "alt-\\": "editor::ShowEditPrediction" - } + "alt-\\": "editor::ShowEditPrediction", + }, }, { "context": "Editor && mode == auto_height", @@ -164,23 +163,23 @@ "bindings": { "ctrl-enter": "editor::Newline", "shift-enter": "editor::Newline", - "ctrl-shift-enter": "editor::NewlineBelow" - } + "ctrl-shift-enter": "editor::NewlineBelow", + }, }, { "context": "Markdown", "use_key_equivalents": true, "bindings": { - "ctrl-c": "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" - } + "ctrl-alt-enter": "repl::RunInPlace", + }, }, { "context": "Editor && !agent_diff", @@ -188,8 +187,8 @@ "bindings": { "ctrl-k ctrl-r": "git::Restore", "alt-y": "git::StageAndNext", - "shift-alt-y": "git::UnstageAndNext" - } + "shift-alt-y": "git::UnstageAndNext", + }, }, { "context": "Editor && editor_agent_diff", @@ -199,8 +198,8 @@ "ctrl-n": "agent::Reject", "ctrl-shift-y": "agent::KeepAll", "ctrl-shift-n": "agent::RejectAll", - "ctrl-shift-r": "agent::OpenAgentDiff" - } + "ctrl-shift-r": "agent::OpenAgentDiff", + }, }, { "context": "AgentDiff", @@ -209,8 +208,8 @@ "ctrl-y": "agent::Keep", "ctrl-n": "agent::Reject", "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } + "ctrl-shift-n": "agent::RejectAll", + }, }, { "context": "ContextEditor > Editor", @@ -226,8 +225,8 @@ "ctrl-k c": "assistant::CopyCode", "ctrl-g": "search::SelectNextMatch", "ctrl-shift-g": "search::SelectPreviousMatch", - "ctrl-k l": "agent::OpenRulesLibrary" - } + "ctrl-k l": "agent::OpenRulesLibrary", + }, }, { "context": "AgentPanel", @@ -252,38 +251,38 @@ "alt-enter": "agent::ContinueWithBurnMode", "shift-alt-a": "agent::AllowOnce", "ctrl-alt-y": "agent::AllowAlways", - "shift-alt-z": "agent::RejectOnce" - } + "shift-alt-z": "agent::RejectOnce", + }, }, { "context": "AgentPanel > NavigationMenu", "use_key_equivalents": true, "bindings": { - "shift-backspace": "agent::DeleteRecentlyOpenThread" - } + "shift-backspace": "agent::DeleteRecentlyOpenThread", + }, }, { "context": "AgentPanel > Markdown", "use_key_equivalents": true, "bindings": { - "ctrl-c": "markdown::CopyAsMarkdown" - } + "ctrl-c": "markdown::CopyAsMarkdown", + }, }, { "context": "AgentPanel && text_thread", "use_key_equivalents": true, "bindings": { "ctrl-n": "agent::NewTextThread", - "ctrl-alt-t": "agent::NewThread" - } + "ctrl-alt-t": "agent::NewThread", + }, }, { "context": "AgentPanel && acp_thread", "use_key_equivalents": true, "bindings": { "ctrl-n": "agent::NewExternalAgentThread", - "ctrl-alt-t": "agent::NewThread" - } + "ctrl-alt-t": "agent::NewThread", + }, }, { "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", @@ -294,8 +293,8 @@ "ctrl-i": "agent::ToggleProfileSelector", "ctrl-shift-r": "agent::OpenAgentDiff", "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } + "ctrl-shift-n": "agent::RejectAll", + }, }, { "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", @@ -306,8 +305,8 @@ "ctrl-i": "agent::ToggleProfileSelector", "ctrl-shift-r": "agent::OpenAgentDiff", "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } + "ctrl-shift-n": "agent::RejectAll", + }, }, { "context": "EditMessageEditor > Editor", @@ -315,8 +314,8 @@ "bindings": { "escape": "menu::Cancel", "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } + "alt-enter": "editor::Newline", + }, }, { "context": "AgentFeedbackMessageEditor > Editor", @@ -324,14 +323,14 @@ "bindings": { "escape": "menu::Cancel", "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } + "alt-enter": "editor::Newline", + }, }, { "context": "AcpThread > ModeSelector", "bindings": { - "ctrl-enter": "menu::Confirm" - } + "ctrl-enter": "menu::Confirm", + }, }, { "context": "AcpThread > Editor && !use_modifier_to_send", @@ -341,8 +340,8 @@ "ctrl-shift-r": "agent::OpenAgentDiff", "ctrl-shift-y": "agent::KeepAll", "ctrl-shift-n": "agent::RejectAll", - "shift-tab": "agent::CycleModeSelector" - } + "shift-tab": "agent::CycleModeSelector", + }, }, { "context": "AcpThread > Editor && use_modifier_to_send", @@ -352,15 +351,15 @@ "ctrl-shift-r": "agent::OpenAgentDiff", "ctrl-shift-y": "agent::KeepAll", "ctrl-shift-n": "agent::RejectAll", - "shift-tab": "agent::CycleModeSelector" - } + "shift-tab": "agent::CycleModeSelector", + }, }, { "context": "ThreadHistory", "use_key_equivalents": true, "bindings": { - "backspace": "agent::RemoveSelectedThread" - } + "backspace": "agent::RemoveSelectedThread", + }, }, { "context": "RulesLibrary", @@ -368,8 +367,8 @@ "bindings": { "ctrl-n": "rules_library::NewRule", "ctrl-shift-s": "rules_library::ToggleDefaultRule", - "ctrl-w": "workspace::CloseWindow" - } + "ctrl-w": "workspace::CloseWindow", + }, }, { "context": "BufferSearchBar", @@ -382,24 +381,24 @@ "alt-enter": "search::SelectAllMatches", "ctrl-f": "search::FocusSearch", "ctrl-h": "search::ToggleReplace", - "ctrl-l": "search::ToggleSelection" - } + "ctrl-l": "search::ToggleSelection", + }, }, { "context": "BufferSearchBar && in_replace > Editor", "use_key_equivalents": true, "bindings": { "enter": "search::ReplaceNext", - "ctrl-enter": "search::ReplaceAll" - } + "ctrl-enter": "search::ReplaceAll", + }, }, { "context": "BufferSearchBar && !in_replace > Editor", "use_key_equivalents": true, "bindings": { "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } + "down": "search::NextHistoryQuery", + }, }, { "context": "ProjectSearchBar", @@ -408,24 +407,24 @@ "escape": "project_search::ToggleFocus", "ctrl-shift-f": "search::FocusSearch", "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } + "alt-r": "search::ToggleRegex", // vscode + }, }, { "context": "ProjectSearchBar > Editor", "use_key_equivalents": true, "bindings": { "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } + "down": "search::NextHistoryQuery", + }, }, { "context": "ProjectSearchBar && in_replace > Editor", "use_key_equivalents": true, "bindings": { "enter": "search::ReplaceNext", - "ctrl-alt-enter": "search::ReplaceAll" - } + "ctrl-alt-enter": "search::ReplaceAll", + }, }, { "context": "ProjectSearchView", @@ -433,8 +432,8 @@ "bindings": { "escape": "project_search::ToggleFocus", "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } + "alt-r": "search::ToggleRegex", // vscode + }, }, { "context": "Pane", @@ -465,8 +464,10 @@ "ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes", "back": "pane::GoBack", "alt--": "pane::GoBack", + "alt-left": "pane::GoBack", "forward": "pane::GoForward", "alt-=": "pane::GoForward", + "alt-right": "pane::GoForward", "f3": "search::SelectNextMatch", "shift-f3": "search::SelectPreviousMatch", "ctrl-shift-f": "project_search::ToggleFocus", @@ -479,8 +480,8 @@ "shift-enter": "project_search::ToggleAllSearchResults", "alt-r": "search::ToggleRegex", // "ctrl-shift-alt-x": "search::ToggleRegex", - "ctrl-k shift-enter": "pane::TogglePinTab" - } + "ctrl-k shift-enter": "pane::TogglePinTab", + }, }, // Bindings from VS Code { @@ -508,6 +509,7 @@ "ctrl-k ctrl-b": "editor::BlameHover", "ctrl-k ctrl-f": "editor::FormatSelections", "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }], + "ctrl-k ctrl-c": ["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", @@ -540,31 +542,31 @@ "ctrl-\\": "pane::SplitRight", "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" - } + "ctrl-shift-v": "markdown::OpenPreview", + }, }, { "context": "Editor && extension == svg", "use_key_equivalents": true, "bindings": { "ctrl-k v": "svg::OpenPreviewToTheSide", - "ctrl-shift-v": "svg::OpenPreview" - } + "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" - } + "ctrl-g": "go_to_line::Toggle", + }, }, { "context": "Workspace", @@ -648,22 +650,22 @@ // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }], "f5": "debugger::Rerun", "ctrl-f4": "workspace::CloseActiveDock", - "ctrl-w": "workspace::CloseActiveDock" - } + "ctrl-w": "workspace::CloseActiveDock", + }, }, { "context": "Workspace && debugger_running", "use_key_equivalents": true, "bindings": { - "f5": "zed::NoAction" - } + "f5": "zed::NoAction", + }, }, { "context": "Workspace && debugger_stopped", "use_key_equivalents": true, "bindings": { - "f5": "debugger::Continue" - } + "f5": "debugger::Continue", + }, }, { "context": "ApplicationMenu", @@ -671,8 +673,8 @@ "bindings": { "f10": "menu::Cancel", "left": "app_menu::ActivateMenuLeft", - "right": "app_menu::ActivateMenuRight" - } + "right": "app_menu::ActivateMenuRight", + }, }, // Bindings from Sublime Text { @@ -689,8 +691,8 @@ "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", "ctrl-alt-right": "editor::MoveToNextSubwordEnd", "ctrl-shift-alt-left": "editor::SelectToPreviousSubwordStart", - "ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd" - } + "ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd", + }, }, // Bindings from Atom { @@ -700,16 +702,16 @@ "ctrl-k up": "pane::SplitUp", "ctrl-k down": "pane::SplitDown", "ctrl-k left": "pane::SplitLeft", - "ctrl-k right": "pane::SplitRight" - } + "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" - } + "enter": "editor::ConfirmRename", + }, }, { "context": "Editor && showing_completions", @@ -717,22 +719,22 @@ "bindings": { "enter": "editor::ConfirmCompletion", "shift-enter": "editor::ConfirmCompletionReplace", - "tab": "editor::ComposeCompletion" - } + "tab": "editor::ComposeCompletion", + }, }, { "context": "Editor && in_snippet && has_next_tabstop && !showing_completions", "use_key_equivalents": true, "bindings": { - "tab": "editor::NextSnippetTabstop" - } + "tab": "editor::NextSnippetTabstop", + }, }, { "context": "Editor && in_snippet && has_previous_tabstop && !showing_completions", "use_key_equivalents": true, "bindings": { - "shift-tab": "editor::PreviousSnippetTabstop" - } + "shift-tab": "editor::PreviousSnippetTabstop", + }, }, // Bindings for accepting edit predictions // @@ -745,8 +747,8 @@ "alt-tab": "editor::AcceptEditPrediction", "alt-l": "editor::AcceptEditPrediction", "tab": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } + "alt-right": "editor::AcceptPartialEditPrediction", + }, }, { "context": "Editor && edit_prediction_conflict", @@ -754,15 +756,15 @@ "bindings": { "alt-tab": "editor::AcceptEditPrediction", "alt-l": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } + "alt-right": "editor::AcceptPartialEditPrediction", + }, }, { "context": "Editor && showing_code_actions", "use_key_equivalents": true, "bindings": { - "enter": "editor::ConfirmCodeAction" - } + "enter": "editor::ConfirmCodeAction", + }, }, { "context": "Editor && (showing_code_actions || showing_completions)", @@ -773,16 +775,16 @@ "ctrl-n": "editor::ContextMenuNext", "down": "editor::ContextMenuNext", "pageup": "editor::ContextMenuFirst", - "pagedown": "editor::ContextMenuLast" - } + "pagedown": "editor::ContextMenuLast", + }, }, { "context": "Editor && showing_signature_help && !showing_completions", "use_key_equivalents": true, "bindings": { "up": "editor::SignatureHelpPrevious", - "down": "editor::SignatureHelpNext" - } + "down": "editor::SignatureHelpNext", + }, }, // Custom bindings { @@ -790,15 +792,15 @@ "bindings": { "ctrl-shift-alt-f": "workspace::FollowNextCollaborator", // Only available in debug builds: opens an element inspector for development. - "shift-alt-i": "dev::ToggleInspector" - } + "shift-alt-i": "dev::ToggleInspector", + }, }, { "context": "!Terminal", "use_key_equivalents": true, "bindings": { - "ctrl-shift-c": "collab_panel::ToggleFocus" - } + "ctrl-shift-c": "collab_panel::ToggleFocus", + }, }, { "context": "!ContextEditor > Editor && mode == full", @@ -811,8 +813,8 @@ "ctrl-f8": "editor::GoToHunk", "ctrl-shift-f8": "editor::GoToPreviousHunk", "ctrl-enter": "assistant::InlineAssist", - "ctrl-shift-;": "editor::ToggleInlayHints" - } + "ctrl-shift-;": "editor::ToggleInlayHints", + }, }, { "context": "PromptEditor", @@ -821,8 +823,8 @@ "ctrl-[": "agent::CyclePreviousInlineAssist", "ctrl-]": "agent::CycleNextInlineAssist", "ctrl-shift-enter": "inline_assistant::ThumbsUpResult", - "ctrl-shift-delete": "inline_assistant::ThumbsDownResult" - } + "ctrl-shift-delete": "inline_assistant::ThumbsDownResult", + }, }, { "context": "Prompt", @@ -831,15 +833,15 @@ "left": "menu::SelectPrevious", "right": "menu::SelectNext", "h": "menu::SelectPrevious", - "l": "menu::SelectNext" - } + "l": "menu::SelectNext", + }, }, { "context": "ProjectSearchBar && !in_replace", "use_key_equivalents": true, "bindings": { - "ctrl-enter": "project_search::SearchInNew" - } + "ctrl-enter": "project_search::SearchInNew", + }, }, { "context": "OutlinePanel && not_editing", @@ -854,8 +856,8 @@ "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrevious", "alt-enter": "editor::OpenExcerpts", - "ctrl-alt-enter": "editor::OpenExcerptsSplit" - } + "ctrl-alt-enter": "editor::OpenExcerptsSplit", + }, }, { "context": "ProjectPanel", @@ -886,15 +888,15 @@ "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory", "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrevious", - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "ProjectPanel && not_editing", "use_key_equivalents": true, "bindings": { - "space": "project_panel::Open" - } + "space": "project_panel::Open", + }, }, { "context": "GitPanel && ChangesList", @@ -915,15 +917,15 @@ "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 }] - } + "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }], + }, }, { "context": "GitPanel && CommitEditor", "use_key_equivalents": true, "bindings": { - "escape": "git::Cancel" - } + "escape": "git::Cancel", + }, }, { "context": "GitCommit > Editor", @@ -933,8 +935,8 @@ "enter": "editor::Newline", "ctrl-enter": "git::Commit", "ctrl-shift-enter": "git::Amend", - "alt-l": "git::GenerateCommitMessage" - } + "alt-l": "git::GenerateCommitMessage", + }, }, { "context": "GitPanel", @@ -951,8 +953,8 @@ "ctrl-space": "git::StageAll", "ctrl-shift-space": "git::UnstageAll", "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend" - } + "ctrl-shift-enter": "git::Amend", + }, }, { "context": "GitDiff > Editor", @@ -961,15 +963,15 @@ "ctrl-enter": "git::Commit", "ctrl-shift-enter": "git::Amend", "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll" - } + "ctrl-shift-space": "git::UnstageAll", + }, }, { "context": "AskPass > Editor", "use_key_equivalents": true, "bindings": { - "enter": "menu::Confirm" - } + "enter": "menu::Confirm", + }, }, { "context": "CommitEditor > Editor", @@ -982,8 +984,8 @@ "ctrl-enter": "git::Commit", "ctrl-shift-enter": "git::Amend", "alt-up": "git_panel::FocusChanges", - "alt-l": "git::GenerateCommitMessage" - } + "alt-l": "git::GenerateCommitMessage", + }, }, { "context": "DebugPanel", @@ -991,8 +993,8 @@ "bindings": { "ctrl-t": "debugger::ToggleThreadPicker", "ctrl-i": "debugger::ToggleSessionPicker", - "shift-alt-escape": "debugger::ToggleExpandItem" - } + "shift-alt-escape": "debugger::ToggleExpandItem", + }, }, { "context": "VariableList", @@ -1005,8 +1007,8 @@ "ctrl-alt-c": "variable_list::CopyVariableName", "delete": "variable_list::RemoveWatch", "backspace": "variable_list::RemoveWatch", - "alt-enter": "variable_list::AddWatch" - } + "alt-enter": "variable_list::AddWatch", + }, }, { "context": "BreakpointList", @@ -1015,16 +1017,16 @@ "space": "debugger::ToggleEnableBreakpoint", "backspace": "debugger::UnsetBreakpoint", "left": "debugger::PreviousBreakpointProperty", - "right": "debugger::NextBreakpointProperty" - } + "right": "debugger::NextBreakpointProperty", + }, }, { "context": "CollabPanel && not_editing", "use_key_equivalents": true, "bindings": { "ctrl-backspace": "collab_panel::Remove", - "space": "menu::Confirm" - } + "space": "menu::Confirm", + }, }, { "context": "CollabPanel", @@ -1032,22 +1034,22 @@ "bindings": { "alt-up": "collab_panel::MoveChannelUp", "alt-down": "collab_panel::MoveChannelDown", - "alt-enter": "collab_panel::OpenSelectedChannelNotes" - } + "alt-enter": "collab_panel::OpenSelectedChannelNotes", + }, }, { "context": "(CollabPanel && editing) > Editor", "use_key_equivalents": true, "bindings": { - "space": "collab_panel::InsertSpace" - } + "space": "collab_panel::InsertSpace", + }, }, { "context": "ChannelModal", "use_key_equivalents": true, "bindings": { - "tab": "channel_modal::ToggleMode" - } + "tab": "channel_modal::ToggleMode", + }, }, { "context": "Picker > Editor", @@ -1057,22 +1059,22 @@ "up": "menu::SelectPrevious", "down": "menu::SelectNext", "tab": "picker::ConfirmCompletion", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }] - } + "alt-enter": ["picker::ConfirmInput", { "secondary": false }], + }, }, { "context": "ChannelModal > Picker > Editor", "use_key_equivalents": true, "bindings": { - "tab": "channel_modal::ToggleMode" - } + "tab": "channel_modal::ToggleMode", + }, }, { "context": "ToolchainSelector", "use_key_equivalents": true, "bindings": { - "ctrl-shift-a": "toolchain::AddToolchain" - } + "ctrl-shift-a": "toolchain::AddToolchain", + }, }, { "context": "FileFinder || (FileFinder > Picker > Editor)", @@ -1080,8 +1082,8 @@ "bindings": { "ctrl-p": "file_finder::Toggle", "ctrl-shift-a": "file_finder::ToggleSplitMenu", - "ctrl-shift-i": "file_finder::ToggleFilterMenu" - } + "ctrl-shift-i": "file_finder::ToggleFilterMenu", + }, }, { "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)", @@ -1091,8 +1093,8 @@ "ctrl-j": "pane::SplitDown", "ctrl-k": "pane::SplitUp", "ctrl-h": "pane::SplitLeft", - "ctrl-l": "pane::SplitRight" - } + "ctrl-l": "pane::SplitRight", + }, }, { "context": "TabSwitcher", @@ -1101,16 +1103,16 @@ "ctrl-shift-tab": "menu::SelectPrevious", "ctrl-up": "menu::SelectPrevious", "ctrl-down": "menu::SelectNext", - "ctrl-backspace": "tab_switcher::CloseSelectedItem" - } + "ctrl-backspace": "tab_switcher::CloseSelectedItem", + }, }, { "context": "StashList || (StashList > Picker > Editor)", "use_key_equivalents": true, "bindings": { "ctrl-shift-backspace": "stash_picker::DropStashItem", - "ctrl-shift-v": "stash_picker::ShowStashItem" - } + "ctrl-shift-v": "stash_picker::ShowStashItem", + }, }, { "context": "Terminal", @@ -1157,21 +1159,21 @@ "ctrl-shift-r": "terminal::RerunTask", "ctrl-alt-r": "terminal::RerunTask", "alt-t": "terminal::RerunTask", - "ctrl-shift-5": "pane::SplitRight" - } + "ctrl-shift-5": "pane::SplitRight", + }, }, { "context": "Terminal && selection", "bindings": { - "ctrl-c": "terminal::Copy" - } + "ctrl-c": "terminal::Copy", + }, }, { "context": "ZedPredictModal", "use_key_equivalents": true, "bindings": { - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "ConfigureContextServerModal > Editor", @@ -1179,45 +1181,45 @@ "bindings": { "escape": "menu::Cancel", "enter": "editor::Newline", - "ctrl-enter": "menu::Confirm" - } + "ctrl-enter": "menu::Confirm", + }, }, { "context": "ContextServerToolsModal", "use_key_equivalents": true, "bindings": { - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "OnboardingAiConfigurationModal", "use_key_equivalents": true, "bindings": { - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "Diagnostics", "use_key_equivalents": true, "bindings": { - "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh" - } + "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh", + }, }, { "context": "DebugConsole > Editor", "use_key_equivalents": true, "bindings": { "enter": "menu::Confirm", - "alt-enter": "console::WatchExpression" - } + "alt-enter": "console::WatchExpression", + }, }, { "context": "RunModal", "use_key_equivalents": true, "bindings": { "ctrl-tab": "pane::ActivateNextItem", - "ctrl-shift-tab": "pane::ActivatePreviousItem" - } + "ctrl-shift-tab": "pane::ActivatePreviousItem", + }, }, { "context": "MarkdownPreview", @@ -1228,8 +1230,8 @@ "up": "markdown::ScrollUp", "down": "markdown::ScrollDown", "alt-up": "markdown::ScrollUpByItem", - "alt-down": "markdown::ScrollDownByItem" - } + "alt-down": "markdown::ScrollDownByItem", + }, }, { "context": "KeymapEditor", @@ -1242,8 +1244,8 @@ "alt-enter": "keymap_editor::CreateBinding", "ctrl-c": "keymap_editor::CopyAction", "ctrl-shift-c": "keymap_editor::CopyContext", - "ctrl-t": "keymap_editor::ShowMatchingKeybinds" - } + "ctrl-t": "keymap_editor::ShowMatchingKeybinds", + }, }, { "context": "KeystrokeInput", @@ -1251,24 +1253,24 @@ "bindings": { "enter": "keystroke_input::StartRecording", "escape escape escape": "keystroke_input::StopRecording", - "delete": "keystroke_input::ClearKeystrokes" - } + "delete": "keystroke_input::ClearKeystrokes", + }, }, { "context": "KeybindEditorModal", "use_key_equivalents": true, "bindings": { "ctrl-enter": "menu::Confirm", - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "KeybindEditorModal > Editor", "use_key_equivalents": true, "bindings": { "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } + "down": "menu::SelectNext", + }, }, { "context": "Onboarding", @@ -1280,8 +1282,8 @@ "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }], "ctrl-enter": "onboarding::Finish", "alt-shift-l": "onboarding::SignIn", - "shift-alt-a": "onboarding::OpenAccount" - } + "shift-alt-a": "onboarding::OpenAccount", + }, }, { "context": "Welcome", @@ -1290,16 +1292,16 @@ "ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }], "ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }], "ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }], - "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }] - } + "ctrl-0": ["zed::ResetUiFontSize", { "persist": false }], + }, }, { "context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)", "use_key_equivalents": true, "bindings": { "ctrl-shift-space": "git::WorktreeFromDefaultOnWindow", - "ctrl-space": "git::WorktreeFromDefault" - } + "ctrl-space": "git::WorktreeFromDefault", + }, }, { "context": "SettingsWindow", @@ -1324,8 +1326,8 @@ "ctrl-9": ["settings_editor::FocusFile", 8], "ctrl-0": ["settings_editor::FocusFile", 9], "ctrl-pageup": "settings_editor::FocusPreviousFile", - "ctrl-pagedown": "settings_editor::FocusNextFile" - } + "ctrl-pagedown": "settings_editor::FocusNextFile", + }, }, { "context": "StashDiff > Editor", @@ -1333,8 +1335,8 @@ "bindings": { "ctrl-space": "git::ApplyCurrentStash", "ctrl-shift-space": "git::PopCurrentStash", - "ctrl-shift-backspace": "git::DropCurrentStash" - } + "ctrl-shift-backspace": "git::DropCurrentStash", + }, }, { "context": "SettingsWindow > NavigationMenu", @@ -1349,22 +1351,22 @@ "pageup": "settings_editor::FocusPreviousRootNavEntry", "pagedown": "settings_editor::FocusNextRootNavEntry", "home": "settings_editor::FocusFirstNavEntry", - "end": "settings_editor::FocusLastNavEntry" - } + "end": "settings_editor::FocusLastNavEntry", + }, }, { "context": "EditPredictionContext > Editor", "bindings": { "alt-left": "dev::EditPredictionContextGoBack", - "alt-right": "dev::EditPredictionContextGoForward" - } + "alt-right": "dev::EditPredictionContextGoForward", + }, }, { "context": "GitBranchSelector || (GitBranchSelector > Picker > Editor)", "use_key_equivalents": true, "bindings": { "ctrl-shift-backspace": "branch_picker::DeleteBranch", - "ctrl-shift-i": "branch_picker::FilterRemotes" - } - } + "ctrl-shift-i": "branch_picker::FilterRemotes", + }, + }, ] diff --git a/assets/keymaps/initial.json b/assets/keymaps/initial.json index 8e4fe59f44ea7346a51e1c064ffa0553315da3b9..3a8d7f382aa57b39efc22845a17a4ef1bfd240ef 100644 --- a/assets/keymaps/initial.json +++ b/assets/keymaps/initial.json @@ -10,12 +10,12 @@ "context": "Workspace", "bindings": { // "shift shift": "file_finder::Toggle" - } + }, }, { "context": "Editor && vim_mode == insert", "bindings": { // "j k": "vim::NormalBefore" - } - } + }, + }, ] diff --git a/assets/keymaps/linux/atom.json b/assets/keymaps/linux/atom.json index 98992b19fac72055807063edae8b7b23652062d3..a15d4877aab79ac2e570697137ba89e3572d074e 100644 --- a/assets/keymaps/linux/atom.json +++ b/assets/keymaps/linux/atom.json @@ -4,15 +4,15 @@ "bindings": { "ctrl-shift-f5": "workspace::Reload", // window:reload "ctrl-k ctrl-n": "workspace::ActivatePreviousPane", // window:focus-next-pane - "ctrl-k ctrl-p": "workspace::ActivateNextPane" // window:focus-previous-pane - } + "ctrl-k ctrl-p": "workspace::ActivateNextPane", // window:focus-previous-pane + }, }, { "context": "Editor", "bindings": { "ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case - "ctrl-k ctrl-l": "editor::ConvertToLowerCase" // editor:lower-case - } + "ctrl-k ctrl-l": "editor::ConvertToLowerCase", // editor:lower-case + }, }, { "context": "Editor && mode == full", @@ -32,8 +32,8 @@ "ctrl-down": "editor::MoveLineDown", // editor:move-line-down "ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle "ctrl-shift-m": "markdown::OpenPreviewToTheSide", // markdown-preview:toggle - "ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols - } + "ctrl-r": "outline::Toggle", // symbols-view:toggle-project-symbols + }, }, { "context": "BufferSearchBar", @@ -41,8 +41,8 @@ "f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next "shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous "ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected - "ctrl-shift-f3": "search::SelectPreviousMatch" // find-and-replace:find-previous-selected - } + "ctrl-shift-f3": "search::SelectPreviousMatch", // find-and-replace:find-previous-selected + }, }, { "context": "Workspace", @@ -50,8 +50,8 @@ "ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle "ctrl-k ctrl-b": "workspace::ToggleLeftDock", // tree-view:toggle "ctrl-t": "file_finder::Toggle", // fuzzy-finder:toggle-file-finder - "ctrl-r": "project_symbols::Toggle" // symbols-view:toggle-project-symbols - } + "ctrl-r": "project_symbols::Toggle", // symbols-view:toggle-project-symbols + }, }, { "context": "Pane", @@ -65,8 +65,8 @@ "ctrl-6": ["pane::ActivateItem", 5], // tree-view:open-selected-entry-in-pane-6 "ctrl-7": ["pane::ActivateItem", 6], // tree-view:open-selected-entry-in-pane-7 "ctrl-8": ["pane::ActivateItem", 7], // tree-view:open-selected-entry-in-pane-8 - "ctrl-9": ["pane::ActivateItem", 8] // tree-view:open-selected-entry-in-pane-9 - } + "ctrl-9": ["pane::ActivateItem", 8], // tree-view:open-selected-entry-in-pane-9 + }, }, { "context": "ProjectPanel", @@ -75,8 +75,8 @@ "backspace": ["project_panel::Trash", { "skip_prompt": false }], "ctrl-x": "project_panel::Cut", // tree-view:cut "ctrl-c": "project_panel::Copy", // tree-view:copy - "ctrl-v": "project_panel::Paste" // tree-view:paste - } + "ctrl-v": "project_panel::Paste", // tree-view:paste + }, }, { "context": "ProjectPanel && not_editing", @@ -90,7 +90,7 @@ "d": "project_panel::Duplicate", // tree-view:duplicate "home": "menu::SelectFirst", // core:move-to-top "end": "menu::SelectLast", // core:move-to-bottom - "shift-a": "project_panel::NewDirectory" // tree-view:add-folder - } - } + "shift-a": "project_panel::NewDirectory", // tree-view:add-folder + }, + }, ] diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json index 4d2d13a90d96c31f72b1bb0ccc74608f81004eda..53f38234bb47a0f7c4412bf767e3eedf0465ba2a 100644 --- a/assets/keymaps/linux/cursor.json +++ b/assets/keymaps/linux/cursor.json @@ -8,8 +8,8 @@ "ctrl-shift-i": "agent::ToggleFocus", "ctrl-l": "agent::ToggleFocus", "ctrl-shift-l": "agent::ToggleFocus", - "ctrl-shift-j": "agent::OpenSettings" - } + "ctrl-shift-j": "agent::OpenSettings", + }, }, { "context": "Editor && mode == full", @@ -20,18 +20,18 @@ "ctrl-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode "ctrl-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode "ctrl-k": "assistant::InlineAssist", - "ctrl-shift-k": "assistant::InsertIntoEditor" - } + "ctrl-shift-k": "assistant::InsertIntoEditor", + }, }, { "context": "InlineAssistEditor", "use_key_equivalents": true, "bindings": { - "ctrl-shift-backspace": "editor::Cancel" + "ctrl-shift-backspace": "editor::Cancel", // "alt-enter": // Quick Question // "ctrl-shift-enter": // Full File Context // "ctrl-shift-k": // Toggle input focus (editor <> inline assist) - } + }, }, { "context": "AgentPanel || ContextEditor || (MessageEditor > Editor)", @@ -47,7 +47,7 @@ "ctrl-shift-backspace": "editor::Cancel", "ctrl-r": "agent::NewThread", "ctrl-shift-v": "editor::Paste", - "ctrl-shift-k": "assistant::InsertIntoEditor" + "ctrl-shift-k": "assistant::InsertIntoEditor", // "escape": "agent::ToggleFocus" ///// Enable when Zed supports multiple thread tabs // "ctrl-t": // new thread tab @@ -56,28 +56,28 @@ ///// Enable if Zed adds support for keyboard navigation of thread elements // "tab": // cycle to next message // "shift-tab": // cycle to previous message - } + }, }, { "context": "Editor && editor_agent_diff", "use_key_equivalents": true, "bindings": { "ctrl-enter": "agent::KeepAll", - "ctrl-backspace": "agent::RejectAll" - } + "ctrl-backspace": "agent::RejectAll", + }, }, { "context": "Editor && mode == full && edit_prediction", "use_key_equivalents": true, "bindings": { - "ctrl-right": "editor::AcceptPartialEditPrediction" - } + "ctrl-right": "editor::AcceptPartialEditPrediction", + }, }, { "context": "Terminal", "use_key_equivalents": true, "bindings": { - "ctrl-k": "assistant::InlineAssist" - } - } + "ctrl-k": "assistant::InlineAssist", + }, + }, ] diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index c5cf22c81220bf286187252394f8fde26bdd6509..5b6f841de07ac2f9bd45c73e032dea0ede409007 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -5,8 +5,8 @@ [ { "bindings": { - "ctrl-g": "menu::Cancel" - } + "ctrl-g": "menu::Cancel", + }, }, { // Workaround to avoid falling back to default bindings. @@ -18,8 +18,8 @@ "ctrl-g": null, // currently activates `go_to_line::Toggle` when there is nothing to cancel "ctrl-x": null, // currently activates `editor::Cut` if no following key is pressed for 1 second "ctrl-p": null, // currently activates `file_finder::Toggle` when the cursor is on the first character of the buffer - "ctrl-n": null // currently activates `workspace::NewFile` when the cursor is on the last character of the buffer - } + "ctrl-n": null, // currently activates `workspace::NewFile` when the cursor is on the last character of the buffer + }, }, { "context": "Editor", @@ -82,8 +82,8 @@ "ctrl-s": "buffer_search::Deploy", // isearch-forward "ctrl-r": "buffer_search::Deploy", // isearch-backward "alt-^": "editor::JoinLines", // join-line - "alt-q": "editor::Rewrap" // fill-paragraph - } + "alt-q": "editor::Rewrap", // fill-paragraph + }, }, { "context": "Editor && selection_mode", // region selection @@ -119,22 +119,22 @@ "alt->": "editor::SelectToEnd", "ctrl-home": "editor::SelectToBeginning", "ctrl-end": "editor::SelectToEnd", - "ctrl-g": "editor::Cancel" - } + "ctrl-g": "editor::Cancel", + }, }, { "context": "Editor && (showing_code_actions || showing_completions)", "bindings": { "ctrl-p": "editor::ContextMenuPrevious", - "ctrl-n": "editor::ContextMenuNext" - } + "ctrl-n": "editor::ContextMenuNext", + }, }, { "context": "Editor && showing_signature_help && !showing_completions", "bindings": { "ctrl-p": "editor::SignatureHelpPrevious", - "ctrl-n": "editor::SignatureHelpNext" - } + "ctrl-n": "editor::SignatureHelpNext", + }, }, // Example setting for using emacs-style tab // (i.e. indent the current line / selection or perform symbol completion depending on context) @@ -164,8 +164,8 @@ "ctrl-x ctrl-f": "file_finder::Toggle", // find-file "ctrl-x ctrl-s": "workspace::Save", // save-buffer "ctrl-x ctrl-w": "workspace::SaveAs", // write-file - "ctrl-x s": "workspace::SaveAll" // save-some-buffers - } + "ctrl-x s": "workspace::SaveAll", // save-some-buffers + }, }, { // Workaround to enable using native emacs from the Zed terminal. @@ -185,22 +185,22 @@ "ctrl-x ctrl-f": null, // find-file "ctrl-x ctrl-s": null, // save-buffer "ctrl-x ctrl-w": null, // write-file - "ctrl-x s": null // save-some-buffers - } + "ctrl-x s": null, // save-some-buffers + }, }, { "context": "BufferSearchBar > Editor", "bindings": { "ctrl-s": "search::SelectNextMatch", "ctrl-r": "search::SelectPreviousMatch", - "ctrl-g": "buffer_search::Dismiss" - } + "ctrl-g": "buffer_search::Dismiss", + }, }, { "context": "Pane", "bindings": { "ctrl-alt-left": "pane::GoBack", - "ctrl-alt-right": "pane::GoForward" - } - } + "ctrl-alt-right": "pane::GoForward", + }, + }, ] diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index a0314c5bc1dd59c17f3f132db804891ef1df0d4e..3a54c92bf33decd968ee8d711fb1a929534ded21 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -13,8 +13,8 @@ "shift-f8": "debugger::StepOut", "f9": "debugger::Continue", "shift-f9": "debugger::Start", - "alt-shift-f9": "debugger::Start" - } + "alt-shift-f9": "debugger::Start", + }, }, { "context": "Editor", @@ -62,8 +62,8 @@ "ctrl-shift-end": "editor::SelectToEnd", "ctrl-f8": "editor::ToggleBreakpoint", "ctrl-shift-f8": "editor::EditLogBreakpoint", - "ctrl-shift-u": "editor::ToggleCase" - } + "ctrl-shift-u": "editor::ToggleCase", + }, }, { "context": "Editor && mode == full", @@ -76,14 +76,14 @@ "ctrl-space": "editor::ShowCompletions", "ctrl-q": "editor::Hover", "ctrl-p": "editor::ShowSignatureHelp", - "ctrl-\\": "assistant::InlineAssist" - } + "ctrl-\\": "assistant::InlineAssist", + }, }, { "context": "BufferSearchBar", "bindings": { - "shift-enter": "search::SelectPreviousMatch" - } + "shift-enter": "search::SelectPreviousMatch", + }, }, { "context": "BufferSearchBar || ProjectSearchBar", @@ -91,8 +91,8 @@ "alt-c": "search::ToggleCaseSensitive", "alt-e": "search::ToggleSelection", "alt-x": "search::ToggleRegex", - "alt-w": "search::ToggleWholeWord" - } + "alt-w": "search::ToggleWholeWord", + }, }, { "context": "Workspace", @@ -114,8 +114,8 @@ "alt-1": "project_panel::ToggleFocus", "alt-5": "debug_panel::ToggleFocus", "alt-6": "diagnostics::Deploy", - "alt-7": "outline_panel::ToggleFocus" - } + "alt-7": "outline_panel::ToggleFocus", + }, }, { "context": "Pane", // this is to override the default Pane mappings to switch tabs @@ -129,15 +129,15 @@ "alt-7": "outline_panel::ToggleFocus", "alt-8": null, // Services (bottom dock) "alt-9": null, // Git History (bottom dock) - "alt-0": "git_panel::ToggleFocus" - } + "alt-0": "git_panel::ToggleFocus", + }, }, { "context": "Workspace || Editor", "bindings": { "alt-f12": "terminal_panel::Toggle", - "ctrl-shift-k": "git::Push" - } + "ctrl-shift-k": "git::Push", + }, }, { "context": "Pane", @@ -145,8 +145,8 @@ "ctrl-alt-left": "pane::GoBack", "ctrl-alt-right": "pane::GoForward", "alt-left": "pane::ActivatePreviousItem", - "alt-right": "pane::ActivateNextItem" - } + "alt-right": "pane::ActivateNextItem", + }, }, { "context": "ProjectPanel", @@ -156,8 +156,8 @@ "backspace": ["project_panel::Trash", { "skip_prompt": false }], "delete": ["project_panel::Trash", { "skip_prompt": false }], "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], - "shift-f6": "project_panel::Rename" - } + "shift-f6": "project_panel::Rename", + }, }, { "context": "Terminal", @@ -167,8 +167,8 @@ "ctrl-up": "terminal::ScrollLineUp", "ctrl-down": "terminal::ScrollLineDown", "shift-pageup": "terminal::ScrollPageUp", - "shift-pagedown": "terminal::ScrollPageDown" - } + "shift-pagedown": "terminal::ScrollPageDown", + }, }, { "context": "GitPanel", "bindings": { "alt-0": "workspace::CloseActiveDock" } }, { "context": "ProjectPanel", "bindings": { "alt-1": "workspace::CloseActiveDock" } }, @@ -179,7 +179,7 @@ "context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "bindings": { "escape": "editor::ToggleFocus", - "shift-escape": "workspace::CloseActiveDock" - } - } + "shift-escape": "workspace::CloseActiveDock", + }, + }, ] diff --git a/assets/keymaps/linux/sublime_text.json b/assets/keymaps/linux/sublime_text.json index eefd59e5bd1aa48125d0c6e3d662f3cb4e270be7..1d689a6f5841a011768113257afbed2c447669ed 100644 --- a/assets/keymaps/linux/sublime_text.json +++ b/assets/keymaps/linux/sublime_text.json @@ -22,8 +22,8 @@ "ctrl-^": ["workspace::MoveItemToPane", { "destination": 5 }], "ctrl-&": ["workspace::MoveItemToPane", { "destination": 6 }], "ctrl-*": ["workspace::MoveItemToPane", { "destination": 7 }], - "ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }] - } + "ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }], + }, }, { "context": "Editor", @@ -55,20 +55,20 @@ "alt-right": "editor::MoveToNextSubwordEnd", "alt-left": "editor::MoveToPreviousSubwordStart", "alt-shift-right": "editor::SelectToNextSubwordEnd", - "alt-shift-left": "editor::SelectToPreviousSubwordStart" - } + "alt-shift-left": "editor::SelectToPreviousSubwordStart", + }, }, { "context": "Editor && mode == full", "bindings": { - "ctrl-r": "outline::Toggle" - } + "ctrl-r": "outline::Toggle", + }, }, { "context": "Editor && !agent_diff", "bindings": { - "ctrl-k ctrl-z": "git::Restore" - } + "ctrl-k ctrl-z": "git::Restore", + }, }, { "context": "Pane", @@ -83,15 +83,15 @@ "alt-6": ["pane::ActivateItem", 5], "alt-7": ["pane::ActivateItem", 6], "alt-8": ["pane::ActivateItem", 7], - "alt-9": "pane::ActivateLastItem" - } + "alt-9": "pane::ActivateLastItem", + }, }, { "context": "Workspace", "bindings": { "ctrl-k ctrl-b": "workspace::ToggleLeftDock", // "ctrl-0": "project_panel::ToggleFocus", // normally resets zoom - "shift-ctrl-r": "project_symbols::Toggle" - } - } + "shift-ctrl-r": "project_symbols::Toggle", + }, + }, ] diff --git a/assets/keymaps/macos/atom.json b/assets/keymaps/macos/atom.json index ca015b667faa05db53d8fdc3bd82352d9bcc62aa..bf049fd3cb3eca8fe8049fa4e0810f82b10a5bbc 100644 --- a/assets/keymaps/macos/atom.json +++ b/assets/keymaps/macos/atom.json @@ -4,16 +4,16 @@ "bindings": { "ctrl-alt-cmd-l": "workspace::Reload", "cmd-k cmd-p": "workspace::ActivatePreviousPane", - "cmd-k cmd-n": "workspace::ActivateNextPane" - } + "cmd-k cmd-n": "workspace::ActivateNextPane", + }, }, { "context": "Editor", "bindings": { "cmd-shift-backspace": "editor::DeleteToBeginningOfLine", "cmd-k cmd-u": "editor::ConvertToUpperCase", - "cmd-k cmd-l": "editor::ConvertToLowerCase" - } + "cmd-k cmd-l": "editor::ConvertToLowerCase", + }, }, { "context": "Editor && mode == full", @@ -33,8 +33,8 @@ "ctrl-cmd-down": "editor::MoveLineDown", "cmd-\\": "workspace::ToggleLeftDock", "ctrl-shift-m": "markdown::OpenPreviewToTheSide", - "cmd-r": "outline::Toggle" - } + "cmd-r": "outline::Toggle", + }, }, { "context": "BufferSearchBar", @@ -42,8 +42,8 @@ "cmd-g": ["editor::SelectNext", { "replace_newest": true }], "cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }], "cmd-f3": "search::SelectNextMatch", - "cmd-shift-f3": "search::SelectPreviousMatch" - } + "cmd-shift-f3": "search::SelectPreviousMatch", + }, }, { "context": "Workspace", @@ -51,8 +51,8 @@ "cmd-\\": "workspace::ToggleLeftDock", "cmd-k cmd-b": "workspace::ToggleLeftDock", "cmd-t": "file_finder::Toggle", - "cmd-shift-r": "project_symbols::Toggle" - } + "cmd-shift-r": "project_symbols::Toggle", + }, }, { "context": "Pane", @@ -67,8 +67,8 @@ "cmd-6": ["pane::ActivateItem", 5], "cmd-7": ["pane::ActivateItem", 6], "cmd-8": ["pane::ActivateItem", 7], - "cmd-9": "pane::ActivateLastItem" - } + "cmd-9": "pane::ActivateLastItem", + }, }, { "context": "ProjectPanel", @@ -77,8 +77,8 @@ "backspace": ["project_panel::Trash", { "skip_prompt": false }], "cmd-x": "project_panel::Cut", "cmd-c": "project_panel::Copy", - "cmd-v": "project_panel::Paste" - } + "cmd-v": "project_panel::Paste", + }, }, { "context": "ProjectPanel && not_editing", @@ -92,7 +92,7 @@ "d": "project_panel::Duplicate", "home": "menu::SelectFirst", "end": "menu::SelectLast", - "shift-a": "project_panel::NewDirectory" - } - } + "shift-a": "project_panel::NewDirectory", + }, + }, ] diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json index 97abc7dd819485850107eca6762fc1ed60ec0515..6a2f46e0ce6d037de6de2d801d80671c63a3e3cd 100644 --- a/assets/keymaps/macos/cursor.json +++ b/assets/keymaps/macos/cursor.json @@ -8,8 +8,8 @@ "cmd-shift-i": "agent::ToggleFocus", "cmd-l": "agent::ToggleFocus", "cmd-shift-l": "agent::ToggleFocus", - "cmd-shift-j": "agent::OpenSettings" - } + "cmd-shift-j": "agent::OpenSettings", + }, }, { "context": "Editor && mode == full", @@ -20,19 +20,19 @@ "cmd-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode "cmd-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode "cmd-k": "assistant::InlineAssist", - "cmd-shift-k": "assistant::InsertIntoEditor" - } + "cmd-shift-k": "assistant::InsertIntoEditor", + }, }, { "context": "InlineAssistEditor", "use_key_equivalents": true, "bindings": { "cmd-shift-backspace": "editor::Cancel", - "cmd-enter": "menu::Confirm" + "cmd-enter": "menu::Confirm", // "alt-enter": // Quick Question // "cmd-shift-enter": // Full File Context // "cmd-shift-k": // Toggle input focus (editor <> inline assist) - } + }, }, { "context": "AgentPanel || ContextEditor || (MessageEditor > Editor)", @@ -48,7 +48,7 @@ "cmd-shift-backspace": "editor::Cancel", "cmd-r": "agent::NewThread", "cmd-shift-v": "editor::Paste", - "cmd-shift-k": "assistant::InsertIntoEditor" + "cmd-shift-k": "assistant::InsertIntoEditor", // "escape": "agent::ToggleFocus" ///// Enable when Zed supports multiple thread tabs // "cmd-t": // new thread tab @@ -57,28 +57,28 @@ ///// Enable if Zed adds support for keyboard navigation of thread elements // "tab": // cycle to next message // "shift-tab": // cycle to previous message - } + }, }, { "context": "Editor && editor_agent_diff", "use_key_equivalents": true, "bindings": { "cmd-enter": "agent::KeepAll", - "cmd-backspace": "agent::RejectAll" - } + "cmd-backspace": "agent::RejectAll", + }, }, { "context": "Editor && mode == full && edit_prediction", "use_key_equivalents": true, "bindings": { - "cmd-right": "editor::AcceptPartialEditPrediction" - } + "cmd-right": "editor::AcceptPartialEditPrediction", + }, }, { "context": "Terminal", "use_key_equivalents": true, "bindings": { - "cmd-k": "assistant::InlineAssist" - } - } + "cmd-k": "assistant::InlineAssist", + }, + }, ] diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index ea831c0c059ea082d002f3af01b8d97be9e86616..2f11e2ce00e8b60a0f1c85b5aeb204e866491a45 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -6,8 +6,8 @@ { "context": "!GitPanel", "bindings": { - "ctrl-g": "menu::Cancel" - } + "ctrl-g": "menu::Cancel", + }, }, { // Workaround to avoid falling back to default bindings. @@ -15,8 +15,8 @@ // NOTE: must be declared before the `Editor` override. "context": "Editor", "bindings": { - "ctrl-g": null // currently activates `go_to_line::Toggle` when there is nothing to cancel - } + "ctrl-g": null, // currently activates `go_to_line::Toggle` when there is nothing to cancel + }, }, { "context": "Editor", @@ -79,8 +79,8 @@ "ctrl-s": "buffer_search::Deploy", // isearch-forward "ctrl-r": "buffer_search::Deploy", // isearch-backward "alt-^": "editor::JoinLines", // join-line - "alt-q": "editor::Rewrap" // fill-paragraph - } + "alt-q": "editor::Rewrap", // fill-paragraph + }, }, { "context": "Editor && selection_mode", // region selection @@ -116,22 +116,22 @@ "alt->": "editor::SelectToEnd", "ctrl-home": "editor::SelectToBeginning", "ctrl-end": "editor::SelectToEnd", - "ctrl-g": "editor::Cancel" - } + "ctrl-g": "editor::Cancel", + }, }, { "context": "Editor && (showing_code_actions || showing_completions)", "bindings": { "ctrl-p": "editor::ContextMenuPrevious", - "ctrl-n": "editor::ContextMenuNext" - } + "ctrl-n": "editor::ContextMenuNext", + }, }, { "context": "Editor && showing_signature_help && !showing_completions", "bindings": { "ctrl-p": "editor::SignatureHelpPrevious", - "ctrl-n": "editor::SignatureHelpNext" - } + "ctrl-n": "editor::SignatureHelpNext", + }, }, // Example setting for using emacs-style tab // (i.e. indent the current line / selection or perform symbol completion depending on context) @@ -161,8 +161,8 @@ "ctrl-x ctrl-f": "file_finder::Toggle", // find-file "ctrl-x ctrl-s": "workspace::Save", // save-buffer "ctrl-x ctrl-w": "workspace::SaveAs", // write-file - "ctrl-x s": "workspace::SaveAll" // save-some-buffers - } + "ctrl-x s": "workspace::SaveAll", // save-some-buffers + }, }, { // Workaround to enable using native emacs from the Zed terminal. @@ -182,22 +182,22 @@ "ctrl-x ctrl-f": null, // find-file "ctrl-x ctrl-s": null, // save-buffer "ctrl-x ctrl-w": null, // write-file - "ctrl-x s": null // save-some-buffers - } + "ctrl-x s": null, // save-some-buffers + }, }, { "context": "BufferSearchBar > Editor", "bindings": { "ctrl-s": "search::SelectNextMatch", "ctrl-r": "search::SelectPreviousMatch", - "ctrl-g": "buffer_search::Dismiss" - } + "ctrl-g": "buffer_search::Dismiss", + }, }, { "context": "Pane", "bindings": { "ctrl-alt-left": "pane::GoBack", - "ctrl-alt-right": "pane::GoForward" - } - } + "ctrl-alt-right": "pane::GoForward", + }, + }, ] diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json index 364f489167f5abb9af21d9f005586bde08439850..1721a9d743a67abddbc55a4b505be497920d15aa 100644 --- a/assets/keymaps/macos/jetbrains.json +++ b/assets/keymaps/macos/jetbrains.json @@ -13,8 +13,8 @@ "shift-f8": "debugger::StepOut", "f9": "debugger::Continue", "shift-f9": "debugger::Start", - "alt-shift-f9": "debugger::Start" - } + "alt-shift-f9": "debugger::Start", + }, }, { "context": "Editor", @@ -60,8 +60,8 @@ "cmd-shift-end": "editor::SelectToEnd", "ctrl-f8": "editor::ToggleBreakpoint", "ctrl-shift-f8": "editor::EditLogBreakpoint", - "cmd-shift-u": "editor::ToggleCase" - } + "cmd-shift-u": "editor::ToggleCase", + }, }, { "context": "Editor && mode == full", @@ -74,14 +74,14 @@ "ctrl-space": "editor::ShowCompletions", "cmd-j": "editor::Hover", "cmd-p": "editor::ShowSignatureHelp", - "cmd-\\": "assistant::InlineAssist" - } + "cmd-\\": "assistant::InlineAssist", + }, }, { "context": "BufferSearchBar", "bindings": { - "shift-enter": "search::SelectPreviousMatch" - } + "shift-enter": "search::SelectPreviousMatch", + }, }, { "context": "BufferSearchBar || ProjectSearchBar", @@ -93,8 +93,8 @@ "ctrl-alt-c": "search::ToggleCaseSensitive", "ctrl-alt-e": "search::ToggleSelection", "ctrl-alt-w": "search::ToggleWholeWord", - "ctrl-alt-x": "search::ToggleRegex" - } + "ctrl-alt-x": "search::ToggleRegex", + }, }, { "context": "Workspace", @@ -116,8 +116,8 @@ "cmd-1": "project_panel::ToggleFocus", "cmd-5": "debug_panel::ToggleFocus", "cmd-6": "diagnostics::Deploy", - "cmd-7": "outline_panel::ToggleFocus" - } + "cmd-7": "outline_panel::ToggleFocus", + }, }, { "context": "Pane", // this is to override the default Pane mappings to switch tabs @@ -131,15 +131,15 @@ "cmd-7": "outline_panel::ToggleFocus", "cmd-8": null, // Services (bottom dock) "cmd-9": null, // Git History (bottom dock) - "cmd-0": "git_panel::ToggleFocus" - } + "cmd-0": "git_panel::ToggleFocus", + }, }, { "context": "Workspace || Editor", "bindings": { "alt-f12": "terminal_panel::Toggle", - "cmd-shift-k": "git::Push" - } + "cmd-shift-k": "git::Push", + }, }, { "context": "Pane", @@ -147,8 +147,8 @@ "cmd-alt-left": "pane::GoBack", "cmd-alt-right": "pane::GoForward", "alt-left": "pane::ActivatePreviousItem", - "alt-right": "pane::ActivateNextItem" - } + "alt-right": "pane::ActivateNextItem", + }, }, { "context": "ProjectPanel", @@ -159,8 +159,8 @@ "backspace": ["project_panel::Trash", { "skip_prompt": false }], "delete": ["project_panel::Trash", { "skip_prompt": false }], "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], - "shift-f6": "project_panel::Rename" - } + "shift-f6": "project_panel::Rename", + }, }, { "context": "Terminal", @@ -170,8 +170,8 @@ "cmd-up": "terminal::ScrollLineUp", "cmd-down": "terminal::ScrollLineDown", "shift-pageup": "terminal::ScrollPageUp", - "shift-pagedown": "terminal::ScrollPageDown" - } + "shift-pagedown": "terminal::ScrollPageDown", + }, }, { "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } }, { "context": "ProjectPanel", "bindings": { "cmd-1": "workspace::CloseActiveDock" } }, @@ -182,7 +182,7 @@ "context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "bindings": { "escape": "editor::ToggleFocus", - "shift-escape": "workspace::CloseActiveDock" - } - } + "shift-escape": "workspace::CloseActiveDock", + }, + }, ] diff --git a/assets/keymaps/macos/sublime_text.json b/assets/keymaps/macos/sublime_text.json index d1bffca755b611d9046d4b7e794d2303835227a2..f4ae1ce5dda4e2c0dd21e97bd3a411dd4a4f3663 100644 --- a/assets/keymaps/macos/sublime_text.json +++ b/assets/keymaps/macos/sublime_text.json @@ -22,8 +22,8 @@ "ctrl-^": ["workspace::MoveItemToPane", { "destination": 5 }], "ctrl-&": ["workspace::MoveItemToPane", { "destination": 6 }], "ctrl-*": ["workspace::MoveItemToPane", { "destination": 7 }], - "ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }] - } + "ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }], + }, }, { "context": "Editor", @@ -57,20 +57,20 @@ "ctrl-right": "editor::MoveToNextSubwordEnd", "ctrl-left": "editor::MoveToPreviousSubwordStart", "ctrl-shift-right": "editor::SelectToNextSubwordEnd", - "ctrl-shift-left": "editor::SelectToPreviousSubwordStart" - } + "ctrl-shift-left": "editor::SelectToPreviousSubwordStart", + }, }, { "context": "Editor && mode == full", "bindings": { - "cmd-r": "outline::Toggle" - } + "cmd-r": "outline::Toggle", + }, }, { "context": "Editor && !agent_diff", "bindings": { - "cmd-k cmd-z": "git::Restore" - } + "cmd-k cmd-z": "git::Restore", + }, }, { "context": "Pane", @@ -85,8 +85,8 @@ "cmd-6": ["pane::ActivateItem", 5], "cmd-7": ["pane::ActivateItem", 6], "cmd-8": ["pane::ActivateItem", 7], - "cmd-9": "pane::ActivateLastItem" - } + "cmd-9": "pane::ActivateLastItem", + }, }, { "context": "Workspace", @@ -95,7 +95,7 @@ "cmd-t": "file_finder::Toggle", "shift-cmd-r": "project_symbols::Toggle", // Currently busted: https://github.com/zed-industries/feedback/issues/898 - "ctrl-0": "project_panel::ToggleFocus" - } - } + "ctrl-0": "project_panel::ToggleFocus", + }, + }, ] diff --git a/assets/keymaps/macos/textmate.json b/assets/keymaps/macos/textmate.json index f91f39b7f5c079f81b5fcf8e28e2092a33ff1aa4..90450e60af7147f1394eb6cb4c1efc389edad2d0 100644 --- a/assets/keymaps/macos/textmate.json +++ b/assets/keymaps/macos/textmate.json @@ -2,8 +2,8 @@ { "bindings": { "cmd-shift-o": "projects::OpenRecent", - "cmd-alt-tab": "project_panel::ToggleFocus" - } + "cmd-alt-tab": "project_panel::ToggleFocus", + }, }, { "context": "Editor && mode == full", @@ -15,8 +15,8 @@ "cmd-enter": "editor::NewlineBelow", "cmd-alt-enter": "editor::NewlineAbove", "cmd-shift-l": "editor::SelectLine", - "cmd-shift-t": "outline::Toggle" - } + "cmd-shift-t": "outline::Toggle", + }, }, { "context": "Editor", @@ -41,30 +41,30 @@ "ctrl-u": "editor::ConvertToUpperCase", "ctrl-shift-u": "editor::ConvertToLowerCase", "ctrl-alt-u": "editor::ConvertToUpperCamelCase", - "ctrl-_": "editor::ConvertToSnakeCase" - } + "ctrl-_": "editor::ConvertToSnakeCase", + }, }, { "context": "BufferSearchBar", "bindings": { "ctrl-s": "search::SelectNextMatch", - "ctrl-shift-s": "search::SelectPreviousMatch" - } + "ctrl-shift-s": "search::SelectPreviousMatch", + }, }, { "context": "Workspace", "bindings": { "cmd-alt-ctrl-d": "workspace::ToggleLeftDock", "cmd-t": "file_finder::Toggle", - "cmd-shift-t": "project_symbols::Toggle" - } + "cmd-shift-t": "project_symbols::Toggle", + }, }, { "context": "Pane", "bindings": { "alt-cmd-r": "search::ToggleRegex", - "ctrl-tab": "project_panel::ToggleFocus" - } + "ctrl-tab": "project_panel::ToggleFocus", + }, }, { "context": "ProjectPanel", @@ -75,11 +75,11 @@ "return": "project_panel::Rename", "cmd-c": "project_panel::Copy", "cmd-v": "project_panel::Paste", - "cmd-alt-c": "project_panel::CopyPath" - } + "cmd-alt-c": "project_panel::CopyPath", + }, }, { "context": "Dock", - "bindings": {} - } + "bindings": {}, + }, ] diff --git a/assets/keymaps/storybook.json b/assets/keymaps/storybook.json index 9b92fbe1a3844043e379647d1dd6c57e082fdf77..432bdc7004a4c66b52e20282aba924611b204aa1 100644 --- a/assets/keymaps/storybook.json +++ b/assets/keymaps/storybook.json @@ -27,7 +27,7 @@ "backspace": "editor::Backspace", "delete": "editor::Delete", "left": "editor::MoveLeft", - "right": "editor::MoveRight" - } - } + "right": "editor::MoveRight", + }, + }, ] diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 533db14a5f7bba4196f6a45cabfbe5d9052f796a..0097480e2775a1048452b2a5e8ec826525da3f2e 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -181,8 +181,8 @@ "ctrl-w space": "editor::OpenExcerptsSplit", "ctrl-w g space": "editor::OpenExcerptsSplit", "ctrl-^": "pane::AlternateFile", - ".": "vim::Repeat" - } + ".": "vim::Repeat", + }, }, { "context": "vim_mode == normal || vim_mode == visual || vim_mode == operator", @@ -223,8 +223,8 @@ "] r": "vim::GoToNextReference", // tree-sitter related commands "[ x": "vim::SelectLargerSyntaxNode", - "] x": "vim::SelectSmallerSyntaxNode" - } + "] x": "vim::SelectSmallerSyntaxNode", + }, }, { "context": "vim_mode == normal", @@ -261,16 +261,16 @@ "[ d": "editor::GoToPreviousDiagnostic", "] c": "editor::GoToHunk", "[ c": "editor::GoToPreviousHunk", - "g c": "vim::PushToggleComments" - } + "g c": "vim::PushToggleComments", + }, }, { "context": "VimControl && VimCount", "bindings": { "0": ["vim::Number", 0], ":": "vim::CountCommand", - "%": "vim::GoToPercentage" - } + "%": "vim::GoToPercentage", + }, }, { "context": "vim_mode == visual", @@ -322,8 +322,8 @@ "g w": "vim::Rewrap", "g ?": "vim::ConvertToRot13", // "g ?": "vim::ConvertToRot47", - "\"": "vim::PushRegister" - } + "\"": "vim::PushRegister", + }, }, { "context": "vim_mode == helix_select", @@ -343,8 +343,8 @@ "ctrl-pageup": "pane::ActivatePreviousItem", "ctrl-pagedown": "pane::ActivateNextItem", ".": "vim::Repeat", - "alt-.": "vim::RepeatFind" - } + "alt-.": "vim::RepeatFind", + }, }, { "context": "vim_mode == insert", @@ -374,8 +374,8 @@ "ctrl-r": "vim::PushRegister", "insert": "vim::ToggleReplace", "ctrl-o": "vim::TemporaryNormal", - "ctrl-s": "editor::ShowSignatureHelp" - } + "ctrl-s": "editor::ShowSignatureHelp", + }, }, { "context": "showing_completions", @@ -383,8 +383,8 @@ "ctrl-d": "vim::ScrollDown", "ctrl-u": "vim::ScrollUp", "ctrl-e": "vim::LineDown", - "ctrl-y": "vim::LineUp" - } + "ctrl-y": "vim::LineUp", + }, }, { "context": "(vim_mode == normal || vim_mode == helix_normal) && !menu", @@ -409,23 +409,31 @@ "shift-s": "vim::SubstituteLine", "\"": "vim::PushRegister", "ctrl-pagedown": "pane::ActivateNextItem", - "ctrl-pageup": "pane::ActivatePreviousItem" - } + "ctrl-pageup": "pane::ActivatePreviousItem", + }, }, { "context": "VimControl && vim_mode == helix_normal && !menu", "bindings": { + "j": ["vim::Down", { "display_lines": true }], + "down": ["vim::Down", { "display_lines": true }], + "k": ["vim::Up", { "display_lines": true }], + "up": ["vim::Up", { "display_lines": true }], + "g j": "vim::Down", + "g down": "vim::Down", + "g k": "vim::Up", + "g up": "vim::Up", "escape": "vim::SwitchToHelixNormalMode", "i": "vim::HelixInsert", "a": "vim::HelixAppend", - "ctrl-[": "editor::Cancel" - } + "ctrl-[": "editor::Cancel", + }, }, { "context": "vim_mode == helix_select && !menu", "bindings": { - "escape": "vim::SwitchToHelixNormalMode" - } + "escape": "vim::SwitchToHelixNormalMode", + }, }, { "context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu", @@ -445,9 +453,9 @@ "shift-r": "editor::Paste", "`": "vim::ConvertToLowerCase", "alt-`": "vim::ConvertToUpperCase", - "insert": "vim::InsertBefore", + "insert": "vim::InsertBefore", // not a helix default "shift-u": "editor::Redo", - "ctrl-r": "vim::Redo", + "ctrl-r": "vim::Redo", // not a helix default "y": "vim::HelixYank", "p": "vim::HelixPaste", "shift-p": ["vim::HelixPaste", { "before": true }], @@ -476,6 +484,7 @@ "alt-p": "editor::SelectPreviousSyntaxNode", "alt-n": "editor::SelectNextSyntaxNode", + // Search "n": "vim::HelixSelectNext", "shift-n": "vim::HelixSelectPrevious", @@ -483,27 +492,27 @@ "g e": "vim::EndOfDocument", "g h": "vim::StartOfLine", "g l": "vim::EndOfLine", - "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" + "g s": "vim::FirstNonWhitespace", "g t": "vim::WindowTop", "g c": "vim::WindowMiddle", "g b": "vim::WindowBottom", - "g r": "editor::FindAllReferences", // zed specific + "g r": "editor::FindAllReferences", "g n": "pane::ActivateNextItem", - "shift-l": "pane::ActivateNextItem", + "shift-l": "pane::ActivateNextItem", // not a helix default "g p": "pane::ActivatePreviousItem", - "shift-h": "pane::ActivatePreviousItem", - "g .": "vim::HelixGotoLastModification", // go to last modification + "shift-h": "pane::ActivatePreviousItem", // not a helix default + "g .": "vim::HelixGotoLastModification", // Window mode + "space w v": "pane::SplitDown", + "space w s": "pane::SplitRight", "space w h": "workspace::ActivatePaneLeft", - "space w l": "workspace::ActivatePaneRight", - "space w k": "workspace::ActivatePaneUp", "space w j": "workspace::ActivatePaneDown", + "space w k": "workspace::ActivatePaneUp", + "space w l": "workspace::ActivatePaneRight", "space w q": "pane::CloseActiveItem", - "space w s": "pane::SplitRight", - "space w r": "pane::SplitRight", - "space w v": "pane::SplitDown", - "space w d": "pane::SplitDown", + "space w r": "pane::SplitRight", // not a helix default + "space w d": "pane::SplitDown", // not a helix default // Space mode "space f": "file_finder::Toggle", @@ -517,6 +526,7 @@ "space c": "editor::ToggleComments", "space p": "editor::Paste", "space y": "editor::Copy", + "space /": "pane::DeploySearch", // Other ":": "command_palette::Toggle", @@ -524,24 +534,22 @@ "]": ["vim::PushHelixNext", { "around": true }], "[": ["vim::PushHelixPrevious", { "around": true }], "g q": "vim::PushRewrap", - "g w": "vim::PushRewrap" - // "tab": "pane::ActivateNextItem", - // "shift-tab": "pane::ActivatePrevItem", - } + "g w": "vim::PushRewrap", // not a helix default & clashes with helix `goto_word` + }, }, { "context": "vim_mode == insert && !(showing_code_actions || showing_completions)", "bindings": { "ctrl-p": "editor::ShowWordCompletions", - "ctrl-n": "editor::ShowWordCompletions" - } + "ctrl-n": "editor::ShowWordCompletions", + }, }, { "context": "(vim_mode == insert || vim_mode == normal) && showing_signature_help && !showing_completions", "bindings": { "ctrl-p": "editor::SignatureHelpPrevious", - "ctrl-n": "editor::SignatureHelpNext" - } + "ctrl-n": "editor::SignatureHelpNext", + }, }, { "context": "vim_mode == replace", @@ -557,8 +565,8 @@ "backspace": "vim::UndoReplace", "tab": "vim::Tab", "enter": "vim::Enter", - "insert": "vim::InsertBefore" - } + "insert": "vim::InsertBefore", + }, }, { "context": "vim_mode == waiting", @@ -570,14 +578,14 @@ "escape": "vim::ClearOperators", "ctrl-k": ["vim::PushDigraph", {}], "ctrl-v": ["vim::PushLiteral", {}], - "ctrl-q": ["vim::PushLiteral", {}] - } + "ctrl-q": ["vim::PushLiteral", {}], + }, }, { "context": "Editor && vim_mode == waiting && (vim_operator == ys || vim_operator == cs)", "bindings": { - "escape": "vim::SwitchToNormalMode" - } + "escape": "vim::SwitchToNormalMode", + }, }, { "context": "vim_mode == operator", @@ -585,8 +593,8 @@ "ctrl-c": "vim::ClearOperators", "ctrl-[": "vim::ClearOperators", "escape": "vim::ClearOperators", - "g c": "vim::Comment" - } + "g c": "vim::Comment", + }, }, { "context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous", @@ -623,14 +631,14 @@ "shift-i": ["vim::IndentObj", { "include_below": true }], "f": "vim::Method", "c": "vim::Class", - "e": "vim::EntireFile" - } + "e": "vim::EntireFile", + }, }, { "context": "vim_operator == helix_m", "bindings": { - "m": "vim::Matching" - } + "m": "vim::Matching", + }, }, { "context": "vim_operator == helix_next", @@ -647,8 +655,8 @@ "x": "editor::SelectSmallerSyntaxNode", "d": "editor::GoToDiagnostic", "c": "editor::GoToHunk", - "space": "vim::InsertEmptyLineBelow" - } + "space": "vim::InsertEmptyLineBelow", + }, }, { "context": "vim_operator == helix_previous", @@ -665,8 +673,8 @@ "x": "editor::SelectLargerSyntaxNode", "d": "editor::GoToPreviousDiagnostic", "c": "editor::GoToPreviousHunk", - "space": "vim::InsertEmptyLineAbove" - } + "space": "vim::InsertEmptyLineAbove", + }, }, { "context": "vim_operator == c", @@ -674,8 +682,8 @@ "c": "vim::CurrentLine", "x": "vim::Exchange", "d": "editor::Rename", // zed specific - "s": ["vim::PushChangeSurrounds", {}] - } + "s": ["vim::PushChangeSurrounds", {}], + }, }, { "context": "vim_operator == d", @@ -687,36 +695,36 @@ "shift-o": "git::ToggleStaged", "p": "git::Restore", // "d p" "u": "git::StageAndNext", // "d u" - "shift-u": "git::UnstageAndNext" // "d shift-u" - } + "shift-u": "git::UnstageAndNext", // "d shift-u" + }, }, { "context": "vim_operator == gu", "bindings": { "g u": "vim::CurrentLine", - "u": "vim::CurrentLine" - } + "u": "vim::CurrentLine", + }, }, { "context": "vim_operator == gU", "bindings": { "g shift-u": "vim::CurrentLine", - "shift-u": "vim::CurrentLine" - } + "shift-u": "vim::CurrentLine", + }, }, { "context": "vim_operator == g~", "bindings": { "g ~": "vim::CurrentLine", - "~": "vim::CurrentLine" - } + "~": "vim::CurrentLine", + }, }, { "context": "vim_operator == g?", "bindings": { "g ?": "vim::CurrentLine", - "?": "vim::CurrentLine" - } + "?": "vim::CurrentLine", + }, }, { "context": "vim_operator == gq", @@ -724,66 +732,66 @@ "g q": "vim::CurrentLine", "q": "vim::CurrentLine", "g w": "vim::CurrentLine", - "w": "vim::CurrentLine" - } + "w": "vim::CurrentLine", + }, }, { "context": "vim_operator == y", "bindings": { "y": "vim::CurrentLine", "v": "vim::PushForcedMotion", - "s": ["vim::PushAddSurrounds", {}] - } + "s": ["vim::PushAddSurrounds", {}], + }, }, { "context": "vim_operator == ys", "bindings": { - "s": "vim::CurrentLine" - } + "s": "vim::CurrentLine", + }, }, { "context": "vim_operator == >", "bindings": { - ">": "vim::CurrentLine" - } + ">": "vim::CurrentLine", + }, }, { "context": "vim_operator == <", "bindings": { - "<": "vim::CurrentLine" - } + "<": "vim::CurrentLine", + }, }, { "context": "vim_operator == eq", "bindings": { - "=": "vim::CurrentLine" - } + "=": "vim::CurrentLine", + }, }, { "context": "vim_operator == sh", "bindings": { - "!": "vim::CurrentLine" - } + "!": "vim::CurrentLine", + }, }, { "context": "vim_operator == gc", "bindings": { - "c": "vim::CurrentLine" - } + "c": "vim::CurrentLine", + }, }, { "context": "vim_operator == gR", "bindings": { "r": "vim::CurrentLine", - "shift-r": "vim::CurrentLine" - } + "shift-r": "vim::CurrentLine", + }, }, { "context": "vim_operator == cx", "bindings": { "x": "vim::CurrentLine", - "c": "vim::ClearExchange" - } + "c": "vim::ClearExchange", + }, }, { "context": "vim_mode == literal", @@ -825,15 +833,15 @@ "tab": ["vim::Literal", ["tab", "\u0009"]], // zed extensions: "backspace": ["vim::Literal", ["backspace", "\u0008"]], - "delete": ["vim::Literal", ["delete", "\u007F"]] - } + "delete": ["vim::Literal", ["delete", "\u007F"]], + }, }, { "context": "BufferSearchBar && !in_replace", "bindings": { "enter": "vim::SearchSubmit", - "escape": "buffer_search::Dismiss" - } + "escape": "buffer_search::Dismiss", + }, }, { "context": "VimControl && !menu || !Editor && !Terminal", @@ -894,8 +902,8 @@ "ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal", "ctrl-w n": "workspace::NewFileSplitHorizontal", "g t": "vim::GoToTab", - "g shift-t": "vim::GoToPreviousTab" - } + "g shift-t": "vim::GoToPreviousTab", + }, }, { "context": "!Editor && !Terminal", @@ -905,8 +913,8 @@ "] b": "pane::ActivateNextItem", "[ b": "pane::ActivatePreviousItem", "] shift-b": "pane::ActivateLastItem", - "[ shift-b": ["pane::ActivateItem", 0] - } + "[ shift-b": ["pane::ActivateItem", 0], + }, }, { // netrw compatibility @@ -956,17 +964,45 @@ "6": ["vim::Number", 6], "7": ["vim::Number", 7], "8": ["vim::Number", 8], - "9": ["vim::Number", 9] - } + "9": ["vim::Number", 9], + }, }, { "context": "OutlinePanel && not_editing", "bindings": { - "j": "menu::SelectNext", - "k": "menu::SelectPrevious", + "h": "outline_panel::CollapseSelectedEntry", + "j": "vim::MenuSelectNext", + "k": "vim::MenuSelectPrevious", + "down": "vim::MenuSelectNext", + "up": "vim::MenuSelectPrevious", + "l": "outline_panel::ExpandSelectedEntry", "shift-g": "menu::SelectLast", - "g g": "menu::SelectFirst" - } + "g g": "menu::SelectFirst", + "-": "outline_panel::SelectParent", + "enter": "editor::ToggleFocus", + "/": "menu::Cancel", + "ctrl-u": "outline_panel::ScrollUp", + "ctrl-d": "outline_panel::ScrollDown", + "z t": "outline_panel::ScrollCursorTop", + "z z": "outline_panel::ScrollCursorCenter", + "z b": "outline_panel::ScrollCursorBottom", + "0": ["vim::Number", 0], + "1": ["vim::Number", 1], + "2": ["vim::Number", 2], + "3": ["vim::Number", 3], + "4": ["vim::Number", 4], + "5": ["vim::Number", 5], + "6": ["vim::Number", 6], + "7": ["vim::Number", 7], + "8": ["vim::Number", 8], + "9": ["vim::Number", 9], + }, + }, + { + "context": "OutlinePanel && editing", + "bindings": { + "enter": "menu::Cancel", + }, }, { "context": "GitPanel && ChangesList", @@ -981,8 +1017,8 @@ "x": "git::ToggleStaged", "shift-x": "git::StageAll", "g x": "git::StageRange", - "shift-u": "git::UnstageAll" - } + "shift-u": "git::UnstageAll", + }, }, { "context": "Editor && mode == auto_height && VimControl", @@ -993,8 +1029,8 @@ "#": null, "*": null, "n": null, - "shift-n": null - } + "shift-n": null, + }, }, { "context": "Picker > Editor", @@ -1003,29 +1039,29 @@ "ctrl-u": "editor::DeleteToBeginningOfLine", "ctrl-w": "editor::DeleteToPreviousWordStart", "ctrl-p": "menu::SelectPrevious", - "ctrl-n": "menu::SelectNext" - } + "ctrl-n": "menu::SelectNext", + }, }, { "context": "GitCommit > Editor && VimControl && vim_mode == normal", "bindings": { "ctrl-c": "menu::Cancel", - "escape": "menu::Cancel" - } + "escape": "menu::Cancel", + }, }, { "context": "Editor && edit_prediction", "bindings": { // This is identical to the binding in the base keymap, but the vim bindings above to // "vim::Tab" shadow it, so it needs to be bound again. - "tab": "editor::AcceptEditPrediction" - } + "tab": "editor::AcceptEditPrediction", + }, }, { "context": "MessageEditor > Editor && VimControl", "bindings": { - "enter": "agent::Chat" - } + "enter": "agent::Chat", + }, }, { "context": "os != macos && Editor && edit_prediction_conflict", @@ -1033,8 +1069,8 @@ // 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 on Linux // and Windows. - "alt-l": "editor::AcceptEditPrediction" - } + "alt-l": "editor::AcceptEditPrediction", + }, }, { "context": "SettingsWindow > NavigationMenu && !search", @@ -1044,8 +1080,8 @@ "k": "settings_editor::FocusPreviousNavEntry", "j": "settings_editor::FocusNextNavEntry", "g g": "settings_editor::FocusFirstNavEntry", - "shift-g": "settings_editor::FocusLastNavEntry" - } + "shift-g": "settings_editor::FocusLastNavEntry", + }, }, { "context": "MarkdownPreview", @@ -1053,7 +1089,7 @@ "ctrl-u": "markdown::ScrollPageUp", "ctrl-d": "markdown::ScrollPageDown", "ctrl-y": "markdown::ScrollUp", - "ctrl-e": "markdown::ScrollDown" - } - } + "ctrl-e": "markdown::ScrollDown", + }, + }, ] diff --git a/assets/prompts/content_prompt_v2.hbs b/assets/prompts/content_prompt_v2.hbs index e1b6ddc6f023e9e97c9bb851473ac02e989c8feb..87376f49f12f0e27cc61e9f9747d9de6bfde43cb 100644 --- a/assets/prompts/content_prompt_v2.hbs +++ b/assets/prompts/content_prompt_v2.hbs @@ -39,6 +39,5 @@ Only make changes that are necessary to fulfill the prompt, leave everything els Start at the indentation level in the original file in the rewritten {{content_type}}. -You must use one of the provided tools to make the rewrite or to provide an explanation as to why the user's request cannot be fulfilled. It is an error if -you simply send back unstructured text. If you need to make a statement or ask a question you must use one of the tools to do so. +IMPORTANT: You MUST use one of the provided tools to make the rewrite or to provide an explanation as to why the user's request cannot be fulfilled. You MUST NOT send back unstructured text. If you need to make a statement or ask a question you MUST use one of the tools to do so. It is an error if you try to make a change that cannot be made simply by editing the rewrite_section. diff --git a/assets/settings/default.json b/assets/settings/default.json index 8dc9f837199cbbe1ff1d0bf2caa62b6a60263902..39a5f26b6201ad4a0b765c2f19b387fd4fc9c855 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -436,6 +436,8 @@ "show_onboarding_banner": true, // Whether to show user picture in the titlebar. "show_user_picture": true, + // Whether to show the user menu in the titlebar. + "show_user_menu": true, // Whether to show the sign in button in the titlebar. "show_sign_in": true, // Whether to show the menus in the titlebar. @@ -896,6 +898,8 @@ "default_width": 380, }, "agent": { + // Whether the inline assistant should use streaming tools, when available + "inline_assistant_use_streaming_tools": true, // Whether the agent is enabled. "enabled": true, // What completion mode to start new threads in, if available. Can be 'normal' or 'burn'. @@ -904,6 +908,8 @@ "button": true, // Where to dock the agent panel. Can be 'left', 'right' or 'bottom'. "dock": "right", + // Where to dock the agents panel. Can be 'left' or 'right'. + "agents_panel_dock": "left", // Default width when the agent panel is docked to the left or right. "default_width": 640, // Default height when the agent panel is docked to the bottom. @@ -1410,8 +1416,9 @@ "proxy_no_verify": null, }, "codestral": { - "model": null, - "max_tokens": null, + "api_url": "https://codestral.mistral.ai", + "model": "codestral-latest", + "max_tokens": 150, }, // Whether edit predictions are enabled when editing text threads in the agent panel. // This setting has no effect if globally disabled. @@ -1932,6 +1939,9 @@ "words": "disabled", }, }, + "Proto": { + "language_servers": ["buf", "!protols", "!protobuf-language-server", "..."], + }, "Python": { "code_actions_on_format": { "source.organizeImports.ruff": true, diff --git a/assets/settings/initial_debug_tasks.json b/assets/settings/initial_debug_tasks.json index af4512bd51aa82d57ce62e605b45ee61e8f98030..851289392a65aecfca17e00d4c123823ac9e21cb 100644 --- a/assets/settings/initial_debug_tasks.json +++ b/assets/settings/initial_debug_tasks.json @@ -8,7 +8,7 @@ "adapter": "Debugpy", "program": "$ZED_FILE", "request": "launch", - "cwd": "$ZED_WORKTREE_ROOT" + "cwd": "$ZED_WORKTREE_ROOT", }, { "label": "Debug active JavaScript file", @@ -16,7 +16,7 @@ "program": "$ZED_FILE", "request": "launch", "cwd": "$ZED_WORKTREE_ROOT", - "type": "pwa-node" + "type": "pwa-node", }, { "label": "JavaScript debug terminal", @@ -24,6 +24,6 @@ "request": "launch", "cwd": "$ZED_WORKTREE_ROOT", "console": "integratedTerminal", - "type": "pwa-node" - } + "type": "pwa-node", + }, ] diff --git a/assets/settings/initial_server_settings.json b/assets/settings/initial_server_settings.json index d6ec33e60128380378610a273a1bbdff1ecdbaa8..29aa569b105157df7ec48164e2066fdac72c7b41 100644 --- a/assets/settings/initial_server_settings.json +++ b/assets/settings/initial_server_settings.json @@ -3,5 +3,5 @@ // For a full list of overridable settings, and general information on settings, // see the documentation: https://zed.dev/docs/configuring-zed#settings-files { - "lsp": {} + "lsp": {}, } diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index a79e98063237ca297a89b0d151bd48149061b7bb..5bedafbd3a1e75a755598e37cd673742e146fdcc 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -47,8 +47,8 @@ // 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_command": true + "show_command": true, // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. // "tags": [] - } + }, ] diff --git a/assets/settings/initial_user_settings.json b/assets/settings/initial_user_settings.json index 5ac2063bdb481e057a2d124c1e72f998390b066b..8b573854895a03243803a71a91a35af647f45ca2 100644 --- a/assets/settings/initial_user_settings.json +++ b/assets/settings/initial_user_settings.json @@ -12,6 +12,6 @@ "theme": { "mode": "system", "light": "One Light", - "dark": "One Dark" - } + "dark": "One Dark", + }, } diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json index 90973fd6c3469a1ef0e698d629376dfaaf3b5a76..16ae188712f7a800ab4fb8a81a2d24cac99da56b 100644 --- a/assets/themes/gruvbox/gruvbox.json +++ b/assets/themes/gruvbox/gruvbox.json @@ -71,33 +71,33 @@ "editor.document_highlight.read_background": "#83a5981a", "editor.document_highlight.write_background": "#92847466", "terminal.background": "#282828ff", - "terminal.foreground": "#fbf1c7ff", + "terminal.foreground": "#ebdbb2ff", "terminal.bright_foreground": "#fbf1c7ff", - "terminal.dim_foreground": "#282828ff", + "terminal.dim_foreground": "#766b5dff", "terminal.ansi.black": "#282828ff", - "terminal.ansi.bright_black": "#73675eff", + "terminal.ansi.bright_black": "#928374ff", "terminal.ansi.dim_black": "#fbf1c7ff", - "terminal.ansi.red": "#fb4a35ff", - "terminal.ansi.bright_red": "#93201dff", - "terminal.ansi.dim_red": "#ffaa95ff", - "terminal.ansi.green": "#b7bb26ff", - "terminal.ansi.bright_green": "#605c1bff", - "terminal.ansi.dim_green": "#e0dc98ff", - "terminal.ansi.yellow": "#f9bd2fff", - "terminal.ansi.bright_yellow": "#91611bff", - "terminal.ansi.dim_yellow": "#fedc9bff", - "terminal.ansi.blue": "#83a598ff", - "terminal.ansi.bright_blue": "#414f4aff", - "terminal.ansi.dim_blue": "#c0d2cbff", - "terminal.ansi.magenta": "#d3869bff", - "terminal.ansi.bright_magenta": "#8e5868ff", - "terminal.ansi.dim_magenta": "#ff9ebbff", - "terminal.ansi.cyan": "#8ec07cff", - "terminal.ansi.bright_cyan": "#45603eff", - "terminal.ansi.dim_cyan": "#c7dfbdff", - "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", + "terminal.ansi.red": "#cc241dff", + "terminal.ansi.bright_red": "#fb4934ff", + "terminal.ansi.dim_red": "#8e1814ff", + "terminal.ansi.green": "#98971aff", + "terminal.ansi.bright_green": "#b8bb26ff", + "terminal.ansi.dim_green": "#6a6912ff", + "terminal.ansi.yellow": "#d79921ff", + "terminal.ansi.bright_yellow": "#fabd2fff", + "terminal.ansi.dim_yellow": "#966a17ff", + "terminal.ansi.blue": "#458588ff", + "terminal.ansi.bright_blue": "#83a598ff", + "terminal.ansi.dim_blue": "#305d5fff", + "terminal.ansi.magenta": "#b16286ff", + "terminal.ansi.bright_magenta": "#d3869bff", + "terminal.ansi.dim_magenta": "#7c455eff", + "terminal.ansi.cyan": "#689d6aff", + "terminal.ansi.bright_cyan": "#8ec07cff", + "terminal.ansi.dim_cyan": "#496e4aff", + "terminal.ansi.white": "#a89984ff", + "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.dim_white": "#766b5dff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", "version_control.modified": "#f9bd2fff", @@ -478,33 +478,33 @@ "editor.document_highlight.read_background": "#83a5981a", "editor.document_highlight.write_background": "#92847466", "terminal.background": "#1d2021ff", - "terminal.foreground": "#fbf1c7ff", + "terminal.foreground": "#ebdbb2ff", "terminal.bright_foreground": "#fbf1c7ff", - "terminal.dim_foreground": "#1d2021ff", - "terminal.ansi.black": "#1d2021ff", - "terminal.ansi.bright_black": "#73675eff", + "terminal.dim_foreground": "#766b5dff", + "terminal.ansi.black": "#282828ff", + "terminal.ansi.bright_black": "#928374ff", "terminal.ansi.dim_black": "#fbf1c7ff", - "terminal.ansi.red": "#fb4a35ff", - "terminal.ansi.bright_red": "#93201dff", - "terminal.ansi.dim_red": "#ffaa95ff", - "terminal.ansi.green": "#b7bb26ff", - "terminal.ansi.bright_green": "#605c1bff", - "terminal.ansi.dim_green": "#e0dc98ff", - "terminal.ansi.yellow": "#f9bd2fff", - "terminal.ansi.bright_yellow": "#91611bff", - "terminal.ansi.dim_yellow": "#fedc9bff", - "terminal.ansi.blue": "#83a598ff", - "terminal.ansi.bright_blue": "#414f4aff", - "terminal.ansi.dim_blue": "#c0d2cbff", - "terminal.ansi.magenta": "#d3869bff", - "terminal.ansi.bright_magenta": "#8e5868ff", - "terminal.ansi.dim_magenta": "#ff9ebbff", - "terminal.ansi.cyan": "#8ec07cff", - "terminal.ansi.bright_cyan": "#45603eff", - "terminal.ansi.dim_cyan": "#c7dfbdff", - "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", + "terminal.ansi.red": "#cc241dff", + "terminal.ansi.bright_red": "#fb4934ff", + "terminal.ansi.dim_red": "#8e1814ff", + "terminal.ansi.green": "#98971aff", + "terminal.ansi.bright_green": "#b8bb26ff", + "terminal.ansi.dim_green": "#6a6912ff", + "terminal.ansi.yellow": "#d79921ff", + "terminal.ansi.bright_yellow": "#fabd2fff", + "terminal.ansi.dim_yellow": "#966a17ff", + "terminal.ansi.blue": "#458588ff", + "terminal.ansi.bright_blue": "#83a598ff", + "terminal.ansi.dim_blue": "#305d5fff", + "terminal.ansi.magenta": "#b16286ff", + "terminal.ansi.bright_magenta": "#d3869bff", + "terminal.ansi.dim_magenta": "#7c455eff", + "terminal.ansi.cyan": "#689d6aff", + "terminal.ansi.bright_cyan": "#8ec07cff", + "terminal.ansi.dim_cyan": "#496e4aff", + "terminal.ansi.white": "#a89984ff", + "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.dim_white": "#766b5dff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", "version_control.modified": "#f9bd2fff", @@ -885,33 +885,33 @@ "editor.document_highlight.read_background": "#83a5981a", "editor.document_highlight.write_background": "#92847466", "terminal.background": "#32302fff", - "terminal.foreground": "#fbf1c7ff", + "terminal.foreground": "#ebdbb2ff", "terminal.bright_foreground": "#fbf1c7ff", - "terminal.dim_foreground": "#32302fff", - "terminal.ansi.black": "#32302fff", - "terminal.ansi.bright_black": "#73675eff", + "terminal.dim_foreground": "#766b5dff", + "terminal.ansi.black": "#282828ff", + "terminal.ansi.bright_black": "#928374ff", "terminal.ansi.dim_black": "#fbf1c7ff", - "terminal.ansi.red": "#fb4a35ff", - "terminal.ansi.bright_red": "#93201dff", - "terminal.ansi.dim_red": "#ffaa95ff", - "terminal.ansi.green": "#b7bb26ff", - "terminal.ansi.bright_green": "#605c1bff", - "terminal.ansi.dim_green": "#e0dc98ff", - "terminal.ansi.yellow": "#f9bd2fff", - "terminal.ansi.bright_yellow": "#91611bff", - "terminal.ansi.dim_yellow": "#fedc9bff", - "terminal.ansi.blue": "#83a598ff", - "terminal.ansi.bright_blue": "#414f4aff", - "terminal.ansi.dim_blue": "#c0d2cbff", - "terminal.ansi.magenta": "#d3869bff", - "terminal.ansi.bright_magenta": "#8e5868ff", - "terminal.ansi.dim_magenta": "#ff9ebbff", - "terminal.ansi.cyan": "#8ec07cff", - "terminal.ansi.bright_cyan": "#45603eff", - "terminal.ansi.dim_cyan": "#c7dfbdff", - "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", + "terminal.ansi.red": "#cc241dff", + "terminal.ansi.bright_red": "#fb4934ff", + "terminal.ansi.dim_red": "#8e1814ff", + "terminal.ansi.green": "#98971aff", + "terminal.ansi.bright_green": "#b8bb26ff", + "terminal.ansi.dim_green": "#6a6912ff", + "terminal.ansi.yellow": "#d79921ff", + "terminal.ansi.bright_yellow": "#fabd2fff", + "terminal.ansi.dim_yellow": "#966a17ff", + "terminal.ansi.blue": "#458588ff", + "terminal.ansi.bright_blue": "#83a598ff", + "terminal.ansi.dim_blue": "#305d5fff", + "terminal.ansi.magenta": "#b16286ff", + "terminal.ansi.bright_magenta": "#d3869bff", + "terminal.ansi.dim_magenta": "#7c455eff", + "terminal.ansi.cyan": "#689d6aff", + "terminal.ansi.bright_cyan": "#8ec07cff", + "terminal.ansi.dim_cyan": "#496e4aff", + "terminal.ansi.white": "#a89984ff", + "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.dim_white": "#766b5dff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", "version_control.modified": "#f9bd2fff", @@ -1295,30 +1295,30 @@ "terminal.foreground": "#282828ff", "terminal.bright_foreground": "#282828ff", "terminal.dim_foreground": "#fbf1c7ff", - "terminal.ansi.black": "#282828ff", - "terminal.ansi.bright_black": "#0b6678ff", - "terminal.ansi.dim_black": "#5f5650ff", - "terminal.ansi.red": "#9d0308ff", - "terminal.ansi.bright_red": "#db8b7aff", - "terminal.ansi.dim_red": "#4e1207ff", - "terminal.ansi.green": "#797410ff", - "terminal.ansi.bright_green": "#bfb787ff", - "terminal.ansi.dim_green": "#3e3a11ff", - "terminal.ansi.yellow": "#b57615ff", - "terminal.ansi.bright_yellow": "#e2b88bff", - "terminal.ansi.dim_yellow": "#5c3a12ff", - "terminal.ansi.blue": "#0b6678ff", - "terminal.ansi.bright_blue": "#8fb0baff", - "terminal.ansi.dim_blue": "#14333bff", - "terminal.ansi.magenta": "#8f3e71ff", - "terminal.ansi.bright_magenta": "#c76da0ff", - "terminal.ansi.dim_magenta": "#5c2848ff", - "terminal.ansi.cyan": "#437b59ff", - "terminal.ansi.bright_cyan": "#9fbca8ff", - "terminal.ansi.dim_cyan": "#253e2eff", - "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", + "terminal.ansi.black": "#fbf1c7ff", + "terminal.ansi.bright_black": "#928374ff", + "terminal.ansi.dim_black": "#7c6f64ff", + "terminal.ansi.red": "#cc241dff", + "terminal.ansi.bright_red": "#9d0006ff", + "terminal.ansi.dim_red": "#c31c16ff", + "terminal.ansi.green": "#98971aff", + "terminal.ansi.bright_green": "#79740eff", + "terminal.ansi.dim_green": "#929015ff", + "terminal.ansi.yellow": "#d79921ff", + "terminal.ansi.bright_yellow": "#b57614ff", + "terminal.ansi.dim_yellow": "#cf8e1aff", + "terminal.ansi.blue": "#458588ff", + "terminal.ansi.bright_blue": "#076678ff", + "terminal.ansi.dim_blue": "#356f77ff", + "terminal.ansi.magenta": "#b16286ff", + "terminal.ansi.bright_magenta": "#8f3f71ff", + "terminal.ansi.dim_magenta": "#a85580ff", + "terminal.ansi.cyan": "#689d6aff", + "terminal.ansi.bright_cyan": "#427b58ff", + "terminal.ansi.dim_cyan": "#5f9166ff", + "terminal.ansi.white": "#7c6f64ff", + "terminal.ansi.bright_white": "#282828ff", + "terminal.ansi.dim_white": "#282828ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", "version_control.modified": "#b57615ff", @@ -1702,30 +1702,30 @@ "terminal.foreground": "#282828ff", "terminal.bright_foreground": "#282828ff", "terminal.dim_foreground": "#f9f5d7ff", - "terminal.ansi.black": "#282828ff", - "terminal.ansi.bright_black": "#73675eff", - "terminal.ansi.dim_black": "#f9f5d7ff", - "terminal.ansi.red": "#9d0308ff", - "terminal.ansi.bright_red": "#db8b7aff", - "terminal.ansi.dim_red": "#4e1207ff", - "terminal.ansi.green": "#797410ff", - "terminal.ansi.bright_green": "#bfb787ff", - "terminal.ansi.dim_green": "#3e3a11ff", - "terminal.ansi.yellow": "#b57615ff", - "terminal.ansi.bright_yellow": "#e2b88bff", - "terminal.ansi.dim_yellow": "#5c3a12ff", - "terminal.ansi.blue": "#0b6678ff", - "terminal.ansi.bright_blue": "#8fb0baff", - "terminal.ansi.dim_blue": "#14333bff", - "terminal.ansi.magenta": "#8f3e71ff", - "terminal.ansi.bright_magenta": "#c76da0ff", - "terminal.ansi.dim_magenta": "#5c2848ff", - "terminal.ansi.cyan": "#437b59ff", - "terminal.ansi.bright_cyan": "#9fbca8ff", - "terminal.ansi.dim_cyan": "#253e2eff", - "terminal.ansi.white": "#f9f5d7ff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", + "terminal.ansi.black": "#fbf1c7ff", + "terminal.ansi.bright_black": "#928374ff", + "terminal.ansi.dim_black": "#7c6f64ff", + "terminal.ansi.red": "#cc241dff", + "terminal.ansi.bright_red": "#9d0006ff", + "terminal.ansi.dim_red": "#c31c16ff", + "terminal.ansi.green": "#98971aff", + "terminal.ansi.bright_green": "#79740eff", + "terminal.ansi.dim_green": "#929015ff", + "terminal.ansi.yellow": "#d79921ff", + "terminal.ansi.bright_yellow": "#b57614ff", + "terminal.ansi.dim_yellow": "#cf8e1aff", + "terminal.ansi.blue": "#458588ff", + "terminal.ansi.bright_blue": "#076678ff", + "terminal.ansi.dim_blue": "#356f77ff", + "terminal.ansi.magenta": "#b16286ff", + "terminal.ansi.bright_magenta": "#8f3f71ff", + "terminal.ansi.dim_magenta": "#a85580ff", + "terminal.ansi.cyan": "#689d6aff", + "terminal.ansi.bright_cyan": "#427b58ff", + "terminal.ansi.dim_cyan": "#5f9166ff", + "terminal.ansi.white": "#7c6f64ff", + "terminal.ansi.bright_white": "#282828ff", + "terminal.ansi.dim_white": "#282828ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", "version_control.modified": "#b57615ff", @@ -2109,30 +2109,30 @@ "terminal.foreground": "#282828ff", "terminal.bright_foreground": "#282828ff", "terminal.dim_foreground": "#f2e5bcff", - "terminal.ansi.black": "#282828ff", - "terminal.ansi.bright_black": "#73675eff", - "terminal.ansi.dim_black": "#f2e5bcff", - "terminal.ansi.red": "#9d0308ff", - "terminal.ansi.bright_red": "#db8b7aff", - "terminal.ansi.dim_red": "#4e1207ff", - "terminal.ansi.green": "#797410ff", - "terminal.ansi.bright_green": "#bfb787ff", - "terminal.ansi.dim_green": "#3e3a11ff", - "terminal.ansi.yellow": "#b57615ff", - "terminal.ansi.bright_yellow": "#e2b88bff", - "terminal.ansi.dim_yellow": "#5c3a12ff", - "terminal.ansi.blue": "#0b6678ff", - "terminal.ansi.bright_blue": "#8fb0baff", - "terminal.ansi.dim_blue": "#14333bff", - "terminal.ansi.magenta": "#8f3e71ff", - "terminal.ansi.bright_magenta": "#c76da0ff", - "terminal.ansi.dim_magenta": "#5c2848ff", - "terminal.ansi.cyan": "#437b59ff", - "terminal.ansi.bright_cyan": "#9fbca8ff", - "terminal.ansi.dim_cyan": "#253e2eff", - "terminal.ansi.white": "#f2e5bcff", - "terminal.ansi.bright_white": "#ffffffff", - "terminal.ansi.dim_white": "#b0a189ff", + "terminal.ansi.black": "#fbf1c7ff", + "terminal.ansi.bright_black": "#928374ff", + "terminal.ansi.dim_black": "#7c6f64ff", + "terminal.ansi.red": "#cc241dff", + "terminal.ansi.bright_red": "#9d0006ff", + "terminal.ansi.dim_red": "#c31c16ff", + "terminal.ansi.green": "#98971aff", + "terminal.ansi.bright_green": "#79740eff", + "terminal.ansi.dim_green": "#929015ff", + "terminal.ansi.yellow": "#d79921ff", + "terminal.ansi.bright_yellow": "#b57614ff", + "terminal.ansi.dim_yellow": "#cf8e1aff", + "terminal.ansi.blue": "#458588ff", + "terminal.ansi.bright_blue": "#076678ff", + "terminal.ansi.dim_blue": "#356f77ff", + "terminal.ansi.magenta": "#b16286ff", + "terminal.ansi.bright_magenta": "#8f3f71ff", + "terminal.ansi.dim_magenta": "#a85580ff", + "terminal.ansi.cyan": "#689d6aff", + "terminal.ansi.bright_cyan": "#427b58ff", + "terminal.ansi.dim_cyan": "#5f9166ff", + "terminal.ansi.white": "#7c6f64ff", + "terminal.ansi.bright_white": "#282828ff", + "terminal.ansi.dim_white": "#282828ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", "version_control.modified": "#b57615ff", diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index f17e9d0fce404483ae99efc95bf666586c1f644b..cae1aad90810c217324659d29c065af443494933 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -166,7 +166,7 @@ impl Diff { } pub fn has_revealed_range(&self, cx: &App) -> bool { - self.multibuffer().read(cx).excerpt_paths().next().is_some() + self.multibuffer().read(cx).paths().next().is_some() } pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool { diff --git a/crates/acp_thread/src/terminal.rs b/crates/acp_thread/src/terminal.rs index 2da4125209d3bcf902d23380c5273d9b31902905..f70e044fbc1b380768dbcd807f1833f6fb5cd48b 100644 --- a/crates/acp_thread/src/terminal.rs +++ b/crates/acp_thread/src/terminal.rs @@ -187,8 +187,10 @@ pub async fn create_terminal_entity( Default::default() }; - // Disables paging for `git` and hopefully other commands + // Disable pagers so agent/terminal commands don't hang behind interactive UIs env.insert("PAGER".into(), "".into()); + // Override user core.pager (e.g. delta) which Git prefers over PAGER + env.insert("GIT_PAGER".into(), "cat".into()); env.extend(env_vars); // Use remote shell or default system shell, as appropriate diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index 0905effce38d1bfd4fa18e1d00169d6c7ef6c2d7..b0d30367da0634dc82f8db96fc099e268aa4790e 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -371,13 +371,13 @@ impl AcpTools { syntax: cx.theme().syntax().clone(), code_block_overflow_x_scroll: true, code_block: StyleRefinement { - text: Some(TextStyleRefinement { + text: TextStyleRefinement { font_family: Some( theme_settings.buffer_font.family.clone(), ), font_size: Some((base_size * 0.8).into()), ..Default::default() - }), + }, ..Default::default() }, ..Default::default() diff --git a/crates/agent/src/edit_agent/evals.rs b/crates/agent/src/edit_agent/evals.rs index edf8a0f671d231b3bfbd29526c256388fd41f85a..01c81e0103a2d3624c7e8eb9b9c587726fcc4876 100644 --- a/crates/agent/src/edit_agent/evals.rs +++ b/crates/agent/src/edit_agent/evals.rs @@ -1343,6 +1343,7 @@ fn run_eval(eval: EvalInput) -> eval_utils::EvalOutput { let test = EditAgentTest::new(&mut cx).await; test.eval(eval, &mut cx).await }); + cx.quit(); match result { Ok(output) => eval_utils::EvalOutput { data: output.to_string(), diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 41aff48a2092645764d598684d13c1ce61704c44..e99855fe8a7241468e93f01fe6c7b6fee161f600 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -89,7 +89,7 @@ impl AcpConnection { cx: &mut AsyncApp, ) -> Result { let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?; - let builder = ShellBuilder::new(&shell, cfg!(windows)); + let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive(); let mut child = builder.build_command(Some(command.path.display().to_string()), &command.args); child diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 084ac7c3e7a1be4920126f857145e64b65a255dd..25ca5c78d6b76145a1b1b5d19ac86246ff419d1d 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -9,7 +9,7 @@ use project::DisableAiSettings; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ - DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection, + DefaultAgentView, DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection, NotifyWhenAgentWaiting, RegisterSetting, Settings, }; @@ -24,10 +24,12 @@ pub struct AgentSettings { pub enabled: bool, pub button: bool, pub dock: DockPosition, + pub agents_panel_dock: DockSide, pub default_width: Pixels, pub default_height: Pixels, pub default_model: Option, pub inline_assistant_model: Option, + pub inline_assistant_use_streaming_tools: bool, pub commit_message_model: Option, pub thread_summary_model: Option, pub inline_alternatives: Vec, @@ -151,10 +153,14 @@ impl Settings for AgentSettings { enabled: agent.enabled.unwrap(), button: agent.button.unwrap(), dock: agent.dock.unwrap(), + agents_panel_dock: agent.agents_panel_dock.unwrap(), default_width: px(agent.default_width.unwrap()), default_height: px(agent.default_height.unwrap()), default_model: Some(agent.default_model.unwrap()), inline_assistant_model: agent.inline_assistant_model, + inline_assistant_use_streaming_tools: agent + .inline_assistant_use_streaming_tools + .unwrap_or(true), commit_message_model: agent.commit_message_model, thread_summary_model: agent.thread_summary_model, inline_alternatives: agent.inline_alternatives.unwrap_or_default(), diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 2af0ce6fbd2b636d19d9cb8e544851514800313c..38580b4d2c61597718d9fb718a20e52e84222481 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -13,7 +13,7 @@ path = "src/agent_ui.rs" doctest = false [features] -test-support = ["gpui/test-support", "language/test-support", "reqwest_client"] +test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support"] unit-eval = [] [dependencies] @@ -40,6 +40,7 @@ component.workspace = true context_server.workspace = true db.workspace = true editor.workspace = true +eval_utils = { workspace = true, optional = true } extension.workspace = true extension_host.workspace = true feature_flags.workspace = true @@ -71,6 +72,7 @@ postage.workspace = true project.workspace = true prompt_store.workspace = true proto.workspace = true +rand.workspace = true release_channel.workspace = true rope.workspace = true rules_library.workspace = true @@ -84,7 +86,6 @@ smol.workspace = true streaming_diff.workspace = true task.workspace = true telemetry.workspace = true -telemetry_events.workspace = true terminal.workspace = true terminal_view.workspace = true text.workspace = true @@ -120,7 +121,6 @@ language_model = { workspace = true, "features" = ["test-support"] } pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } semver.workspace = true -rand.workspace = true reqwest_client.workspace = true tree-sitter-md.workspace = true unindent.workspace = true diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 63ea9eb279d26ff610c12f9785ef882be61f5e26..6cd2ec2fa3442bbf4961dffb0c4538ac9615d982 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -6053,13 +6053,13 @@ fn default_markdown_style( }, border_color: Some(colors.border_variant), background: Some(colors.editor_background.into()), - text: Some(TextStyleRefinement { + text: TextStyleRefinement { font_family: Some(theme_settings.buffer_font.family.clone()), font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), font_features: Some(theme_settings.buffer_font.features.clone()), font_size: Some(buffer_font_size.into()), ..Default::default() - }), + }, ..Default::default() }, inline_code: TextStyleRefinement { diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 2d4b18c34cb67de3859f1bcbda14ec3795b1e08d..cc29d2137dc5f5dd55522f49475f5aff7dbef868 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -34,9 +34,9 @@ use project::{ }; use settings::{Settings, SettingsStore, update_settings_file}; use ui::{ - Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, - Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, - PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*, + ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider, + DividerColor, ElevationIndex, Indicator, LabelSize, PopoverMenu, Switch, SwitchColor, Tooltip, + WithScrollbar, prelude::*, }; use util::ResultExt as _; use workspace::{Workspace, create_and_open_local_file}; @@ -980,7 +980,7 @@ impl AgentConfiguration { let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) { AgentIcon::Path(icon_path) } else { - AgentIcon::Name(IconName::Ai) + AgentIcon::Name(IconName::Sparkle) }; let display_name = agent_server_store .agent_display_name(&name) @@ -1142,6 +1142,7 @@ impl AgentConfiguration { ) -> impl IntoElement { let id = id.into(); let display_name = display_name.into(); + let icon = match icon { AgentIcon::Name(icon_name) => Icon::new(icon_name) .size(IconSize::Small) diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 11acd649ef9df500edf99926e754228e4c41e7bc..06fce64819d3ce66b9e39f2b83cbebefb6ba9698 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -130,7 +130,12 @@ impl AgentDiffPane { .action_log() .read(cx) .changed_buffers(cx); - let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::>(); + let mut paths_to_delete = self + .multibuffer + .read(cx) + .paths() + .cloned() + .collect::>(); for (buffer, diff_handle) in changed_buffers { if buffer.read(cx).file().is_none() { diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 79c41e97ac3af1a257d9cb448b42e9649f8591e0..37c771d0e36d088a1fa8bec136ab6cb45dc1c2e8 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -259,7 +259,7 @@ impl AgentType { Self::Gemini => Some(IconName::AiGemini), Self::ClaudeCode => Some(IconName::AiClaude), Self::Codex => Some(IconName::AiOpenAi), - Self::Custom { .. } => Some(IconName::Terminal), + Self::Custom { .. } => Some(IconName::Sparkle), } } } @@ -1851,14 +1851,17 @@ impl AgentPanel { let agent_server_store = self.project.read(cx).agent_server_store().clone(); let focus_handle = self.focus_handle(cx); - // Get custom icon path for selected agent before building menu (to avoid borrow issues) - let selected_agent_custom_icon = + let (selected_agent_custom_icon, selected_agent_label) = if let AgentType::Custom { name, .. } = &self.selected_agent { - agent_server_store - .read(cx) - .agent_icon(&ExternalAgentServerName(name.clone())) + let store = agent_server_store.read(cx); + let icon = store.agent_icon(&ExternalAgentServerName(name.clone())); + + let label = store + .agent_display_name(&ExternalAgentServerName(name.clone())) + .unwrap_or_else(|| self.selected_agent.label()); + (icon, label) } else { - None + (None, self.selected_agent.label()) }; let active_thread = match &self.active_view { @@ -2090,7 +2093,7 @@ impl AgentPanel { if let Some(icon_path) = icon_path { entry = entry.custom_icon_svg(icon_path); } else { - entry = entry.icon(IconName::Terminal); + entry = entry.icon(IconName::Sparkle); } entry = entry .when( @@ -2154,8 +2157,6 @@ impl AgentPanel { } }); - let selected_agent_label = self.selected_agent.label(); - let is_thread_loading = self .active_thread_view() .map(|thread| thread.read(cx).is_loading()) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 970ad369caba5f10523cab5e0fc2372e7ed6a2a9..83614bdb422926ab41195bf3380b7df1abcde8a4 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -1,4 +1,4 @@ -mod acp; +pub mod acp; mod agent_configuration; mod agent_diff; mod agent_model_selector; @@ -7,8 +7,6 @@ mod buffer_codegen; mod completion_provider; mod context; mod context_server_configuration; -#[cfg(test)] -mod evals; mod inline_assistant; mod inline_prompt_editor; mod language_model_selector; @@ -28,7 +26,7 @@ use agent_settings::{AgentProfileId, AgentSettings}; use assistant_slash_command::SlashCommandRegistry; use client::Client; use command_palette_hooks::CommandPaletteFilter; -use feature_flags::FeatureFlagAppExt as _; +use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _}; use fs::Fs; use gpui::{Action, App, Entity, SharedString, actions}; use language::{ @@ -216,7 +214,7 @@ pub fn init( is_eval: bool, cx: &mut App, ) { - assistant_text_thread::init(client.clone(), cx); + assistant_text_thread::init(client, cx); rules_library::init(cx); if !is_eval { // Initializing the language model from the user settings messes with the eval, so we only initialize them when @@ -229,13 +227,8 @@ pub fn init( TextThreadEditor::init(cx); register_slash_commands(cx); - inline_assistant::init( - fs.clone(), - prompt_builder.clone(), - client.telemetry().clone(), - cx, - ); - terminal_inline_assistant::init(fs.clone(), prompt_builder, client.telemetry().clone(), cx); + inline_assistant::init(fs.clone(), prompt_builder.clone(), cx); + terminal_inline_assistant::init(fs.clone(), prompt_builder, cx); cx.observe_new(move |workspace, window, cx| { ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx) }) @@ -251,11 +244,17 @@ pub fn init( update_command_palette_filter(app_cx); }) .detach(); + + cx.on_flags_ready(|_, cx| { + update_command_palette_filter(cx); + }) + .detach(); } fn update_command_palette_filter(cx: &mut App) { let disable_ai = DisableAiSettings::get_global(cx).disable_ai; let agent_enabled = AgentSettings::get_global(cx).enabled; + let agent_v2_enabled = cx.has_flag::(); let edit_prediction_provider = AllLanguageSettings::get_global(cx) .edit_predictions .provider; @@ -276,6 +275,7 @@ fn update_command_palette_filter(cx: &mut App) { if disable_ai { filter.hide_namespace("agent"); + filter.hide_namespace("agents"); filter.hide_namespace("assistant"); filter.hide_namespace("copilot"); filter.hide_namespace("supermaven"); @@ -287,8 +287,10 @@ fn update_command_palette_filter(cx: &mut App) { } else { if agent_enabled { filter.show_namespace("agent"); + filter.show_namespace("agents"); } else { filter.hide_namespace("agent"); + filter.hide_namespace("agents"); } filter.show_namespace("assistant"); @@ -324,6 +326,9 @@ fn update_command_palette_filter(cx: &mut App) { filter.show_namespace("zed_predict_onboarding"); filter.show_action_types(&[TypeId::of::()]); + if !agent_v2_enabled { + filter.hide_action_types(&[TypeId::of::()]); + } } }); } @@ -446,7 +451,7 @@ mod tests { use gpui::{BorrowAppContext, TestAppContext, px}; use project::DisableAiSettings; use settings::{ - DefaultAgentView, DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore, + DefaultAgentView, DockPosition, DockSide, NotifyWhenAgentWaiting, Settings, SettingsStore, }; #[gpui::test] @@ -465,10 +470,12 @@ mod tests { enabled: true, button: true, dock: DockPosition::Right, + agents_panel_dock: DockSide::Left, default_width: px(300.), default_height: px(600.), default_model: None, inline_assistant_model: None, + inline_assistant_use_streaming_tools: false, commit_message_model: None, thread_summary_model: None, inline_alternatives: vec![], diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index 1cd7bec7b5b2c24cfbcf01a20091e8a07608e73a..d8d0efda0fbd70153b02452f6281ee66b90eca92 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -1,23 +1,26 @@ use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus}; use agent_settings::AgentSettings; use anyhow::{Context as _, Result}; -use client::telemetry::Telemetry; +use uuid::Uuid; + use cloud_llm_client::CompletionIntent; use collections::HashSet; use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint}; -use feature_flags::{FeatureFlagAppExt as _, InlineAssistantV2FeatureFlag}; +use feature_flags::{FeatureFlagAppExt as _, InlineAssistantUseToolFeatureFlag}; use futures::{ SinkExt, Stream, StreamExt, TryStreamExt as _, channel::mpsc, future::{LocalBoxFuture, Shared}, join, + stream::BoxStream, }; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task}; -use language::{Buffer, IndentKind, Point, TransactionId, line_diff}; +use language::{Buffer, IndentKind, LanguageName, Point, TransactionId, line_diff}; use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelRegistry, LanguageModelRequest, - LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelTextStream, Role, - report_assistant_event, + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, + LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, + LanguageModelRequestTool, LanguageModelTextStream, LanguageModelToolChoice, + LanguageModelToolUse, Role, TokenUsage, }; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; @@ -25,6 +28,7 @@ use prompt_store::PromptBuilder; use rope::Rope; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use settings::Settings as _; use smol::future::FutureExt; use std::{ cmp, @@ -37,28 +41,24 @@ use std::{ time::Instant, }; use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff}; -use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; -use ui::SharedString; -/// Use this tool to provide a message to the user when you're unable to complete a task. +/// Use this tool when you cannot or should not make a rewrite. This includes: +/// - The user's request is unclear, ambiguous, or nonsensical +/// - The requested change cannot be made by only editing the section #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct FailureMessageInput { /// A brief message to the user explaining why you're unable to fulfill the request or to ask a question about the request. - /// - /// The message may use markdown formatting if you wish. + #[serde(default)] pub message: String, } /// Replaces text in tags with your replacement_text. +/// Only use this tool when you are confident you understand the user's request and can fulfill it +/// by editing the marked section. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct RewriteSectionInput { - /// A brief description of the edit you have made. - /// - /// The description may use markdown formatting if you wish. - /// This is optional - if the edit is simple or obvious, you should leave it empty. - pub description: String, - /// The text to replace the section with. + #[serde(default)] pub replacement_text: String, } @@ -70,9 +70,9 @@ pub struct BufferCodegen { buffer: Entity, range: Range, initial_transaction_id: Option, - telemetry: Arc, builder: Arc, pub is_insertion: bool, + session_id: Uuid, } impl BufferCodegen { @@ -80,7 +80,7 @@ impl BufferCodegen { buffer: Entity, range: Range, initial_transaction_id: Option, - telemetry: Arc, + session_id: Uuid, builder: Arc, cx: &mut Context, ) -> Self { @@ -89,8 +89,8 @@ impl BufferCodegen { buffer.clone(), range.clone(), false, - Some(telemetry.clone()), builder.clone(), + session_id, cx, ) }); @@ -103,8 +103,8 @@ impl BufferCodegen { buffer, range, initial_transaction_id, - telemetry, builder, + session_id, }; this.activate(0, cx); this @@ -127,6 +127,10 @@ impl BufferCodegen { &self.alternatives[self.active_alternative] } + pub fn language_name(&self, cx: &App) -> Option { + self.active_alternative().read(cx).language_name(cx) + } + pub fn status<'a>(&self, cx: &'a App) -> &'a CodegenStatus { &self.active_alternative().read(cx).status } @@ -185,8 +189,8 @@ impl BufferCodegen { self.buffer.clone(), self.range.clone(), false, - Some(self.telemetry.clone()), self.builder.clone(), + self.session_id, cx, ) })); @@ -249,6 +253,10 @@ impl BufferCodegen { pub fn selected_text<'a>(&self, cx: &'a App) -> Option<&'a str> { self.active_alternative().read(cx).selected_text() } + + pub fn session_id(&self) -> Uuid { + self.session_id + } } impl EventEmitter for BufferCodegen {} @@ -264,7 +272,6 @@ pub struct CodegenAlternative { status: CodegenStatus, generation: Task<()>, diff: Diff, - telemetry: Option>, _subscription: gpui::Subscription, builder: Arc, active: bool, @@ -274,7 +281,9 @@ pub struct CodegenAlternative { completion: Option, selected_text: Option, pub message_id: Option, - pub model_explanation: Option, + session_id: Uuid, + pub description: Option, + pub failure: Option, } impl EventEmitter for CodegenAlternative {} @@ -284,8 +293,8 @@ impl CodegenAlternative { buffer: Entity, range: Range, active: bool, - telemetry: Option>, builder: Arc, + session_id: Uuid, cx: &mut Context, ) -> Self { let snapshot = buffer.read(cx).snapshot(cx); @@ -324,7 +333,6 @@ impl CodegenAlternative { status: CodegenStatus::Idle, generation: Task::ready(()), diff: Diff::default(), - telemetry, builder, active: active, edits: Vec::new(), @@ -333,11 +341,20 @@ impl CodegenAlternative { elapsed_time: None, completion: None, selected_text: None, - model_explanation: None, + session_id, + description: None, + failure: None, _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), } } + pub fn language_name(&self, cx: &App) -> Option { + self.old_buffer + .read(cx) + .language() + .map(|language| language.name()) + } + pub fn set_active(&mut self, active: bool, cx: &mut Context) { if active != self.active { self.active = active; @@ -379,6 +396,12 @@ impl CodegenAlternative { &self.last_equal_ranges } + pub fn use_streaming_tools(model: &dyn LanguageModel, cx: &App) -> bool { + model.supports_streaming_tools() + && cx.has_flag::() + && AgentSettings::get_global(cx).inline_assistant_use_streaming_tools + } + pub fn start( &mut self, user_prompt: String, @@ -394,33 +417,34 @@ impl CodegenAlternative { self.edit_position = Some(self.range.start.bias_right(&self.snapshot)); - let api_key = model.api_key(cx); - let telemetry_id = model.telemetry_id(); - let provider_id = model.provider_id(); - - if cx.has_flag::() { + if Self::use_streaming_tools(model.as_ref(), cx) { let request = self.build_request(&model, user_prompt, context_task, cx)?; - let tool_use = - cx.spawn(async move |_, cx| model.stream_completion_tool(request.await, cx).await); - self.handle_tool_use(telemetry_id, provider_id.to_string(), api_key, tool_use, cx); + let completion_events = cx.spawn({ + let model = model.clone(); + async move |_, cx| model.stream_completion(request.await, cx).await + }); + self.generation = self.handle_completion(model, completion_events, cx); } else { let stream: LocalBoxFuture> = if user_prompt.trim().to_lowercase() == "delete" { async { Ok(LanguageModelTextStream::default()) }.boxed_local() } else { let request = self.build_request(&model, user_prompt, context_task, cx)?; - cx.spawn(async move |_, cx| { - Ok(model.stream_completion_text(request.await, cx).await?) + cx.spawn({ + let model = model.clone(); + async move |_, cx| { + Ok(model.stream_completion_text(request.await, cx).await?) + } }) .boxed_local() }; - self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx); + self.generation = self.handle_stream(model, stream, cx); } Ok(()) } - fn build_request_v2( + fn build_request_tools( &self, model: &Arc, user_prompt: String, @@ -456,7 +480,7 @@ impl CodegenAlternative { let system_prompt = self .builder - .generate_inline_transformation_prompt_v2( + .generate_inline_transformation_prompt_tools( language_name, buffer, range.start.0..range.end.0, @@ -466,6 +490,9 @@ impl CodegenAlternative { let temperature = AgentSettings::temperature_for_model(model, cx); let tool_input_format = model.tool_input_format(); + let tool_choice = model + .supports_tool_choice(LanguageModelToolChoice::Any) + .then_some(LanguageModelToolChoice::Any); Ok(cx.spawn(async move |_cx| { let mut messages = vec![LanguageModelRequestMessage { @@ -508,7 +535,7 @@ impl CodegenAlternative { intent: Some(CompletionIntent::InlineAssist), mode: None, tools, - tool_choice: None, + tool_choice, stop: Vec::new(), temperature, messages, @@ -524,8 +551,8 @@ impl CodegenAlternative { context_task: Shared>>, cx: &mut App, ) -> Result> { - if cx.has_flag::() { - return self.build_request_v2(model, user_prompt, context_task, cx); + if Self::use_streaming_tools(model.as_ref(), cx) { + return self.build_request_tools(model, user_prompt, context_task, cx); } let buffer = self.buffer.read(cx).snapshot(cx); @@ -598,12 +625,14 @@ impl CodegenAlternative { pub fn handle_stream( &mut self, - model_telemetry_id: String, - model_provider_id: String, - model_api_key: Option, + model: Arc, stream: impl 'static + Future>, cx: &mut Context, - ) { + ) -> Task<()> { + let anthropic_reporter = language_model::AnthropicEventReporter::new(&model, cx); + let session_id = self.session_id; + let model_telemetry_id = model.telemetry_id(); + let model_provider_id = model.provider_id().to_string(); let start_time = Instant::now(); // Make a new snapshot and re-resolve anchor in case the document was modified. @@ -641,8 +670,6 @@ impl CodegenAlternative { } } - let http_client = cx.http_client(); - let telemetry = self.telemetry.clone(); let language_name = { let multibuffer = self.buffer.read(cx); let snapshot = multibuffer.snapshot(cx); @@ -659,7 +686,8 @@ impl CodegenAlternative { let completion = Arc::new(Mutex::new(String::new())); let completion_clone = completion.clone(); - self.generation = cx.spawn(async move |codegen, cx| { + cx.notify(); + cx.spawn(async move |codegen, cx| { let stream = stream.await; let token_usage = stream @@ -674,10 +702,11 @@ impl CodegenAlternative { let model_telemetry_id = model_telemetry_id.clone(); let model_provider_id = model_provider_id.clone(); let (mut diff_tx, mut diff_rx) = mpsc::channel(1); - let executor = cx.background_executor().clone(); let message_id = message_id.clone(); - let line_based_stream_diff: Task> = - cx.background_spawn(async move { + let line_based_stream_diff: Task> = cx.background_spawn({ + let anthropic_reporter = anthropic_reporter.clone(); + let language_name = language_name.clone(); + async move { let mut response_latency = None; let request_start = Instant::now(); let diff = async { @@ -685,6 +714,7 @@ impl CodegenAlternative { stream?.stream.map_err(|error| error.into()), ); futures::pin_mut!(chunks); + let mut diff = StreamingDiff::new(selected_text.to_string()); let mut line_diff = LineDiff::default(); @@ -773,27 +803,30 @@ impl CodegenAlternative { let result = diff.await; let error_message = result.as_ref().err().map(|error| error.to_string()); - report_assistant_event( - AssistantEventData { - conversation_id: None, - message_id, - kind: AssistantKind::Inline, - phase: AssistantPhase::Response, - model: model_telemetry_id, - model_provider: model_provider_id, - response_latency, - error_message, - language_name: language_name.map(|name| name.to_proto()), - }, - telemetry, - http_client, - model_api_key, - &executor, + telemetry::event!( + "Assistant Responded", + kind = "inline", + phase = "response", + session_id = session_id.to_string(), + model = model_telemetry_id, + model_provider = model_provider_id, + language_name = language_name.as_ref().map(|n| n.to_string()), + message_id = message_id.as_deref(), + response_latency = response_latency, + error_message = error_message.as_deref(), ); + anthropic_reporter.report(language_model::AnthropicEventData { + completion_type: language_model::AnthropicCompletionType::Editor, + event: language_model::AnthropicEventType::Response, + language_name: language_name.map(|n| n.to_string()), + message_id, + }); + result?; Ok(()) - }); + } + }); while let Some((char_ops, line_ops)) = diff_rx.next().await { codegen.update(cx, |codegen, cx| { @@ -876,14 +909,23 @@ impl CodegenAlternative { cx.notify(); }) .ok(); - }); - cx.notify(); + }) } pub fn current_completion(&self) -> Option { self.completion.clone() } + #[cfg(any(test, feature = "test-support"))] + pub fn current_description(&self) -> Option { + self.description.clone() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn current_failure(&self) -> Option { + self.failure.clone() + } + pub fn selected_text(&self) -> Option<&str> { self.selected_text.as_deref() } @@ -1060,21 +1102,27 @@ impl CodegenAlternative { }) } - fn handle_tool_use( + fn handle_completion( &mut self, - _telemetry_id: String, - _provider_id: String, - _api_key: Option, - tool_use: impl 'static - + Future< - Output = Result, + model: Arc, + completion_stream: Task< + Result< + BoxStream< + 'static, + Result, + >, + LanguageModelCompletionError, + >, >, cx: &mut Context, - ) { + ) -> Task<()> { self.diff = Diff::default(); self.status = CodegenStatus::Pending; - self.generation = cx.spawn(async move |codegen, cx| { + cx.notify(); + // Leaving this in generation so that STOP equivalent events are respected even + // while we're still pre-processing the completion event + cx.spawn(async move |codegen, cx| { let finish_with_status = |status: CodegenStatus, cx: &mut AsyncApp| { let _ = codegen.update(cx, |this, cx| { this.status = status; @@ -1083,76 +1131,188 @@ impl CodegenAlternative { }); }; - let tool_use = tool_use.await; - - match tool_use { - Ok(tool_use) if tool_use.name.as_ref() == "rewrite_section" => { - // Parse the input JSON into RewriteSectionInput - match serde_json::from_value::(tool_use.input) { - Ok(input) => { - // Store the description if non-empty - let description = if !input.description.trim().is_empty() { - Some(input.description.clone()) - } else { - None - }; - - // Apply the replacement text to the buffer and compute diff - let batch_diff_task = codegen - .update(cx, |this, cx| { - this.model_explanation = description.map(Into::into); - let range = this.range.clone(); - this.apply_edits( - std::iter::once((range, input.replacement_text)), - cx, - ); - this.reapply_batch_diff(cx) - }) - .ok(); - - // Wait for the diff computation to complete - if let Some(diff_task) = batch_diff_task { - diff_task.await; - } + let mut completion_events = match completion_stream.await { + Ok(events) => events, + Err(err) => { + finish_with_status(CodegenStatus::Error(err.into()), cx); + return; + } + }; - finish_with_status(CodegenStatus::Done, cx); - return; - } - Err(e) => { - finish_with_status(CodegenStatus::Error(e.into()), cx); - return; - } + enum ToolUseOutput { + Rewrite { + text: String, + description: Option, + }, + Failure(String), + } + + enum ModelUpdate { + Description(String), + Failure(String), + } + + let chars_read_so_far = Arc::new(Mutex::new(0usize)); + let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option { + let mut chars_read_so_far = chars_read_so_far.lock(); + match tool_use.name.as_ref() { + "rewrite_section" => { + let Ok(input) = + serde_json::from_value::(tool_use.input) + else { + return None; + }; + let text = input.replacement_text[*chars_read_so_far..].to_string(); + *chars_read_so_far = input.replacement_text.len(); + Some(ToolUseOutput::Rewrite { + text, + description: None, + }) + } + "failure_message" => { + let Ok(mut input) = + serde_json::from_value::(tool_use.input) + else { + return None; + }; + Some(ToolUseOutput::Failure(std::mem::take(&mut input.message))) } + _ => None, } - Ok(tool_use) if tool_use.name.as_ref() == "failure_message" => { - // Handle failure message tool use - match serde_json::from_value::(tool_use.input) { - Ok(input) => { - let _ = codegen.update(cx, |this, _cx| { - // Store the failure message as the tool description - this.model_explanation = Some(input.message.into()); - }); - finish_with_status(CodegenStatus::Done, cx); - return; + }; + + let (message_tx, mut message_rx) = futures::channel::mpsc::unbounded::(); + + cx.spawn({ + let codegen = codegen.clone(); + async move |cx| { + while let Some(update) = message_rx.next().await { + let _ = codegen.update(cx, |this, _cx| match update { + ModelUpdate::Description(d) => this.description = Some(d), + ModelUpdate::Failure(f) => this.failure = Some(f), + }); + } + } + }) + .detach(); + + let mut message_id = None; + let mut first_text = None; + let last_token_usage = Arc::new(Mutex::new(TokenUsage::default())); + let total_text = Arc::new(Mutex::new(String::new())); + + loop { + if let Some(first_event) = completion_events.next().await { + match first_event { + Ok(LanguageModelCompletionEvent::StartMessage { message_id: id }) => { + message_id = Some(id); + } + Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => { + if let Some(output) = process_tool_use(tool_use) { + let (text, update) = match output { + ToolUseOutput::Rewrite { text, description } => { + (Some(text), description.map(ModelUpdate::Description)) + } + ToolUseOutput::Failure(message) => { + (None, Some(ModelUpdate::Failure(message))) + } + }; + if let Some(update) = update { + let _ = message_tx.unbounded_send(update); + } + first_text = text; + if first_text.is_some() { + break; + } + } + } + Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => { + *last_token_usage.lock() = token_usage; + } + Ok(LanguageModelCompletionEvent::Text(text)) => { + let mut lock = total_text.lock(); + lock.push_str(&text); + } + Ok(e) => { + log::warn!("Unexpected event: {:?}", e); + break; } Err(e) => { finish_with_status(CodegenStatus::Error(e.into()), cx); - return; + break; } } } - Ok(_tool_use) => { - // Unexpected tool. - finish_with_status(CodegenStatus::Done, cx); - return; - } - Err(e) => { - finish_with_status(CodegenStatus::Error(e.into()), cx); - return; - } } - }); - cx.notify(); + + let Some(first_text) = first_text else { + finish_with_status(CodegenStatus::Done, cx); + return; + }; + + let move_last_token_usage = last_token_usage.clone(); + + let text_stream = Box::pin(futures::stream::once(async { Ok(first_text) }).chain( + completion_events.filter_map(move |e| { + let process_tool_use = process_tool_use.clone(); + let last_token_usage = move_last_token_usage.clone(); + let total_text = total_text.clone(); + let mut message_tx = message_tx.clone(); + async move { + match e { + Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => { + let Some(output) = process_tool_use(tool_use) else { + return None; + }; + let (text, update) = match output { + ToolUseOutput::Rewrite { text, description } => { + (Some(text), description.map(ModelUpdate::Description)) + } + ToolUseOutput::Failure(message) => { + (None, Some(ModelUpdate::Failure(message))) + } + }; + if let Some(update) = update { + let _ = message_tx.send(update).await; + } + text.map(Ok) + } + Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => { + *last_token_usage.lock() = token_usage; + None + } + Ok(LanguageModelCompletionEvent::Text(text)) => { + let mut lock = total_text.lock(); + lock.push_str(&text); + None + } + Ok(LanguageModelCompletionEvent::Stop(_reason)) => None, + e => { + log::error!("UNEXPECTED EVENT {:?}", e); + None + } + } + } + }), + )); + + let language_model_text_stream = LanguageModelTextStream { + message_id: message_id, + stream: text_stream, + last_token_usage, + }; + + let Some(task) = codegen + .update(cx, move |codegen, cx| { + codegen.handle_stream(model, async { Ok(language_model_text_stream) }, cx) + }) + .ok() + else { + return; + }; + + task.await; + }) } } @@ -1316,6 +1476,7 @@ mod tests { use gpui::TestAppContext; use indoc::indoc; use language::{Buffer, Point}; + use language_model::fake_provider::FakeLanguageModel; use language_model::{LanguageModelRegistry, TokenUsage}; use languages::rust_lang; use rand::prelude::*; @@ -1346,8 +1507,8 @@ mod tests { buffer.clone(), range.clone(), true, - None, prompt_builder, + Uuid::new_v4(), cx, ) }); @@ -1408,8 +1569,8 @@ mod tests { buffer.clone(), range.clone(), true, - None, prompt_builder, + Uuid::new_v4(), cx, ) }); @@ -1472,8 +1633,8 @@ mod tests { buffer.clone(), range.clone(), true, - None, prompt_builder, + Uuid::new_v4(), cx, ) }); @@ -1536,8 +1697,8 @@ mod tests { buffer.clone(), range.clone(), true, - None, prompt_builder, + Uuid::new_v4(), cx, ) }); @@ -1588,8 +1749,8 @@ mod tests { buffer.clone(), range.clone(), false, - None, prompt_builder, + Uuid::new_v4(), cx, ) }); @@ -1678,11 +1839,10 @@ mod tests { cx: &mut TestAppContext, ) -> mpsc::UnboundedSender { let (chunks_tx, chunks_rx) = mpsc::unbounded(); + let model = Arc::new(FakeLanguageModel::default()); codegen.update(cx, |codegen, cx| { - codegen.handle_stream( - String::new(), - String::new(), - None, + codegen.generation = codegen.handle_stream( + model, future::ready(Ok(LanguageModelTextStream { message_id: None, stream: chunks_rx.map(Ok).boxed(), diff --git a/crates/agent_ui/src/evals.rs b/crates/agent_ui/src/evals.rs deleted file mode 100644 index e82d21bd1fdb02a666c61bdf4754f27e79f92fda..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/evals.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::str::FromStr; - -use crate::inline_assistant::test::run_inline_assistant_test; - -use eval_utils::{EvalOutput, NoProcessor}; -use gpui::TestAppContext; -use language_model::{LanguageModelRegistry, SelectedModel}; -use rand::{SeedableRng as _, rngs::StdRng}; - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_single_cursor_edit() { - eval_utils::eval(20, 1.0, NoProcessor, move || { - run_eval( - &EvalInput { - prompt: "Rename this variable to buffer_text".to_string(), - buffer: indoc::indoc! {" - struct EvalExampleStruct { - text: Strˇing, - prompt: String, - } - "} - .to_string(), - }, - &|_, output| { - let expected = indoc::indoc! {" - struct EvalExampleStruct { - buffer_text: String, - prompt: String, - } - "}; - if output == expected { - EvalOutput { - outcome: eval_utils::OutcomeKind::Passed, - data: "Passed!".to_string(), - metadata: (), - } - } else { - EvalOutput { - outcome: eval_utils::OutcomeKind::Failed, - data: format!("Failed to rename variable, output: {}", output), - metadata: (), - } - } - }, - ) - }); -} - -struct EvalInput { - buffer: String, - prompt: String, -} - -fn run_eval( - input: &EvalInput, - judge: &dyn Fn(&EvalInput, &str) -> eval_utils::EvalOutput<()>, -) -> eval_utils::EvalOutput<()> { - let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng()); - let mut cx = TestAppContext::build(dispatcher, None); - cx.skip_drawing(); - - let buffer_text = run_inline_assistant_test( - input.buffer.clone(), - input.prompt.clone(), - |cx| { - // Reconfigure to use a real model instead of the fake one - let model_name = std::env::var("ZED_AGENT_MODEL") - .unwrap_or("anthropic/claude-sonnet-4-latest".into()); - - let selected_model = SelectedModel::from_str(&model_name) - .expect("Invalid model format. Use 'provider/model-id'"); - - log::info!("Selected model: {selected_model:?}"); - - cx.update(|_, cx| { - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - registry.select_inline_assistant_model(Some(&selected_model), cx); - }); - }); - }, - |_cx| { - log::info!("Waiting for actual response from the LLM..."); - }, - &mut cx, - ); - - judge(input, &buffer_text) -} diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 48da85d38554da8227d76d3cbe290e29ef4fc531..6e3ab7a162bc69a5b0ec081b060b4a2ba08b09aa 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -1,8 +1,11 @@ +use language_model::AnthropicEventData; +use language_model::report_anthropic_event; use std::cmp; use std::mem; use std::ops::Range; use std::rc::Rc; use std::sync::Arc; +use uuid::Uuid; use crate::context::load_context; use crate::mention_set::MentionSet; @@ -15,7 +18,6 @@ use crate::{ use agent::HistoryStore; use agent_settings::AgentSettings; use anyhow::{Context as _, Result}; -use client::telemetry::Telemetry; use collections::{HashMap, HashSet, VecDeque, hash_map}; use editor::EditorSnapshot; use editor::MultiBufferOffset; @@ -38,15 +40,13 @@ use gpui::{ WeakEntity, Window, point, }; use language::{Buffer, Point, Selection, TransactionId}; -use language_model::{ - ConfigurationError, ConfiguredModel, LanguageModelRegistry, report_assistant_event, -}; +use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry}; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; use project::{CodeAction, DisableAiSettings, LspAction, Project, ProjectTransaction}; use prompt_store::{PromptBuilder, PromptStore}; use settings::{Settings, SettingsStore}; -use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; + use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; use text::{OffsetRangeExt, ToPoint as _}; use ui::prelude::*; @@ -54,13 +54,8 @@ use util::{RangeExt, ResultExt, maybe}; use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId}; use zed_actions::agent::OpenSettings; -pub fn init( - fs: Arc, - prompt_builder: Arc, - telemetry: Arc, - cx: &mut App, -) { - cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry)); +pub fn init(fs: Arc, prompt_builder: Arc, cx: &mut App) { + cx.set_global(InlineAssistant::new(fs, prompt_builder)); cx.observe_global::(|cx| { if DisableAiSettings::get_global(cx).disable_ai { @@ -100,7 +95,6 @@ pub struct InlineAssistant { confirmed_assists: HashMap>, prompt_history: VecDeque, prompt_builder: Arc, - telemetry: Arc, fs: Arc, _inline_assistant_completions: Option>>, } @@ -108,11 +102,7 @@ pub struct InlineAssistant { impl Global for InlineAssistant {} impl InlineAssistant { - pub fn new( - fs: Arc, - prompt_builder: Arc, - telemetry: Arc, - ) -> Self { + pub fn new(fs: Arc, prompt_builder: Arc) -> Self { Self { next_assist_id: InlineAssistId::default(), next_assist_group_id: InlineAssistGroupId::default(), @@ -122,20 +112,11 @@ impl InlineAssistant { confirmed_assists: HashMap::default(), prompt_history: VecDeque::default(), prompt_builder, - telemetry, fs, _inline_assistant_completions: None, } } - #[cfg(any(test, feature = "test-support"))] - pub fn set_completion_receiver( - &mut self, - sender: mpsc::UnboundedSender>, - ) { - self._inline_assistant_completions = Some(sender); - } - pub fn register_workspace( &mut self, workspace: &Entity, @@ -457,17 +438,25 @@ impl InlineAssistant { codegen_ranges.push(anchor_range); if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() { - self.telemetry.report_assistant_event(AssistantEventData { - conversation_id: None, - kind: AssistantKind::Inline, - phase: AssistantPhase::Invoked, - message_id: None, - model: model.model.telemetry_id(), - model_provider: model.provider.id().to_string(), - response_latency: None, - error_message: None, - language_name: buffer.language().map(|language| language.name().to_proto()), - }); + telemetry::event!( + "Assistant Invoked", + kind = "inline", + phase = "invoked", + model = model.model.telemetry_id(), + model_provider = model.provider.id().to_string(), + language_name = buffer.language().map(|language| language.name().to_proto()) + ); + + report_anthropic_event( + &model.model, + AnthropicEventData { + completion_type: language_model::AnthropicCompletionType::Editor, + event: language_model::AnthropicEventType::Invoked, + language_name: buffer.language().map(|language| language.name().to_proto()), + message_id: None, + }, + cx, + ); } } @@ -491,6 +480,7 @@ impl InlineAssistant { let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); let assist_group_id = self.next_assist_group_id.post_inc(); + let session_id = Uuid::new_v4(); let prompt_buffer = cx.new(|cx| { MultiBuffer::singleton( cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)), @@ -508,7 +498,7 @@ impl InlineAssistant { editor.read(cx).buffer().clone(), range.clone(), initial_transaction_id, - self.telemetry.clone(), + session_id, self.prompt_builder.clone(), cx, ) @@ -522,6 +512,7 @@ impl InlineAssistant { self.prompt_history.clone(), prompt_buffer.clone(), codegen.clone(), + session_id, self.fs.clone(), thread_store.clone(), prompt_store.clone(), @@ -1069,8 +1060,6 @@ impl InlineAssistant { } let active_alternative = assist.codegen.read(cx).active_alternative().clone(); - let message_id = active_alternative.read(cx).message_id.clone(); - if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() { let language_name = assist.editor.upgrade().and_then(|editor| { let multibuffer = editor.read(cx).buffer().read(cx); @@ -1079,28 +1068,49 @@ impl InlineAssistant { ranges .first() .and_then(|(buffer, _, _)| buffer.language()) - .map(|language| language.name()) + .map(|language| language.name().0.to_string()) }); - report_assistant_event( - AssistantEventData { - conversation_id: None, - kind: AssistantKind::Inline, + + let codegen = assist.codegen.read(cx); + let session_id = codegen.session_id(); + let message_id = active_alternative.read(cx).message_id.clone(); + let model_telemetry_id = model.model.telemetry_id(); + let model_provider_id = model.model.provider_id().to_string(); + + let (phase, event_type, anthropic_event_type) = if undo { + ( + "rejected", + "Assistant Response Rejected", + language_model::AnthropicEventType::Reject, + ) + } else { + ( + "accepted", + "Assistant Response Accepted", + language_model::AnthropicEventType::Accept, + ) + }; + + telemetry::event!( + event_type, + phase, + session_id = session_id.to_string(), + kind = "inline", + model = model_telemetry_id, + model_provider = model_provider_id, + language_name = language_name, + message_id = message_id.as_deref(), + ); + + report_anthropic_event( + &model.model, + language_model::AnthropicEventData { + completion_type: language_model::AnthropicCompletionType::Editor, + event: anthropic_event_type, + language_name, message_id, - phase: if undo { - AssistantPhase::Rejected - } else { - AssistantPhase::Accepted - }, - model: model.model.telemetry_id(), - model_provider: model.model.provider_id().to_string(), - response_latency: None, - error_message: None, - language_name: language_name.map(|name| name.to_proto()), }, - Some(self.telemetry.clone()), - cx.http_client(), - model.model.api_key(cx), - cx.background_executor(), + cx, ); } @@ -1455,60 +1465,8 @@ impl InlineAssistant { let old_snapshot = codegen.snapshot(cx); let old_buffer = codegen.old_buffer(cx); let deleted_row_ranges = codegen.diff(cx).deleted_row_ranges.clone(); - // let model_explanation = codegen.model_explanation(cx); editor.update(cx, |editor, cx| { - // Update tool description block - // if let Some(description) = model_explanation { - // if let Some(block_id) = decorations.model_explanation { - // editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); - // let new_block_id = editor.insert_blocks( - // [BlockProperties { - // style: BlockStyle::Flex, - // placement: BlockPlacement::Below(assist.range.end), - // height: Some(1), - // render: Arc::new({ - // let description = description.clone(); - // move |cx| { - // div() - // .w_full() - // .py_1() - // .px_2() - // .bg(cx.theme().colors().editor_background) - // .border_y_1() - // .border_color(cx.theme().status().info_border) - // .child( - // Label::new(description.clone()) - // .color(Color::Muted) - // .size(LabelSize::Small), - // ) - // .into_any_element() - // } - // }), - // priority: 0, - // }], - // None, - // cx, - // ); - // decorations.model_explanation = new_block_id.into_iter().next(); - // } - // } else if let Some(block_id) = decorations.model_explanation { - // // Hide the block if there's no description - // editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); - // let new_block_id = editor.insert_blocks( - // [BlockProperties { - // style: BlockStyle::Flex, - // placement: BlockPlacement::Below(assist.range.end), - // height: Some(0), - // render: Arc::new(|_cx| div().into_any_element()), - // priority: 0, - // }], - // None, - // cx, - // ); - // decorations.model_explanation = new_block_id.into_iter().next(); - // } - let old_blocks = mem::take(&mut decorations.removed_line_block_ids); editor.remove_blocks(old_blocks, None, cx); @@ -1627,6 +1585,27 @@ impl InlineAssistant { .map(InlineAssistTarget::Terminal) } } + + #[cfg(any(test, feature = "test-support"))] + pub fn set_completion_receiver( + &mut self, + sender: mpsc::UnboundedSender>, + ) { + self._inline_assistant_completions = Some(sender); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn get_codegen( + &mut self, + assist_id: InlineAssistId, + cx: &mut App, + ) -> Option> { + self.assists.get(&assist_id).map(|inline_assist| { + inline_assist + .codegen + .update(cx, |codegen, _cx| codegen.active_alternative().clone()) + }) + } } struct EditorInlineAssists { @@ -2048,8 +2027,10 @@ fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { } } -#[cfg(any(test, feature = "test-support"))] +#[cfg(any(test, feature = "unit-eval"))] +#[cfg_attr(not(test), allow(dead_code))] pub mod test { + use std::sync::Arc; use agent::HistoryStore; @@ -2060,7 +2041,6 @@ pub mod test { use futures::channel::mpsc; use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; use language::Buffer; - use language_model::LanguageModelRegistry; use project::Project; use prompt_store::PromptBuilder; use smol::stream::StreamExt as _; @@ -2069,13 +2049,32 @@ pub mod test { use crate::InlineAssistant; + #[derive(Debug)] + pub enum InlineAssistantOutput { + Success { + completion: Option, + description: Option, + full_buffer_text: String, + }, + Failure { + failure: String, + }, + // These fields are used for logging + #[allow(unused)] + Malformed { + completion: Option, + description: Option, + failure: Option, + }, + } + pub fn run_inline_assistant_test( base_buffer: String, prompt: String, setup: SetupF, test: TestF, cx: &mut TestAppContext, - ) -> String + ) -> InlineAssistantOutput where SetupF: FnOnce(&mut gpui::VisualTestContext), TestF: FnOnce(&mut gpui::VisualTestContext), @@ -2088,8 +2087,7 @@ pub mod test { cx.set_http_client(http); Client::production(cx) }); - let mut inline_assistant = - InlineAssistant::new(fs.clone(), prompt_builder, client.telemetry().clone()); + let mut inline_assistant = InlineAssistant::new(fs.clone(), prompt_builder); let (tx, mut completion_rx) = mpsc::unbounded(); inline_assistant.set_completion_receiver(tx); @@ -2168,39 +2166,217 @@ pub mod test { test(cx); - cx.executor() - .block_test(async { completion_rx.next().await }); + let assist_id = cx + .executor() + .block_test(async { completion_rx.next().await }) + .unwrap() + .unwrap(); - buffer.read_with(cx, |buffer, _| buffer.text()) + let (completion, description, failure) = cx.update(|_, cx| { + InlineAssistant::update_global(cx, |inline_assistant, cx| { + let codegen = inline_assistant.get_codegen(assist_id, cx).unwrap(); + + let completion = codegen.read(cx).current_completion(); + let description = codegen.read(cx).current_description(); + let failure = codegen.read(cx).current_failure(); + + (completion, description, failure) + }) + }); + + if failure.is_some() && (completion.is_some() || description.is_some()) { + InlineAssistantOutput::Malformed { + completion, + description, + failure, + } + } else if let Some(failure) = failure { + InlineAssistantOutput::Failure { failure } + } else { + InlineAssistantOutput::Success { + completion, + description, + full_buffer_text: buffer.read_with(cx, |buffer, _| buffer.text()), + } + } } +} - #[allow(unused)] - pub fn test_inline_assistant( - base_buffer: &'static str, - llm_output: &'static str, - cx: &mut TestAppContext, - ) -> String { - run_inline_assistant_test( - base_buffer.to_string(), - "Prompt doesn't matter because we're using a fake model".to_string(), - |cx| { - cx.update(|_, cx| LanguageModelRegistry::test(cx)); - }, - |cx| { - let fake_model = cx.update(|_, cx| { - LanguageModelRegistry::global(cx) - .update(cx, |registry, _| registry.fake_model()) - }); - let fake = fake_model.as_fake(); +#[cfg(any(test, feature = "unit-eval"))] +#[cfg_attr(not(test), allow(dead_code))] +pub mod evals { + use std::str::FromStr; + + use eval_utils::{EvalOutput, NoProcessor}; + use gpui::TestAppContext; + use language_model::{LanguageModelRegistry, SelectedModel}; + use rand::{SeedableRng as _, rngs::StdRng}; + + use crate::inline_assistant::test::{InlineAssistantOutput, run_inline_assistant_test}; + + #[test] + #[cfg_attr(not(feature = "unit-eval"), ignore)] + fn eval_single_cursor_edit() { + run_eval( + 20, + 1.0, + "Rename this variable to buffer_text".to_string(), + indoc::indoc! {" + struct EvalExampleStruct { + text: Strˇing, + prompt: String, + } + "} + .to_string(), + exact_buffer_match(indoc::indoc! {" + struct EvalExampleStruct { + buffer_text: String, + prompt: String, + } + "}), + ); + } - // let fake = fake_model; - fake.send_last_completion_stream_text_chunk(llm_output.to_string()); - fake.end_last_completion_stream(); + #[test] + #[cfg_attr(not(feature = "unit-eval"), ignore)] + fn eval_cant_do() { + run_eval( + 20, + 0.95, + "Rename the struct to EvalExampleStructNope", + indoc::indoc! {" + struct EvalExampleStruct { + text: Strˇing, + prompt: String, + } + "}, + uncertain_output, + ); + } - // Run again to process the model's response - cx.run_until_parked(); - }, - cx, - ) + #[test] + #[cfg_attr(not(feature = "unit-eval"), ignore)] + fn eval_unclear() { + run_eval( + 20, + 0.95, + "Make exactly the change I want you to make", + indoc::indoc! {" + struct EvalExampleStruct { + text: Strˇing, + prompt: String, + } + "}, + uncertain_output, + ); + } + + fn run_eval( + iterations: usize, + expected_pass_ratio: f32, + prompt: impl Into, + buffer: impl Into, + judge: impl Fn(InlineAssistantOutput) -> eval_utils::EvalOutput<()> + Send + Sync + 'static, + ) { + let buffer = buffer.into(); + let prompt = prompt.into(); + + eval_utils::eval(iterations, expected_pass_ratio, NoProcessor, move || { + let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng()); + let mut cx = TestAppContext::build(dispatcher, None); + cx.skip_drawing(); + + let output = run_inline_assistant_test( + buffer.clone(), + prompt.clone(), + |cx| { + // Reconfigure to use a real model instead of the fake one + let model_name = std::env::var("ZED_AGENT_MODEL") + .unwrap_or("anthropic/claude-sonnet-4-latest".into()); + + let selected_model = SelectedModel::from_str(&model_name) + .expect("Invalid model format. Use 'provider/model-id'"); + + log::info!("Selected model: {selected_model:?}"); + + cx.update(|_, cx| { + LanguageModelRegistry::global(cx).update(cx, |registry, cx| { + registry.select_inline_assistant_model(Some(&selected_model), cx); + }); + }); + }, + |_cx| { + log::info!("Waiting for actual response from the LLM..."); + }, + &mut cx, + ); + + cx.quit(); + + judge(output) + }); + } + + fn uncertain_output(output: InlineAssistantOutput) -> EvalOutput<()> { + match &output { + o @ InlineAssistantOutput::Success { + completion, + description, + .. + } => { + if description.is_some() && completion.is_none() { + EvalOutput::passed(format!( + "Assistant produced no completion, but a description:\n{}", + description.as_ref().unwrap() + )) + } else { + EvalOutput::failed(format!("Assistant produced a completion:\n{:?}", o)) + } + } + InlineAssistantOutput::Failure { + failure: error_message, + } => EvalOutput::passed(format!( + "Assistant produced a failure message: {}", + error_message + )), + o @ InlineAssistantOutput::Malformed { .. } => { + EvalOutput::failed(format!("Assistant produced a malformed response:\n{:?}", o)) + } + } + } + + fn exact_buffer_match( + correct_output: impl Into, + ) -> impl Fn(InlineAssistantOutput) -> EvalOutput<()> { + let correct_output = correct_output.into(); + move |output| match output { + InlineAssistantOutput::Success { + description, + full_buffer_text, + .. + } => { + if full_buffer_text == correct_output && description.is_none() { + EvalOutput::passed("Assistant output matches") + } else if full_buffer_text == correct_output { + EvalOutput::failed(format!( + "Assistant output produced an unescessary description description:\n{:?}", + description + )) + } else { + EvalOutput::failed(format!( + "Assistant output does not match expected output:\n{:?}\ndescription:\n{:?}", + full_buffer_text, description + )) + } + } + o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!( + "Assistant output does not match expected output: {:?}", + o + )), + o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!( + "Assistant output does not match expected output: {:?}", + o + )), + } } } diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 4856d4024c94856e8dee91c048fe6ce72e79a7b8..51e65447b2f888ab70f5942baca108134b239593 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -8,7 +8,7 @@ use editor::{ ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer, actions::{MoveDown, MoveUp}, }; -use feature_flags::{FeatureFlag, FeatureFlagAppExt}; +use feature_flags::{FeatureFlagAppExt, InlineAssistantUseToolFeatureFlag}; use fs::Fs; use gpui::{ AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, @@ -20,10 +20,10 @@ use parking_lot::Mutex; use project::Project; use prompt_store::PromptStore; use settings::Settings; +use std::cmp; use std::ops::Range; use std::rc::Rc; use std::sync::Arc; -use std::{cmp, mem}; use theme::ThemeSettings; use ui::utils::WithRemSize; use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*}; @@ -33,7 +33,7 @@ use workspace::{Toast, Workspace}; use zed_actions::agent::ToggleModelSelector; use crate::agent_model_selector::AgentModelSelector; -use crate::buffer_codegen::BufferCodegen; +use crate::buffer_codegen::{BufferCodegen, CodegenAlternative}; use crate::completion_provider::{ PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType, }; @@ -44,54 +44,15 @@ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext} actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]); -pub struct InlineAssistRatingFeatureFlag; - -impl FeatureFlag for InlineAssistRatingFeatureFlag { - const NAME: &'static str = "inline-assist-rating"; - - fn enabled_for_staff() -> bool { - false - } -} - -enum RatingState { +enum CompletionState { Pending, - GeneratedCompletion(Option), - Rated(Uuid), + Generated { completion_text: Option }, + Rated, } -impl RatingState { - fn is_pending(&self) -> bool { - matches!(self, RatingState::Pending) - } - - fn rating_id(&self) -> Option { - match self { - RatingState::Pending => None, - RatingState::GeneratedCompletion(_) => None, - RatingState::Rated(id) => Some(*id), - } - } - - fn rate(&mut self) -> (Uuid, Option) { - let id = Uuid::new_v4(); - let old_state = mem::replace(self, RatingState::Rated(id)); - let completion = match old_state { - RatingState::Pending => None, - RatingState::GeneratedCompletion(completion) => completion, - RatingState::Rated(_) => None, - }; - - (id, completion) - } - - fn reset(&mut self) { - *self = RatingState::Pending; - } - - fn generated_completion(&mut self, generated_completion: Option) { - *self = RatingState::GeneratedCompletion(generated_completion); - } +struct SessionState { + session_id: Uuid, + completion: CompletionState, } pub struct PromptEditor { @@ -109,7 +70,7 @@ pub struct PromptEditor { _codegen_subscription: Subscription, editor_subscriptions: Vec, show_rate_limit_notice: bool, - rated: RatingState, + session_state: SessionState, _phantom: std::marker::PhantomData, } @@ -140,11 +101,11 @@ impl Render for PromptEditor { let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0); let right_padding = editor_margins.right + RIGHT_PADDING; - let explanation = codegen - .active_alternative() - .read(cx) - .model_explanation - .clone(); + let active_alternative = codegen.active_alternative().read(cx); + let explanation = active_alternative + .description + .clone() + .or_else(|| active_alternative.failure.clone()); (left_gutter_width, right_padding, explanation) } @@ -178,7 +139,7 @@ impl Render for PromptEditor { if let Some(explanation) = &explanation { markdown.update(cx, |markdown, cx| { - markdown.reset(explanation.clone(), cx); + markdown.reset(SharedString::from(explanation), cx); }); } @@ -487,7 +448,7 @@ impl PromptEditor { } self.edited_since_done = true; - self.rated.reset(); + self.session_state.completion = CompletionState::Pending; cx.notify(); } EditorEvent::Blurred => { @@ -559,109 +520,179 @@ impl PromptEditor { fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { match self.codegen_status(cx) { CodegenStatus::Idle => { + self.fire_started_telemetry(cx); cx.emit(PromptEditorEvent::StartRequested); } CodegenStatus::Pending => {} CodegenStatus::Done => { if self.edited_since_done { + self.fire_started_telemetry(cx); cx.emit(PromptEditorEvent::StartRequested); } else { cx.emit(PromptEditorEvent::ConfirmRequested { execute: false }); } } CodegenStatus::Error(_) => { + self.fire_started_telemetry(cx); cx.emit(PromptEditorEvent::StartRequested); } } } - fn thumbs_up(&mut self, _: &ThumbsUpResult, _window: &mut Window, cx: &mut Context) { - if self.rated.is_pending() { - self.toast("Still generating...", None, cx); - return; - } - - if let Some(rating_id) = self.rated.rating_id() { - self.toast("Already rated this completion", Some(rating_id), cx); + fn fire_started_telemetry(&self, cx: &Context) { + let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() else { return; - } + }; - let (rating_id, completion) = self.rated.rate(); + let model_telemetry_id = model.model.telemetry_id(); + let model_provider_id = model.provider.id().to_string(); - let selected_text = match &self.mode { + let (kind, language_name) = match &self.mode { PromptEditorMode::Buffer { codegen, .. } => { - codegen.read(cx).selected_text(cx).map(|s| s.to_string()) + let codegen = codegen.read(cx); + ( + "inline", + codegen.language_name(cx).map(|name| name.to_string()), + ) } - PromptEditorMode::Terminal { .. } => None, + PromptEditorMode::Terminal { .. } => ("inline_terminal", None), }; - let model_info = self.model_selector.read(cx).active_model(cx); - let model_id = { - let Some(configured_model) = model_info else { - self.toast("No configured model", None, cx); - return; - }; + telemetry::event!( + "Assistant Started", + session_id = self.session_state.session_id.to_string(), + kind = kind, + phase = "started", + model = model_telemetry_id, + model_provider = model_provider_id, + language_name = language_name, + ); + } - configured_model.model.telemetry_id() - }; + fn thumbs_up(&mut self, _: &ThumbsUpResult, _window: &mut Window, cx: &mut Context) { + match &self.session_state.completion { + CompletionState::Pending => { + self.toast("Can't rate, still generating...", None, cx); + return; + } + CompletionState::Rated => { + self.toast( + "Already rated this completion", + Some(self.session_state.session_id), + cx, + ); + return; + } + CompletionState::Generated { completion_text } => { + let model_info = self.model_selector.read(cx).active_model(cx); + let (model_id, use_streaming_tools) = { + let Some(configured_model) = model_info else { + self.toast("No configured model", None, cx); + return; + }; + ( + configured_model.model.telemetry_id(), + CodegenAlternative::use_streaming_tools( + configured_model.model.as_ref(), + cx, + ), + ) + }; - let prompt = self.editor.read(cx).text(cx); + let selected_text = match &self.mode { + PromptEditorMode::Buffer { codegen, .. } => { + codegen.read(cx).selected_text(cx).map(|s| s.to_string()) + } + PromptEditorMode::Terminal { .. } => None, + }; - telemetry::event!( - "Inline Assistant Rated", - rating = "positive", - model = model_id, - prompt = prompt, - completion = completion, - selected_text = selected_text, - rating_id = rating_id.to_string() - ); + let prompt = self.editor.read(cx).text(cx); - cx.notify(); - } + let kind = match &self.mode { + PromptEditorMode::Buffer { .. } => "inline", + PromptEditorMode::Terminal { .. } => "inline_terminal", + }; - fn thumbs_down(&mut self, _: &ThumbsDownResult, _window: &mut Window, cx: &mut Context) { - if self.rated.is_pending() { - self.toast("Still generating...", None, cx); - return; - } - if let Some(rating_id) = self.rated.rating_id() { - self.toast("Already rated this completion", Some(rating_id), cx); - return; - } + telemetry::event!( + "Inline Assistant Rated", + rating = "positive", + session_id = self.session_state.session_id.to_string(), + kind = kind, + model = model_id, + prompt = prompt, + completion = completion_text, + selected_text = selected_text, + use_streaming_tools + ); - let (rating_id, completion) = self.rated.rate(); + self.session_state.completion = CompletionState::Rated; - let selected_text = match &self.mode { - PromptEditorMode::Buffer { codegen, .. } => { - codegen.read(cx).selected_text(cx).map(|s| s.to_string()) + cx.notify(); } - PromptEditorMode::Terminal { .. } => None, - }; + } + } - let model_info = self.model_selector.read(cx).active_model(cx); - let model_telemetry_id = { - let Some(configured_model) = model_info else { - self.toast("No configured model", None, cx); + fn thumbs_down(&mut self, _: &ThumbsDownResult, _window: &mut Window, cx: &mut Context) { + match &self.session_state.completion { + CompletionState::Pending => { + self.toast("Can't rate, still generating...", None, cx); return; - }; + } + CompletionState::Rated => { + self.toast( + "Already rated this completion", + Some(self.session_state.session_id), + cx, + ); + return; + } + CompletionState::Generated { completion_text } => { + let model_info = self.model_selector.read(cx).active_model(cx); + let (model_telemetry_id, use_streaming_tools) = { + let Some(configured_model) = model_info else { + self.toast("No configured model", None, cx); + return; + }; + ( + configured_model.model.telemetry_id(), + CodegenAlternative::use_streaming_tools( + configured_model.model.as_ref(), + cx, + ), + ) + }; - configured_model.model.telemetry_id() - }; + let selected_text = match &self.mode { + PromptEditorMode::Buffer { codegen, .. } => { + codegen.read(cx).selected_text(cx).map(|s| s.to_string()) + } + PromptEditorMode::Terminal { .. } => None, + }; - let prompt = self.editor.read(cx).text(cx); + let prompt = self.editor.read(cx).text(cx); - telemetry::event!( - "Inline Assistant Rated", - rating = "negative", - model = model_telemetry_id, - prompt = prompt, - completion = completion, - selected_text = selected_text, - rating_id = rating_id.to_string() - ); + let kind = match &self.mode { + PromptEditorMode::Buffer { .. } => "inline", + PromptEditorMode::Terminal { .. } => "inline_terminal", + }; + + telemetry::event!( + "Inline Assistant Rated", + rating = "negative", + session_id = self.session_state.session_id.to_string(), + kind = kind, + model = model_telemetry_id, + prompt = prompt, + completion = completion_text, + selected_text = selected_text, + use_streaming_tools + ); + + self.session_state.completion = CompletionState::Rated; - cx.notify(); + cx.notify(); + } + } } fn toast(&mut self, msg: &str, uuid: Option, cx: &mut Context<'_, PromptEditor>) { @@ -795,8 +826,8 @@ impl PromptEditor { .into_any_element(), ] } else { - let show_rating_buttons = cx.has_flag::(); - let rated = self.rated.rating_id().is_some(); + let show_rating_buttons = cx.has_flag::(); + let rated = matches!(self.session_state.completion, CompletionState::Rated); let accept = IconButton::new("accept", IconName::Check) .icon_color(Color::Info) @@ -1120,6 +1151,7 @@ impl PromptEditor { prompt_history: VecDeque, prompt_buffer: Entity, codegen: Entity, + session_id: Uuid, fs: Arc, history_store: Entity, prompt_store: Option>, @@ -1190,7 +1222,10 @@ impl PromptEditor { editor_subscriptions: Vec::new(), show_rate_limit_notice: false, mode, - rated: RatingState::Pending, + session_state: SessionState { + session_id, + completion: CompletionState::Pending, + }, _phantom: Default::default(), }; @@ -1210,13 +1245,15 @@ impl PromptEditor { .update(cx, |editor, _| editor.set_read_only(false)); } CodegenStatus::Pending => { - self.rated.reset(); + self.session_state.completion = CompletionState::Pending; self.editor .update(cx, |editor, _| editor.set_read_only(true)); } CodegenStatus::Done => { let completion = codegen.read(cx).active_completion(cx); - self.rated.generated_completion(completion); + self.session_state.completion = CompletionState::Generated { + completion_text: completion, + }; self.edited_since_done = false; self.editor .update(cx, |editor, _| editor.set_read_only(false)); @@ -1272,6 +1309,7 @@ impl PromptEditor { prompt_history: VecDeque, prompt_buffer: Entity, codegen: Entity, + session_id: Uuid, fs: Arc, history_store: Entity, prompt_store: Option>, @@ -1337,7 +1375,10 @@ impl PromptEditor { editor_subscriptions: Vec::new(), mode, show_rate_limit_notice: false, - rated: RatingState::Pending, + session_state: SessionState { + session_id, + completion: CompletionState::Pending, + }, _phantom: Default::default(), }; this.count_lines(cx); @@ -1377,13 +1418,14 @@ impl PromptEditor { .update(cx, |editor, _| editor.set_read_only(false)); } CodegenStatus::Pending => { - self.rated = RatingState::Pending; + self.session_state.completion = CompletionState::Pending; self.editor .update(cx, |editor, _| editor.set_read_only(true)); } CodegenStatus::Done | CodegenStatus::Error(_) => { - self.rated - .generated_completion(codegen.read(cx).completion()); + self.session_state.completion = CompletionState::Generated { + completion_text: codegen.read(cx).completion(), + }; self.edited_since_done = false; self.editor .update(cx, |editor, _| editor.set_read_only(false)); diff --git a/crates/agent_ui/src/terminal_codegen.rs b/crates/agent_ui/src/terminal_codegen.rs index cc99471f7f3037cb94ff23979036bd6c2026e2f0..e93d3d3991378ddb4156b264be1f0a5ab4d4faac 100644 --- a/crates/agent_ui/src/terminal_codegen.rs +++ b/crates/agent_ui/src/terminal_codegen.rs @@ -1,37 +1,38 @@ use crate::inline_prompt_editor::CodegenStatus; -use client::telemetry::Telemetry; use futures::{SinkExt, StreamExt, channel::mpsc}; use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task}; -use language_model::{ - ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, report_assistant_event, -}; -use std::{sync::Arc, time::Instant}; -use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; +use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelRequest}; +use std::time::Instant; use terminal::Terminal; +use uuid::Uuid; pub struct TerminalCodegen { pub status: CodegenStatus, - pub telemetry: Option>, terminal: Entity, generation: Task<()>, pub message_id: Option, transaction: Option, + session_id: Uuid, } impl EventEmitter for TerminalCodegen {} impl TerminalCodegen { - pub fn new(terminal: Entity, telemetry: Option>) -> Self { + pub fn new(terminal: Entity, session_id: Uuid) -> Self { Self { terminal, - telemetry, status: CodegenStatus::Idle, generation: Task::ready(()), message_id: None, transaction: None, + session_id, } } + pub fn session_id(&self) -> Uuid { + self.session_id + } + pub fn start(&mut self, prompt_task: Task, cx: &mut Context) { let Some(ConfiguredModel { model, .. }) = LanguageModelRegistry::read_global(cx).inline_assistant_model() @@ -39,15 +40,15 @@ impl TerminalCodegen { return; }; - let model_api_key = model.api_key(cx); - let http_client = cx.http_client(); - let telemetry = self.telemetry.clone(); + let anthropic_reporter = language_model::AnthropicEventReporter::new(&model, cx); + let session_id = self.session_id; + let model_telemetry_id = model.telemetry_id(); + let model_provider_id = model.provider_id().to_string(); + self.status = CodegenStatus::Pending; self.transaction = Some(TerminalTransaction::start(self.terminal.clone())); self.generation = cx.spawn(async move |this, cx| { let prompt = prompt_task.await; - let model_telemetry_id = model.telemetry_id(); - let model_provider_id = model.provider_id(); let response = model.stream_completion_text(prompt, cx).await; let generate = async { let message_id = response @@ -59,7 +60,7 @@ impl TerminalCodegen { let task = cx.background_spawn({ let message_id = message_id.clone(); - let executor = cx.background_executor().clone(); + let anthropic_reporter = anthropic_reporter.clone(); async move { let mut response_latency = None; let request_start = Instant::now(); @@ -79,24 +80,27 @@ impl TerminalCodegen { let result = task.await; let error_message = result.as_ref().err().map(|error| error.to_string()); - report_assistant_event( - AssistantEventData { - conversation_id: None, - kind: AssistantKind::InlineTerminal, - message_id, - phase: AssistantPhase::Response, - model: model_telemetry_id, - model_provider: model_provider_id.to_string(), - response_latency, - error_message, - language_name: None, - }, - telemetry, - http_client, - model_api_key, - &executor, + + telemetry::event!( + "Assistant Responded", + session_id = session_id.to_string(), + kind = "inline_terminal", + phase = "response", + model = model_telemetry_id, + model_provider = model_provider_id, + language_name = Option::<&str>::None, + message_id = message_id, + response_latency = response_latency, + error_message = error_message, ); + anthropic_reporter.report(language_model::AnthropicEventData { + completion_type: language_model::AnthropicCompletionType::Terminal, + event: language_model::AnthropicEventType::Response, + language_name: None, + message_id, + }); + result?; anyhow::Ok(()) } diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs index 43ea697bece318699f350259a0e2e38d1a4f4d8d..84a74242b80d0b2f8479b3c6dbca1c7d0bb2cb6d 100644 --- a/crates/agent_ui/src/terminal_inline_assistant.rs +++ b/crates/agent_ui/src/terminal_inline_assistant.rs @@ -8,7 +8,7 @@ use crate::{ use agent::HistoryStore; use agent_settings::AgentSettings; use anyhow::{Context as _, Result}; -use client::telemetry::Telemetry; + use cloud_llm_client::CompletionIntent; use collections::{HashMap, VecDeque}; use editor::{MultiBuffer, actions::SelectAll}; @@ -17,24 +17,19 @@ use gpui::{App, Entity, Focusable, Global, Subscription, Task, UpdateGlobal, Wea use language::Buffer; use language_model::{ ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, - Role, report_assistant_event, + Role, report_anthropic_event, }; use project::Project; use prompt_store::{PromptBuilder, PromptStore}; use std::sync::Arc; -use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; use terminal_view::TerminalView; use ui::prelude::*; use util::ResultExt; +use uuid::Uuid; use workspace::{Toast, Workspace, notifications::NotificationId}; -pub fn init( - fs: Arc, - prompt_builder: Arc, - telemetry: Arc, - cx: &mut App, -) { - cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry)); +pub fn init(fs: Arc, prompt_builder: Arc, cx: &mut App) { + cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder)); } const DEFAULT_CONTEXT_LINES: usize = 50; @@ -44,7 +39,6 @@ pub struct TerminalInlineAssistant { next_assist_id: TerminalInlineAssistId, assists: HashMap, prompt_history: VecDeque, - telemetry: Option>, fs: Arc, prompt_builder: Arc, } @@ -52,16 +46,11 @@ pub struct TerminalInlineAssistant { impl Global for TerminalInlineAssistant {} impl TerminalInlineAssistant { - pub fn new( - fs: Arc, - prompt_builder: Arc, - telemetry: Arc, - ) -> Self { + pub fn new(fs: Arc, prompt_builder: Arc) -> Self { Self { next_assist_id: TerminalInlineAssistId::default(), assists: HashMap::default(), prompt_history: VecDeque::default(), - telemetry: Some(telemetry), fs, prompt_builder, } @@ -80,13 +69,14 @@ impl TerminalInlineAssistant { ) { let terminal = terminal_view.read(cx).terminal().clone(); let assist_id = self.next_assist_id.post_inc(); + let session_id = Uuid::new_v4(); let prompt_buffer = cx.new(|cx| { MultiBuffer::singleton( cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)), cx, ) }); - let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone())); + let codegen = cx.new(|_| TerminalCodegen::new(terminal, session_id)); let prompt_editor = cx.new(|cx| { PromptEditor::new_terminal( @@ -94,6 +84,7 @@ impl TerminalInlineAssistant { self.prompt_history.clone(), prompt_buffer.clone(), codegen, + session_id, self.fs.clone(), thread_store.clone(), prompt_store.clone(), @@ -309,27 +300,45 @@ impl TerminalInlineAssistant { LanguageModelRegistry::read_global(cx).inline_assistant_model() { let codegen = assist.codegen.read(cx); - let executor = cx.background_executor().clone(); - report_assistant_event( - AssistantEventData { - conversation_id: None, - kind: AssistantKind::InlineTerminal, - message_id: codegen.message_id.clone(), - phase: if undo { - AssistantPhase::Rejected - } else { - AssistantPhase::Accepted - }, - model: model.telemetry_id(), - model_provider: model.provider_id().to_string(), - response_latency: None, - error_message: None, + let session_id = codegen.session_id(); + let message_id = codegen.message_id.clone(); + let model_telemetry_id = model.telemetry_id(); + let model_provider_id = model.provider_id().to_string(); + + let (phase, event_type, anthropic_event_type) = if undo { + ( + "rejected", + "Assistant Response Rejected", + language_model::AnthropicEventType::Reject, + ) + } else { + ( + "accepted", + "Assistant Response Accepted", + language_model::AnthropicEventType::Accept, + ) + }; + + // Fire Zed telemetry + telemetry::event!( + event_type, + kind = "inline_terminal", + phase = phase, + model = model_telemetry_id, + model_provider = model_provider_id, + message_id = message_id, + session_id = session_id, + ); + + report_anthropic_event( + &model, + language_model::AnthropicEventData { + completion_type: language_model::AnthropicCompletionType::Terminal, + event: anthropic_event_type, language_name: None, + message_id, }, - codegen.telemetry.clone(), - cx.http_client(), - model.api_key(cx), - &executor, + cx, ); } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 340a3eada3c1c102351ce05bac483f8f6272d925..f87ca845cab0fe562968ae7893b63a278c4dfdd8 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -3333,7 +3333,6 @@ mod tests { let mut text_thread = TextThread::local( registry, None, - None, prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), cx, diff --git a/crates/agent_ui_v2/Cargo.toml b/crates/agent_ui_v2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..f24ef47471cdcfe0910cf36c5e220c5276d5f6ae --- /dev/null +++ b/crates/agent_ui_v2/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "agent_ui_v2" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/agent_ui_v2.rs" +doctest = false + +[dependencies] +agent.workspace = true +agent_servers.workspace = true +agent_settings.workspace = true +agent_ui.workspace = true +anyhow.workspace = true +assistant_text_thread.workspace = true +chrono.workspace = true +db.workspace = true +editor.workspace = true +feature_flags.workspace = true +fs.workspace = true +fuzzy.workspace = true +gpui.workspace = true +menu.workspace = true +project.workspace = true +prompt_store.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +text.workspace = true +time.workspace = true +time_format.workspace = true +ui.workspace = true +util.workspace = true +workspace.workspace = true diff --git a/crates/agent_ui_v2/LICENSE-GPL b/crates/agent_ui_v2/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..e0f9dbd5d63fef1630c297edc4ceba4790be6f02 --- /dev/null +++ b/crates/agent_ui_v2/LICENSE-GPL @@ -0,0 +1 @@ +LICENSE-GPL \ No newline at end of file diff --git a/crates/agent_ui_v2/src/agent_thread_pane.rs b/crates/agent_ui_v2/src/agent_thread_pane.rs new file mode 100644 index 0000000000000000000000000000000000000000..72886f87eca38c630ec29b9b410930f1d3936b50 --- /dev/null +++ b/crates/agent_ui_v2/src/agent_thread_pane.rs @@ -0,0 +1,287 @@ +use agent::{HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer}; +use agent_servers::AgentServer; +use agent_settings::AgentSettings; +use agent_ui::acp::AcpThreadView; +use fs::Fs; +use gpui::{ + Entity, EventEmitter, Focusable, Pixels, SharedString, Subscription, WeakEntity, prelude::*, +}; +use project::Project; +use prompt_store::PromptStore; +use serde::{Deserialize, Serialize}; +use settings::DockSide; +use settings::Settings as _; +use std::rc::Rc; +use std::sync::Arc; +use ui::{Tab, Tooltip, prelude::*}; +use workspace::{ + Workspace, + dock::{ClosePane, MinimizePane, UtilityPane, UtilityPanePosition}, + utility_pane::UtilityPaneSlot, +}; + +pub const DEFAULT_UTILITY_PANE_WIDTH: Pixels = gpui::px(400.0); + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum SerializedHistoryEntryId { + AcpThread(String), + TextThread(String), +} + +impl From for SerializedHistoryEntryId { + fn from(id: HistoryEntryId) -> Self { + match id { + HistoryEntryId::AcpThread(session_id) => { + SerializedHistoryEntryId::AcpThread(session_id.0.to_string()) + } + HistoryEntryId::TextThread(path) => { + SerializedHistoryEntryId::TextThread(path.to_string_lossy().to_string()) + } + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SerializedAgentThreadPane { + pub expanded: bool, + pub width: Option, + pub thread_id: Option, +} + +pub enum AgentsUtilityPaneEvent { + StateChanged, +} + +impl EventEmitter for AgentThreadPane {} +impl EventEmitter for AgentThreadPane {} +impl EventEmitter for AgentThreadPane {} + +struct ActiveThreadView { + view: Entity, + thread_id: HistoryEntryId, + _notify: Subscription, +} + +pub struct AgentThreadPane { + focus_handle: gpui::FocusHandle, + expanded: bool, + width: Option, + thread_view: Option, + workspace: WeakEntity, +} + +impl AgentThreadPane { + pub fn new(workspace: WeakEntity, cx: &mut ui::Context) -> Self { + let focus_handle = cx.focus_handle(); + Self { + focus_handle, + expanded: false, + width: None, + thread_view: None, + workspace, + } + } + + pub fn thread_id(&self) -> Option { + self.thread_view.as_ref().map(|tv| tv.thread_id.clone()) + } + + pub fn serialize(&self) -> SerializedAgentThreadPane { + SerializedAgentThreadPane { + expanded: self.expanded, + width: self.width, + thread_id: self.thread_id().map(SerializedHistoryEntryId::from), + } + } + + pub fn open_thread( + &mut self, + entry: HistoryEntry, + fs: Arc, + workspace: WeakEntity, + project: Entity, + history_store: Entity, + prompt_store: Option>, + window: &mut Window, + cx: &mut Context, + ) { + let thread_id = entry.id(); + + let resume_thread = match &entry { + HistoryEntry::AcpThread(thread) => Some(thread.clone()), + HistoryEntry::TextThread(_) => None, + }; + + let agent: Rc = Rc::new(NativeAgentServer::new(fs, history_store.clone())); + + let thread_view = cx.new(|cx| { + AcpThreadView::new( + agent, + resume_thread, + None, + workspace, + project, + history_store, + prompt_store, + true, + window, + cx, + ) + }); + + let notify = cx.observe(&thread_view, |_, _, cx| { + cx.notify(); + }); + + self.thread_view = Some(ActiveThreadView { + view: thread_view, + thread_id, + _notify: notify, + }); + + cx.notify(); + } + + fn title(&self, cx: &App) -> SharedString { + if let Some(active_thread_view) = &self.thread_view { + let thread_view = active_thread_view.view.read(cx); + if let Some(thread) = thread_view.thread() { + let title = thread.read(cx).title(); + if !title.is_empty() { + return title; + } + } + thread_view.title(cx) + } else { + "Thread".into() + } + } + + fn render_header(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let position = self.position(window, cx); + let slot = match position { + UtilityPanePosition::Left => UtilityPaneSlot::Left, + UtilityPanePosition::Right => UtilityPaneSlot::Right, + }; + + let workspace = self.workspace.clone(); + let toggle_icon = self.toggle_icon(cx); + let title = self.title(cx); + + let pane_toggle_button = |workspace: WeakEntity| { + IconButton::new("toggle_utility_pane", toggle_icon) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Toggle Agent Pane")) + .on_click(move |_, window, cx| { + workspace + .update(cx, |workspace, cx| { + workspace.toggle_utility_pane(slot, window, cx) + }) + .ok(); + }) + }; + + h_flex() + .id("utility-pane-header") + .w_full() + .h(Tab::container_height(cx)) + .px_1p5() + .gap(DynamicSpacing::Base06.rems(cx)) + .when(slot == UtilityPaneSlot::Right, |this| { + this.flex_row_reverse() + }) + .flex_none() + .border_b_1() + .border_color(cx.theme().colors().border) + .child(pane_toggle_button(workspace)) + .child( + h_flex() + .size_full() + .min_w_0() + .gap_1() + .map(|this| { + if slot == UtilityPaneSlot::Right { + this.flex_row_reverse().justify_start() + } else { + this.justify_between() + } + }) + .child(Label::new(title).truncate()) + .child( + IconButton::new("close_btn", IconName::Close) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Close Agent Pane")) + .on_click(cx.listener(|this, _: &gpui::ClickEvent, _window, cx| { + cx.emit(ClosePane); + this.thread_view = None; + cx.notify() + })), + ), + ) + } +} + +impl Focusable for AgentThreadPane { + fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle { + if let Some(thread_view) = &self.thread_view { + thread_view.view.focus_handle(cx) + } else { + self.focus_handle.clone() + } + } +} + +impl UtilityPane for AgentThreadPane { + fn position(&self, _window: &Window, cx: &App) -> UtilityPanePosition { + match AgentSettings::get_global(cx).agents_panel_dock { + DockSide::Left => UtilityPanePosition::Left, + DockSide::Right => UtilityPanePosition::Right, + } + } + + fn toggle_icon(&self, _cx: &App) -> IconName { + IconName::Thread + } + + fn expanded(&self, _cx: &App) -> bool { + self.expanded + } + + fn set_expanded(&mut self, expanded: bool, cx: &mut Context) { + self.expanded = expanded; + cx.emit(AgentsUtilityPaneEvent::StateChanged); + cx.notify(); + } + + fn width(&self, _cx: &App) -> Pixels { + self.width.unwrap_or(DEFAULT_UTILITY_PANE_WIDTH) + } + + fn set_width(&mut self, width: Option, cx: &mut Context) { + self.width = width; + cx.emit(AgentsUtilityPaneEvent::StateChanged); + cx.notify(); + } +} + +impl Render for AgentThreadPane { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let content = if let Some(thread_view) = &self.thread_view { + div().size_full().child(thread_view.view.clone()) + } else { + div() + .size_full() + .flex() + .items_center() + .justify_center() + .child(Label::new("Select a thread to view details").size(LabelSize::Default)) + }; + + div() + .size_full() + .flex() + .flex_col() + .child(self.render_header(window, cx)) + .child(content) + } +} diff --git a/crates/agent_ui_v2/src/agent_ui_v2.rs b/crates/agent_ui_v2/src/agent_ui_v2.rs new file mode 100644 index 0000000000000000000000000000000000000000..92a4144e304e9afbdcdde54623a3bbf3c65b8746 --- /dev/null +++ b/crates/agent_ui_v2/src/agent_ui_v2.rs @@ -0,0 +1,4 @@ +mod agent_thread_pane; +mod thread_history; + +pub mod agents_panel; diff --git a/crates/agent_ui_v2/src/agents_panel.rs b/crates/agent_ui_v2/src/agents_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..a7afdddda43514ade40b7fd9dfd8bcd8ace33dc7 --- /dev/null +++ b/crates/agent_ui_v2/src/agents_panel.rs @@ -0,0 +1,438 @@ +use agent::{HistoryEntry, HistoryEntryId, HistoryStore}; +use agent_settings::AgentSettings; +use anyhow::Result; +use assistant_text_thread::TextThreadStore; +use db::kvp::KEY_VALUE_STORE; +use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt}; +use fs::Fs; +use gpui::{ + Action, AsyncWindowContext, Entity, EventEmitter, Focusable, Pixels, Subscription, Task, + WeakEntity, actions, prelude::*, +}; +use project::Project; +use prompt_store::{PromptBuilder, PromptStore}; +use serde::{Deserialize, Serialize}; +use settings::{Settings as _, update_settings_file}; +use std::sync::Arc; +use ui::{App, Context, IconName, IntoElement, ParentElement, Render, Styled, Window}; +use util::ResultExt; +use workspace::{ + Panel, Workspace, + dock::{ClosePane, DockPosition, PanelEvent, UtilityPane}, + utility_pane::{UtilityPaneSlot, utility_slot_for_dock_position}, +}; + +use crate::agent_thread_pane::{ + AgentThreadPane, AgentsUtilityPaneEvent, SerializedAgentThreadPane, SerializedHistoryEntryId, +}; +use crate::thread_history::{AcpThreadHistory, ThreadHistoryEvent}; + +const AGENTS_PANEL_KEY: &str = "agents_panel"; + +#[derive(Serialize, Deserialize, Debug)] +struct SerializedAgentsPanel { + width: Option, + pane: Option, +} + +actions!( + agents, + [ + /// Toggle the visibility of the agents panel. + ToggleAgentsPanel + ] +); + +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _, _| { + workspace.register_action(|workspace, _: &ToggleAgentsPanel, window, cx| { + workspace.toggle_panel_focus::(window, cx); + }); + }) + .detach(); +} + +pub struct AgentsPanel { + focus_handle: gpui::FocusHandle, + workspace: WeakEntity, + project: Entity, + agent_thread_pane: Option>, + history: Entity, + history_store: Entity, + prompt_store: Option>, + fs: Arc, + width: Option, + pending_serialization: Task>, + _subscriptions: Vec, +} + +impl AgentsPanel { + pub fn load( + workspace: WeakEntity, + cx: AsyncWindowContext, + ) -> Task, anyhow::Error>> { + cx.spawn(async move |cx| { + let serialized_panel = cx + .background_spawn(async move { + KEY_VALUE_STORE + .read_kvp(AGENTS_PANEL_KEY) + .ok() + .flatten() + .and_then(|panel| { + serde_json::from_str::(&panel).ok() + }) + }) + .await; + + let (fs, project, prompt_builder) = workspace.update(cx, |workspace, cx| { + let fs = workspace.app_state().fs.clone(); + let project = workspace.project().clone(); + let prompt_builder = PromptBuilder::load(fs.clone(), false, cx); + (fs, project, prompt_builder) + })?; + + let text_thread_store = workspace + .update(cx, |_, cx| { + TextThreadStore::new( + project.clone(), + prompt_builder.clone(), + Default::default(), + cx, + ) + })? + .await?; + + let prompt_store = workspace + .update(cx, |_, cx| PromptStore::global(cx))? + .await + .log_err(); + + workspace.update_in(cx, |_, window, cx| { + cx.new(|cx| { + let mut panel = Self::new( + workspace.clone(), + fs, + project, + prompt_store, + text_thread_store, + window, + cx, + ); + if let Some(serialized_panel) = serialized_panel { + panel.width = serialized_panel.width; + if let Some(serialized_pane) = serialized_panel.pane { + panel.restore_utility_pane(serialized_pane, window, cx); + } + } + panel + }) + }) + }) + } + + fn new( + workspace: WeakEntity, + fs: Arc, + project: Entity, + prompt_store: Option>, + text_thread_store: Entity, + window: &mut Window, + cx: &mut ui::Context, + ) -> Self { + let focus_handle = cx.focus_handle(); + + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); + let history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx)); + + let this = cx.weak_entity(); + let subscriptions = vec![ + cx.subscribe_in(&history, window, Self::handle_history_event), + cx.on_flags_ready(move |_, cx| { + this.update(cx, |_, cx| { + cx.notify(); + }) + .ok(); + }), + ]; + + Self { + focus_handle, + workspace, + project, + agent_thread_pane: None, + history, + history_store, + prompt_store, + fs, + width: None, + pending_serialization: Task::ready(None), + _subscriptions: subscriptions, + } + } + + fn restore_utility_pane( + &mut self, + serialized_pane: SerializedAgentThreadPane, + window: &mut Window, + cx: &mut Context, + ) { + let Some(thread_id) = &serialized_pane.thread_id else { + return; + }; + + let entry = self + .history_store + .read(cx) + .entries() + .find(|e| match (&e.id(), thread_id) { + ( + HistoryEntryId::AcpThread(session_id), + SerializedHistoryEntryId::AcpThread(id), + ) => session_id.to_string() == *id, + (HistoryEntryId::TextThread(path), SerializedHistoryEntryId::TextThread(id)) => { + path.to_string_lossy() == *id + } + _ => false, + }); + + if let Some(entry) = entry { + self.open_thread( + entry, + serialized_pane.expanded, + serialized_pane.width, + window, + cx, + ); + } + } + + fn handle_utility_pane_event( + &mut self, + _utility_pane: Entity, + event: &AgentsUtilityPaneEvent, + cx: &mut Context, + ) { + match event { + AgentsUtilityPaneEvent::StateChanged => { + self.serialize(cx); + cx.notify(); + } + } + } + + fn handle_close_pane_event( + &mut self, + _utility_pane: Entity, + _event: &ClosePane, + cx: &mut Context, + ) { + self.agent_thread_pane = None; + self.serialize(cx); + cx.notify(); + } + + fn handle_history_event( + &mut self, + _history: &Entity, + event: &ThreadHistoryEvent, + window: &mut Window, + cx: &mut Context, + ) { + match event { + ThreadHistoryEvent::Open(entry) => { + self.open_thread(entry.clone(), true, None, window, cx); + } + } + } + + fn open_thread( + &mut self, + entry: HistoryEntry, + expanded: bool, + width: Option, + window: &mut Window, + cx: &mut Context, + ) { + let entry_id = entry.id(); + + if let Some(existing_pane) = &self.agent_thread_pane { + if existing_pane.read(cx).thread_id() == Some(entry_id) { + existing_pane.update(cx, |pane, cx| { + pane.set_expanded(true, cx); + }); + return; + } + } + + let fs = self.fs.clone(); + let workspace = self.workspace.clone(); + let project = self.project.clone(); + let history_store = self.history_store.clone(); + let prompt_store = self.prompt_store.clone(); + + let agent_thread_pane = cx.new(|cx| { + let mut pane = AgentThreadPane::new(workspace.clone(), cx); + pane.open_thread( + entry, + fs, + workspace.clone(), + project, + history_store, + prompt_store, + window, + cx, + ); + if let Some(width) = width { + pane.set_width(Some(width), cx); + } + pane.set_expanded(expanded, cx); + pane + }); + + let state_subscription = cx.subscribe(&agent_thread_pane, Self::handle_utility_pane_event); + let close_subscription = cx.subscribe(&agent_thread_pane, Self::handle_close_pane_event); + + self._subscriptions.push(state_subscription); + self._subscriptions.push(close_subscription); + + let slot = self.utility_slot(window, cx); + let panel_id = cx.entity_id(); + + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.register_utility_pane(slot, panel_id, agent_thread_pane.clone(), cx); + }); + } + + self.agent_thread_pane = Some(agent_thread_pane); + self.serialize(cx); + cx.notify(); + } + + fn utility_slot(&self, window: &Window, cx: &App) -> UtilityPaneSlot { + let position = self.position(window, cx); + utility_slot_for_dock_position(position) + } + + fn re_register_utility_pane(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(pane) = &self.agent_thread_pane { + let slot = self.utility_slot(window, cx); + let panel_id = cx.entity_id(); + let pane = pane.clone(); + + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.register_utility_pane(slot, panel_id, pane, cx); + }); + } + } + } + + fn serialize(&mut self, cx: &mut Context) { + let width = self.width; + let pane = self + .agent_thread_pane + .as_ref() + .map(|pane| pane.read(cx).serialize()); + + self.pending_serialization = cx.background_spawn(async move { + KEY_VALUE_STORE + .write_kvp( + AGENTS_PANEL_KEY.into(), + serde_json::to_string(&SerializedAgentsPanel { width, pane }).unwrap(), + ) + .await + .log_err() + }); + } +} + +impl EventEmitter for AgentsPanel {} + +impl Focusable for AgentsPanel { + fn focus_handle(&self, _cx: &ui::App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Panel for AgentsPanel { + fn persistent_name() -> &'static str { + "AgentsPanel" + } + + fn panel_key() -> &'static str { + AGENTS_PANEL_KEY + } + + fn position(&self, _window: &Window, cx: &App) -> DockPosition { + match AgentSettings::get_global(cx).agents_panel_dock { + settings::DockSide::Left => DockPosition::Left, + settings::DockSide::Right => DockPosition::Right, + } + } + + fn position_is_valid(&self, position: DockPosition) -> bool { + position != DockPosition::Bottom + } + + fn set_position( + &mut self, + position: DockPosition, + window: &mut Window, + cx: &mut Context, + ) { + update_settings_file(self.fs.clone(), cx, move |settings, _| { + settings.agent.get_or_insert_default().agents_panel_dock = Some(match position { + DockPosition::Left => settings::DockSide::Left, + DockPosition::Bottom => settings::DockSide::Right, + DockPosition::Right => settings::DockSide::Left, + }); + }); + self.re_register_utility_pane(window, cx); + } + + fn size(&self, window: &Window, cx: &App) -> Pixels { + let settings = AgentSettings::get_global(cx); + match self.position(window, cx) { + DockPosition::Left | DockPosition::Right => { + self.width.unwrap_or(settings.default_width) + } + DockPosition::Bottom => self.width.unwrap_or(settings.default_height), + } + } + + fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { + match self.position(window, cx) { + DockPosition::Left | DockPosition::Right => self.width = size, + DockPosition::Bottom => {} + } + self.serialize(cx); + cx.notify(); + } + + fn icon(&self, _window: &Window, cx: &App) -> Option { + (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAgentTwo) + } + + fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { + Some("Agents Panel") + } + + fn toggle_action(&self) -> Box { + Box::new(ToggleAgentsPanel) + } + + fn activation_priority(&self) -> u32 { + 4 + } + + fn enabled(&self, cx: &App) -> bool { + AgentSettings::get_global(cx).enabled(cx) && cx.has_flag::() + } +} + +impl Render for AgentsPanel { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + gpui::div().size_full().child(self.history.clone()) + } +} diff --git a/crates/agent_ui_v2/src/thread_history.rs b/crates/agent_ui_v2/src/thread_history.rs new file mode 100644 index 0000000000000000000000000000000000000000..8f6626814902a9489536439e90041437a527e151 --- /dev/null +++ b/crates/agent_ui_v2/src/thread_history.rs @@ -0,0 +1,735 @@ +use agent::{HistoryEntry, HistoryStore}; +use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; +use editor::{Editor, EditorEvent}; +use fuzzy::StringMatchCandidate; +use gpui::{ + App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task, + UniformListScrollHandle, Window, actions, uniform_list, +}; +use std::{fmt::Display, ops::Range}; +use text::Bias; +use time::{OffsetDateTime, UtcOffset}; +use ui::{ + HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar, + prelude::*, +}; + +actions!( + agents, + [ + /// Removes all thread history. + RemoveHistory, + /// Removes the currently selected thread. + RemoveSelectedThread, + ] +); + +pub struct AcpThreadHistory { + pub(crate) history_store: Entity, + scroll_handle: UniformListScrollHandle, + selected_index: usize, + hovered_index: Option, + search_editor: Entity, + search_query: SharedString, + visible_items: Vec, + local_timezone: UtcOffset, + confirming_delete_history: bool, + _update_task: Task<()>, + _subscriptions: Vec, +} + +enum ListItemType { + BucketSeparator(TimeBucket), + Entry { + entry: HistoryEntry, + format: EntryTimeFormat, + }, + SearchResult { + entry: HistoryEntry, + positions: Vec, + }, +} + +impl ListItemType { + fn history_entry(&self) -> Option<&HistoryEntry> { + match self { + ListItemType::Entry { entry, .. } => Some(entry), + ListItemType::SearchResult { entry, .. } => Some(entry), + _ => None, + } + } +} + +#[allow(dead_code)] +pub enum ThreadHistoryEvent { + Open(HistoryEntry), +} + +impl EventEmitter for AcpThreadHistory {} + +impl AcpThreadHistory { + pub fn new( + history_store: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let search_editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_placeholder_text("Search threads...", window, cx); + editor + }); + + let search_editor_subscription = + cx.subscribe(&search_editor, |this, search_editor, event, cx| { + if let EditorEvent::BufferEdited = event { + let query = search_editor.read(cx).text(cx); + if this.search_query != query { + this.search_query = query.into(); + this.update_visible_items(false, cx); + } + } + }); + + let history_store_subscription = cx.observe(&history_store, |this, _, cx| { + this.update_visible_items(true, cx); + }); + + let scroll_handle = UniformListScrollHandle::default(); + + let mut this = Self { + history_store, + scroll_handle, + selected_index: 0, + hovered_index: None, + visible_items: Default::default(), + search_editor, + local_timezone: UtcOffset::from_whole_seconds( + chrono::Local::now().offset().local_minus_utc(), + ) + .unwrap(), + search_query: SharedString::default(), + confirming_delete_history: false, + _subscriptions: vec![search_editor_subscription, history_store_subscription], + _update_task: Task::ready(()), + }; + this.update_visible_items(false, cx); + this + } + + fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context) { + let entries = self + .history_store + .update(cx, |store, _| store.entries().collect()); + let new_list_items = if self.search_query.is_empty() { + self.add_list_separators(entries, cx) + } else { + self.filter_search_results(entries, cx) + }; + let selected_history_entry = if preserve_selected_item { + self.selected_history_entry().cloned() + } else { + None + }; + + self._update_task = cx.spawn(async move |this, cx| { + let new_visible_items = new_list_items.await; + this.update(cx, |this, cx| { + let new_selected_index = if let Some(history_entry) = selected_history_entry { + let history_entry_id = history_entry.id(); + new_visible_items + .iter() + .position(|visible_entry| { + visible_entry + .history_entry() + .is_some_and(|entry| entry.id() == history_entry_id) + }) + .unwrap_or(0) + } else { + 0 + }; + + this.visible_items = new_visible_items; + this.set_selected_index(new_selected_index, Bias::Right, cx); + cx.notify(); + }) + .ok(); + }); + } + + fn add_list_separators(&self, entries: Vec, cx: &App) -> Task> { + cx.background_spawn(async move { + let mut items = Vec::with_capacity(entries.len() + 1); + let mut bucket = None; + let today = Local::now().naive_local().date(); + + for entry in entries.into_iter() { + let entry_date = entry + .updated_at() + .with_timezone(&Local) + .naive_local() + .date(); + let entry_bucket = TimeBucket::from_dates(today, entry_date); + + if Some(entry_bucket) != bucket { + bucket = Some(entry_bucket); + items.push(ListItemType::BucketSeparator(entry_bucket)); + } + + items.push(ListItemType::Entry { + entry, + format: entry_bucket.into(), + }); + } + items + }) + } + + fn filter_search_results( + &self, + entries: Vec, + cx: &App, + ) -> Task> { + let query = self.search_query.clone(); + cx.background_spawn({ + let executor = cx.background_executor().clone(); + async move { + let mut candidates = Vec::with_capacity(entries.len()); + + for (idx, entry) in entries.iter().enumerate() { + candidates.push(StringMatchCandidate::new(idx, entry.title())); + } + + const MAX_MATCHES: usize = 100; + + let matches = fuzzy::match_strings( + &candidates, + &query, + false, + true, + MAX_MATCHES, + &Default::default(), + executor, + ) + .await; + + matches + .into_iter() + .map(|search_match| ListItemType::SearchResult { + entry: entries[search_match.candidate_id].clone(), + positions: search_match.positions, + }) + .collect() + } + }) + } + + fn search_produced_no_matches(&self) -> bool { + self.visible_items.is_empty() && !self.search_query.is_empty() + } + + fn selected_history_entry(&self) -> Option<&HistoryEntry> { + self.get_history_entry(self.selected_index) + } + + fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> { + self.visible_items.get(visible_items_ix)?.history_entry() + } + + fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context) { + if self.visible_items.is_empty() { + self.selected_index = 0; + return; + } + while matches!( + self.visible_items.get(index), + None | Some(ListItemType::BucketSeparator(..)) + ) { + index = match bias { + Bias::Left => { + if index == 0 { + self.visible_items.len() - 1 + } else { + index - 1 + } + } + Bias::Right => { + if index >= self.visible_items.len() - 1 { + 0 + } else { + index + 1 + } + } + }; + } + self.selected_index = index; + self.scroll_handle + .scroll_to_item(index, ScrollStrategy::Top); + cx.notify() + } + + pub fn select_previous( + &mut self, + _: &menu::SelectPrevious, + _window: &mut Window, + cx: &mut Context, + ) { + if self.selected_index == 0 { + self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx); + } else { + self.set_selected_index(self.selected_index - 1, Bias::Left, cx); + } + } + + pub fn select_next( + &mut self, + _: &menu::SelectNext, + _window: &mut Window, + cx: &mut Context, + ) { + if self.selected_index == self.visible_items.len() - 1 { + self.set_selected_index(0, Bias::Right, cx); + } else { + self.set_selected_index(self.selected_index + 1, Bias::Right, cx); + } + } + + fn select_first( + &mut self, + _: &menu::SelectFirst, + _window: &mut Window, + cx: &mut Context, + ) { + self.set_selected_index(0, Bias::Right, cx); + } + + fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { + self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx); + } + + fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { + self.confirm_entry(self.selected_index, cx); + } + + fn confirm_entry(&mut self, ix: usize, cx: &mut Context) { + let Some(entry) = self.get_history_entry(ix) else { + return; + }; + cx.emit(ThreadHistoryEvent::Open(entry.clone())); + } + + fn remove_selected_thread( + &mut self, + _: &RemoveSelectedThread, + _window: &mut Window, + cx: &mut Context, + ) { + self.remove_thread(self.selected_index, cx) + } + + fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context) { + let Some(entry) = self.get_history_entry(visible_item_ix) else { + return; + }; + + let task = match entry { + HistoryEntry::AcpThread(thread) => self + .history_store + .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)), + HistoryEntry::TextThread(text_thread) => self.history_store.update(cx, |this, cx| { + this.delete_text_thread(text_thread.path.clone(), cx) + }), + }; + task.detach_and_log_err(cx); + } + + fn remove_history(&mut self, _window: &mut Window, cx: &mut Context) { + self.history_store.update(cx, |store, cx| { + store.delete_threads(cx).detach_and_log_err(cx) + }); + self.confirming_delete_history = false; + cx.notify(); + } + + fn prompt_delete_history(&mut self, _window: &mut Window, cx: &mut Context) { + self.confirming_delete_history = true; + cx.notify(); + } + + fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context) { + self.confirming_delete_history = false; + cx.notify(); + } + + fn render_list_items( + &mut self, + range: Range, + _window: &mut Window, + cx: &mut Context, + ) -> Vec { + self.visible_items + .get(range.clone()) + .into_iter() + .flatten() + .enumerate() + .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx)) + .collect() + } + + fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context) -> AnyElement { + match item { + ListItemType::Entry { entry, format } => self + .render_history_entry(entry, *format, ix, Vec::default(), cx) + .into_any(), + ListItemType::SearchResult { entry, positions } => self.render_history_entry( + entry, + EntryTimeFormat::DateAndTime, + ix, + positions.clone(), + cx, + ), + ListItemType::BucketSeparator(bucket) => div() + .px(DynamicSpacing::Base06.rems(cx)) + .pt_2() + .pb_1() + .child( + Label::new(bucket.to_string()) + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + .into_any_element(), + } + } + + fn render_history_entry( + &self, + entry: &HistoryEntry, + format: EntryTimeFormat, + ix: usize, + highlight_positions: Vec, + cx: &Context, + ) -> AnyElement { + let selected = ix == self.selected_index; + let hovered = Some(ix) == self.hovered_index; + let timestamp = entry.updated_at().timestamp(); + let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone); + + h_flex() + .w_full() + .pb_1() + .child( + ListItem::new(ix) + .rounded() + .toggle_state(selected) + .spacing(ListItemSpacing::Sparse) + .start_slot( + h_flex() + .w_full() + .gap_2() + .justify_between() + .child( + HighlightedLabel::new(entry.title(), highlight_positions) + .size(LabelSize::Small) + .truncate(), + ) + .child( + Label::new(thread_timestamp) + .color(Color::Muted) + .size(LabelSize::XSmall), + ), + ) + .on_hover(cx.listener(move |this, is_hovered, _window, cx| { + if *is_hovered { + this.hovered_index = Some(ix); + } else if this.hovered_index == Some(ix) { + this.hovered_index = None; + } + + cx.notify(); + })) + .end_slot::(if hovered { + Some( + IconButton::new("delete", IconName::Trash) + .shape(IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(move |_window, cx| { + Tooltip::for_action("Delete", &RemoveSelectedThread, cx) + }) + .on_click(cx.listener(move |this, _, _, cx| { + this.remove_thread(ix, cx); + cx.stop_propagation() + })), + ) + } else { + None + }) + .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))), + ) + .into_any_element() + } +} + +impl Focusable for AcpThreadHistory { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.search_editor.focus_handle(cx) + } +} + +impl Render for AcpThreadHistory { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let has_no_history = self.history_store.read(cx).is_empty(cx); + + v_flex() + .key_context("ThreadHistory") + .size_full() + .bg(cx.theme().colors().panel_background) + .on_action(cx.listener(Self::select_previous)) + .on_action(cx.listener(Self::select_next)) + .on_action(cx.listener(Self::select_first)) + .on_action(cx.listener(Self::select_last)) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::remove_selected_thread)) + .on_action(cx.listener(|this, _: &RemoveHistory, window, cx| { + this.remove_history(window, cx); + })) + .child( + h_flex() + .h(Tab::container_height(cx)) + .w_full() + .py_1() + .px_2() + .gap_2() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border) + .child( + Icon::new(IconName::MagnifyingGlass) + .color(Color::Muted) + .size(IconSize::Small), + ) + .child(self.search_editor.clone()), + ) + .child({ + let view = v_flex() + .id("list-container") + .relative() + .overflow_hidden() + .flex_grow(); + + if has_no_history { + view.justify_center().items_center().child( + Label::new("You don't have any past threads yet.") + .size(LabelSize::Small) + .color(Color::Muted), + ) + } else if self.search_produced_no_matches() { + view.justify_center() + .items_center() + .child(Label::new("No threads match your search.").size(LabelSize::Small)) + } else { + view.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() + .pr_4() + .track_scroll(&self.scroll_handle) + .flex_grow(), + ) + .vertical_scrollbar_for(&self.scroll_handle, window, cx) + } + }) + .when(!has_no_history, |this| { + this.child( + h_flex() + .p_2() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .when(!self.confirming_delete_history, |this| { + this.child( + Button::new("delete_history", "Delete All History") + .full_width() + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + this.prompt_delete_history(window, cx); + })), + ) + }) + .when(self.confirming_delete_history, |this| { + this.w_full() + .gap_2() + .flex_wrap() + .justify_between() + .child( + h_flex() + .flex_wrap() + .gap_1() + .child( + Label::new("Delete all threads?") + .size(LabelSize::Small), + ) + .child( + Label::new("You won't be able to recover them later.") + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .child( + h_flex() + .gap_1() + .child( + Button::new("cancel_delete", "Cancel") + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + this.cancel_delete_history(window, cx); + })), + ) + .child( + Button::new("confirm_delete", "Delete") + .style(ButtonStyle::Tinted(ui::TintColor::Error)) + .color(Color::Error) + .label_size(LabelSize::Small) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action( + Box::new(RemoveHistory), + cx, + ); + })), + ), + ) + }), + ) + }) + } +} + +#[derive(Clone, Copy)] +pub enum EntryTimeFormat { + DateAndTime, + TimeOnly, +} + +impl EntryTimeFormat { + fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String { + let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap(); + + match self { + EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp( + timestamp, + OffsetDateTime::now_utc(), + timezone, + time_format::TimestampFormat::EnhancedAbsolute, + ), + EntryTimeFormat::TimeOnly => time_format::format_time(timestamp.to_offset(timezone)), + } + } +} + +impl From for EntryTimeFormat { + fn from(bucket: TimeBucket) -> Self { + match bucket { + TimeBucket::Today => EntryTimeFormat::TimeOnly, + TimeBucket::Yesterday => EntryTimeFormat::TimeOnly, + TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime, + TimeBucket::PastWeek => EntryTimeFormat::DateAndTime, + TimeBucket::All => EntryTimeFormat::DateAndTime, + } + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +enum TimeBucket { + Today, + Yesterday, + ThisWeek, + PastWeek, + All, +} + +impl TimeBucket { + fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self { + if date == reference { + return TimeBucket::Today; + } + + if date == reference - TimeDelta::days(1) { + return TimeBucket::Yesterday; + } + + let week = date.iso_week(); + + if reference.iso_week() == week { + return TimeBucket::ThisWeek; + } + + let last_week = (reference - TimeDelta::days(7)).iso_week(); + + if week == last_week { + return TimeBucket::PastWeek; + } + + TimeBucket::All + } +} + +impl Display for TimeBucket { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TimeBucket::Today => write!(f, "Today"), + TimeBucket::Yesterday => write!(f, "Yesterday"), + TimeBucket::ThisWeek => write!(f, "This Week"), + TimeBucket::PastWeek => write!(f, "Past Week"), + TimeBucket::All => write!(f, "All"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::NaiveDate; + + #[test] + fn test_time_bucket_from_dates() { + let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); + + let date = today; + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today); + + let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday); + + let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek); + + let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek); + + let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek); + + let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek); + + // All: not in this week or last week + let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All); + + // Test year boundary cases + let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(); + + let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap(); + assert_eq!( + TimeBucket::from_dates(new_year, date), + TimeBucket::Yesterday + ); + + let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap(); + assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek); + } +} diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index bf2a305011fad924d38dcb391840661f1fd3cdbb..275486c275ade25d07d10c007093d7730d2b59ef 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -429,10 +429,24 @@ impl Model { let mut headers = vec![]; match self { + Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 + | Self::ClaudeOpus4_5 + | Self::ClaudeSonnet4 + | Self::ClaudeSonnet4_5 + | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking + | Self::ClaudeOpus4_5Thinking + | Self::ClaudeSonnet4Thinking + | Self::ClaudeSonnet4_5Thinking => { + // Fine-grained tool streaming for newer models + headers.push("fine-grained-tool-streaming-2025-05-14".to_string()); + } Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => { // Try beta token-efficient tool use (supported in Claude 3.7 Sonnet only) // https://docs.anthropic.com/en/docs/build-with-claude/tool-use/token-efficient-tool-use headers.push("token-efficient-tools-2025-02-19".to_string()); + headers.push("fine-grained-tool-streaming-2025-05-14".to_string()); } Self::Custom { extra_beta_headers, .. diff --git a/crates/assistant_slash_commands/Cargo.toml b/crates/assistant_slash_commands/Cargo.toml index 85dd92501f93fb79ba1d3f70b3a06f1077356cfa..b2a70449f449f73c7d0017c5d2ba3707e271559a 100644 --- a/crates/assistant_slash_commands/Cargo.toml +++ b/crates/assistant_slash_commands/Cargo.toml @@ -22,7 +22,6 @@ feature_flags.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true -globset.workspace = true gpui.workspace = true html_to_markdown.workspace = true http_client.workspace = true diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index a17e198ed300f00f70d35149cbe0286af3a65a57..ae4e8363b40d520b9ea33e5cba5ffa68d783ab04 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -226,10 +226,10 @@ fn collect_files( let Ok(matchers) = glob_inputs .iter() .map(|glob_input| { - custom_path_matcher::PathMatcher::new(&[glob_input.to_owned()]) + util::paths::PathMatcher::new(&[glob_input.to_owned()], project.read(cx).path_style(cx)) .with_context(|| format!("invalid path {glob_input}")) }) - .collect::>>() + .collect::>>() else { return futures::stream::once(async { anyhow::bail!("invalid path"); @@ -250,6 +250,7 @@ fn collect_files( let worktree_id = snapshot.id(); let path_style = snapshot.path_style(); let mut directory_stack: Vec> = Vec::new(); + let mut folded_directory_path: Option> = None; let mut folded_directory_names: Arc = RelPath::empty().into(); let mut is_top_level_directory = true; @@ -277,6 +278,16 @@ fn collect_files( )))?; } + if let Some(folded_path) = &folded_directory_path { + if !entry.path.starts_with(folded_path) { + folded_directory_names = RelPath::empty().into(); + folded_directory_path = None; + if directory_stack.is_empty() { + is_top_level_directory = true; + } + } + } + let filename = entry.path.file_name().unwrap_or_default().to_string(); if entry.is_dir() { @@ -292,13 +303,17 @@ fn collect_files( folded_directory_names = folded_directory_names.join(RelPath::unix(&filename).unwrap()); } + folded_directory_path = Some(entry.path.clone()); continue; } } else { // Skip empty directories folded_directory_names = RelPath::empty().into(); + folded_directory_path = None; continue; } + + // Render the directory (either folded or normal) if folded_directory_names.is_empty() { let label = if is_top_level_directory { is_top_level_directory = false; @@ -334,6 +349,8 @@ fn collect_files( }, )))?; directory_stack.push(entry.path.clone()); + folded_directory_names = RelPath::empty().into(); + folded_directory_path = None; } events_tx.unbounded_send(Ok(SlashCommandEvent::Content( SlashCommandContent::Text { @@ -447,87 +464,6 @@ pub fn build_entry_output_section( } } -/// This contains a small fork of the util::paths::PathMatcher, that is stricter about the prefix -/// check. Only subpaths pass the prefix check, rather than any prefix. -mod custom_path_matcher { - use globset::{Glob, GlobSet, GlobSetBuilder}; - use std::fmt::Debug as _; - use util::{paths::SanitizedPath, rel_path::RelPath}; - - #[derive(Clone, Debug, Default)] - pub struct PathMatcher { - sources: Vec, - sources_with_trailing_slash: Vec, - glob: GlobSet, - } - - impl std::fmt::Display for PathMatcher { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.sources.fmt(f) - } - } - - impl PartialEq for PathMatcher { - fn eq(&self, other: &Self) -> bool { - self.sources.eq(&other.sources) - } - } - - impl Eq for PathMatcher {} - - impl PathMatcher { - pub fn new(globs: &[String]) -> Result { - let globs = globs - .iter() - .map(|glob| Glob::new(&SanitizedPath::new(glob).to_string())) - .collect::, _>>()?; - let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect(); - let sources_with_trailing_slash = globs - .iter() - .map(|glob| glob.glob().to_string() + "/") - .collect(); - let mut glob_builder = GlobSetBuilder::new(); - for single_glob in globs { - glob_builder.add(single_glob); - } - let glob = glob_builder.build()?; - Ok(PathMatcher { - glob, - sources, - sources_with_trailing_slash, - }) - } - - pub fn is_match(&self, other: &RelPath) -> bool { - self.sources - .iter() - .zip(self.sources_with_trailing_slash.iter()) - .any(|(source, with_slash)| { - let as_bytes = other.as_unix_str().as_bytes(); - let with_slash = if source.ends_with('/') { - source.as_bytes() - } else { - with_slash.as_bytes() - }; - - as_bytes.starts_with(with_slash) || as_bytes.ends_with(source.as_bytes()) - }) - || self.glob.is_match(other.as_std_path()) - || self.check_with_end_separator(other) - } - - fn check_with_end_separator(&self, path: &RelPath) -> bool { - let path_str = path.as_unix_str(); - let separator = "/"; - if path_str.ends_with(separator) { - false - } else { - self.glob.is_match(path_str.to_string() + separator) - } - } - } -} - pub fn append_buffer_to_output( buffer: &BufferSnapshot, path: Option<&str>, diff --git a/crates/assistant_text_thread/Cargo.toml b/crates/assistant_text_thread/Cargo.toml index 7c8fcca3bfa81f6f2de570fa68ecc795cb81b257..5ad429758ea1785ecb4fcecb2f3ad83a71afda0d 100644 --- a/crates/assistant_text_thread/Cargo.toml +++ b/crates/assistant_text_thread/Cargo.toml @@ -46,7 +46,7 @@ serde_json.workspace = true settings.workspace = true smallvec.workspace = true smol.workspace = true -telemetry_events.workspace = true +telemetry.workspace = true text.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/assistant_text_thread/src/assistant_text_thread_tests.rs b/crates/assistant_text_thread/src/assistant_text_thread_tests.rs index 0743641bf5ce33850f28987d834b2e79771cff6f..7232a03c212a9dfc4bfe9bcce4a78667d9210ad8 100644 --- a/crates/assistant_text_thread/src/assistant_text_thread_tests.rs +++ b/crates/assistant_text_thread/src/assistant_text_thread_tests.rs @@ -50,7 +50,6 @@ fn test_inserting_and_removing_messages(cx: &mut App) { TextThread::local( registry, None, - None, prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), cx, @@ -189,7 +188,6 @@ fn test_message_splitting(cx: &mut App) { TextThread::local( registry.clone(), None, - None, prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), cx, @@ -294,7 +292,6 @@ fn test_messages_for_offsets(cx: &mut App) { TextThread::local( registry, None, - None, prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), cx, @@ -405,7 +402,6 @@ async fn test_slash_commands(cx: &mut TestAppContext) { TextThread::local( registry.clone(), None, - None, prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), cx, @@ -677,7 +673,6 @@ async fn test_serialization(cx: &mut TestAppContext) { TextThread::local( registry.clone(), None, - None, prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), cx, @@ -724,7 +719,6 @@ async fn test_serialization(cx: &mut TestAppContext) { prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), None, - None, cx, ) }); @@ -780,7 +774,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), None, - None, cx, ) }); @@ -1041,7 +1034,6 @@ fn test_mark_cache_anchors(cx: &mut App) { TextThread::local( registry, None, - None, prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), cx, @@ -1368,7 +1360,6 @@ fn setup_context_editor_with_fake_model( TextThread::local( registry, None, - None, prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), cx, diff --git a/crates/assistant_text_thread/src/text_thread.rs b/crates/assistant_text_thread/src/text_thread.rs index b808d9fb0019ccad25366d9ae60cc1f765126c74..5ec72eb0814f9ac09aba36f52d6f011af5b47249 100644 --- a/crates/assistant_text_thread/src/text_thread.rs +++ b/crates/assistant_text_thread/src/text_thread.rs @@ -5,7 +5,7 @@ use assistant_slash_command::{ SlashCommandResult, SlashCommandWorkingSet, }; use assistant_slash_commands::FileCommandMetadata; -use client::{self, ModelRequestUsage, RequestUsage, proto, telemetry::Telemetry}; +use client::{self, ModelRequestUsage, RequestUsage, proto}; use clock::ReplicaId; use cloud_llm_client::{CompletionIntent, UsageLimit}; use collections::{HashMap, HashSet}; @@ -19,10 +19,11 @@ use gpui::{ use itertools::Itertools as _; use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset}; use language_model::{ - LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent, - LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, + AnthropicCompletionType, AnthropicEventData, AnthropicEventType, LanguageModel, + LanguageModelCacheConfiguration, LanguageModelCompletionEvent, LanguageModelImage, + LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent, PaymentRequiredError, Role, StopReason, - report_assistant_event, + report_anthropic_event, }; use open_ai::Model as OpenAiModel; use paths::text_threads_dir; @@ -40,7 +41,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; + use text::{BufferSnapshot, ToPoint}; use ui::IconName; use util::{ResultExt, TryFutureExt, post_inc}; @@ -686,7 +687,6 @@ pub struct TextThread { pending_cache_warming_task: Task>, path: Option>, _subscriptions: Vec, - telemetry: Option>, language_registry: Arc, project: Option>, prompt_builder: Arc, @@ -709,7 +709,6 @@ impl TextThread { pub fn local( language_registry: Arc, project: Option>, - telemetry: Option>, prompt_builder: Arc, slash_commands: Arc, cx: &mut Context, @@ -722,7 +721,6 @@ impl TextThread { prompt_builder, slash_commands, project, - telemetry, cx, ) } @@ -743,7 +741,6 @@ impl TextThread { prompt_builder: Arc, slash_commands: Arc, project: Option>, - telemetry: Option>, cx: &mut Context, ) -> Self { let buffer = cx.new(|_cx| { @@ -784,7 +781,6 @@ impl TextThread { completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, path: None, buffer, - telemetry, project, language_registry, slash_commands, @@ -874,7 +870,6 @@ impl TextThread { prompt_builder: Arc, slash_commands: Arc, project: Option>, - telemetry: Option>, cx: &mut Context, ) -> Self { let id = saved_context.id.clone().unwrap_or_else(TextThreadId::new); @@ -886,7 +881,6 @@ impl TextThread { prompt_builder, slash_commands, project, - telemetry, cx, ); this.path = Some(path); @@ -2212,24 +2206,26 @@ impl TextThread { .read(cx) .language() .map(|language| language.name()); - report_assistant_event( - AssistantEventData { - conversation_id: Some(this.id.0.clone()), - kind: AssistantKind::Panel, - phase: AssistantPhase::Response, - message_id: None, - model: model.telemetry_id(), - model_provider: model.provider_id().to_string(), - response_latency, - error_message, - language_name: language_name.map(|name| name.to_proto()), - }, - this.telemetry.clone(), - cx.http_client(), - model.api_key(cx), - cx.background_executor(), + + telemetry::event!( + "Assistant Responded", + conversation_id = this.id.0.clone(), + kind = "panel", + phase = "response", + model = model.telemetry_id(), + model_provider = model.provider_id().to_string(), + response_latency, + error_message, + language_name = language_name.as_ref().map(|name| name.to_proto()), ); + report_anthropic_event(&model, AnthropicEventData { + completion_type: AnthropicCompletionType::Panel, + event: AnthropicEventType::Response, + language_name: language_name.map(|name| name.to_proto()), + message_id: None, + }, cx); + if let Ok(stop_reason) = result { match stop_reason { StopReason::ToolUse => {} diff --git a/crates/assistant_text_thread/src/text_thread_store.rs b/crates/assistant_text_thread/src/text_thread_store.rs index 71fabed503e8c04a8865bed72c28ae5b30e75574..483baa73134334162ea30d269a1f955dd8fe023a 100644 --- a/crates/assistant_text_thread/src/text_thread_store.rs +++ b/crates/assistant_text_thread/src/text_thread_store.rs @@ -4,7 +4,7 @@ use crate::{ }; use anyhow::{Context as _, Result}; use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet}; -use client::{Client, TypedEnvelope, proto, telemetry::Telemetry}; +use client::{Client, TypedEnvelope, proto}; use clock::ReplicaId; use collections::HashMap; use context_server::ContextServerId; @@ -48,7 +48,6 @@ pub struct TextThreadStore { fs: Arc, languages: Arc, slash_commands: Arc, - telemetry: Arc, _watch_updates: Task>, client: Arc, project: WeakEntity, @@ -88,7 +87,6 @@ impl TextThreadStore { ) -> Task>> { let fs = project.read(cx).fs().clone(); let languages = project.read(cx).languages().clone(); - let telemetry = project.read(cx).client().telemetry().clone(); cx.spawn(async move |cx| { const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100); let (mut events, _) = fs.watch(text_threads_dir(), CONTEXT_WATCH_DURATION).await; @@ -102,7 +100,6 @@ impl TextThreadStore { fs, languages, slash_commands, - telemetry, _watch_updates: cx.spawn(async move |this, cx| { async move { while events.next().await.is_some() { @@ -143,7 +140,6 @@ impl TextThreadStore { fs: project.read(cx).fs().clone(), languages: project.read(cx).languages().clone(), slash_commands: Arc::default(), - telemetry: project.read(cx).client().telemetry().clone(), _watch_updates: Task::ready(None), client: project.read(cx).client(), project: project.downgrade(), @@ -379,7 +375,6 @@ impl TextThreadStore { TextThread::local( self.languages.clone(), Some(self.project.clone()), - Some(self.telemetry.clone()), self.prompt_builder.clone(), self.slash_commands.clone(), cx, @@ -402,7 +397,7 @@ impl TextThreadStore { let capability = project.capability(); let language_registry = self.languages.clone(); let project = self.project.clone(); - let telemetry = self.telemetry.clone(); + let prompt_builder = self.prompt_builder.clone(); let slash_commands = self.slash_commands.clone(); let request = self.client.request(proto::CreateContext { project_id }); @@ -419,7 +414,6 @@ impl TextThreadStore { prompt_builder, slash_commands, Some(project), - Some(telemetry), cx, ) })?; @@ -457,7 +451,6 @@ impl TextThreadStore { let fs = self.fs.clone(); let languages = self.languages.clone(); let project = self.project.clone(); - let telemetry = self.telemetry.clone(); let load = cx.background_spawn({ let path = path.clone(); async move { @@ -478,7 +471,6 @@ impl TextThreadStore { prompt_builder, slash_commands, Some(project), - Some(telemetry), cx, ) })?; @@ -568,7 +560,6 @@ impl TextThreadStore { let capability = project.capability(); let language_registry = self.languages.clone(); let project = self.project.clone(); - let telemetry = self.telemetry.clone(); let request = self.client.request(proto::OpenContext { project_id, context_id: text_thread_id.to_proto(), @@ -587,7 +578,6 @@ impl TextThreadStore { prompt_builder, slash_commands, Some(project), - Some(telemetry), cx, ) })?; diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 6d6d229b940433ceac4c80f11891319550d269a2..14311d6bbf52ecb6df8dcc4a2fbc9454836a4834 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -150,9 +150,8 @@ pub fn init(client: &Arc, cx: &mut App) { .detach_and_log_err(cx); } } - }); - - cx.on_action({ + }) + .on_action({ let client = client.clone(); move |_: &SignOut, cx| { if let Some(client) = client.upgrade() { @@ -162,9 +161,8 @@ pub fn init(client: &Arc, cx: &mut App) { .detach(); } } - }); - - cx.on_action({ + }) + .on_action({ let client = client; move |_: &Reconnect, cx| { if let Some(client) = client.upgrade() { diff --git a/crates/cloud_llm_client/src/cloud_llm_client.rs b/crates/cloud_llm_client/src/cloud_llm_client.rs index 917929a985c85610b907e682792e132cb84d8403..2c5b2649000bb071b9d206d9d2c204f1eea9bda1 100644 --- a/crates/cloud_llm_client/src/cloud_llm_client.rs +++ b/crates/cloud_llm_client/src/cloud_llm_client.rs @@ -371,6 +371,8 @@ pub struct LanguageModel { pub supports_images: bool, pub supports_thinking: bool, pub supports_max_mode: bool, + #[serde(default)] + pub supports_streaming_tools: bool, // only used by OpenAI and xAI #[serde(default)] pub supports_parallel_tool_calls: bool, diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index ba92e868126c7f27fb5051021fce44fe43c8d5e7..4e6cdb0e79aba494bd01137cc262a097a084217e 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -312,6 +312,49 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu "Rust", FakeLspAdapter { capabilities: capabilities.clone(), + initializer: Some(Box::new(|fake_server| { + fake_server.set_request_handler::( + |params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 14), + ); + + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "first_method(…)".into(), + detail: Some("fn(&mut self, B) -> C".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "first_method($1)".to_string(), + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 14), + ), + })), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + lsp::CompletionItem { + label: "second_method(…)".into(), + detail: Some("fn(&mut self, C) -> D".into()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + new_text: "second_method()".to_string(), + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 14), + ), + })), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + ..Default::default() + }, + ]))) + }, + ); + })), ..FakeLspAdapter::default() }, ), @@ -320,6 +363,11 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu FakeLspAdapter { name: "fake-analyzer", capabilities: capabilities.clone(), + initializer: Some(Box::new(|fake_server| { + fake_server.set_request_handler::( + |_, _| async move { Ok(None) }, + ); + })), ..FakeLspAdapter::default() }, ), @@ -373,6 +421,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu let fake_language_server = fake_language_servers[0].next().await.unwrap(); let second_fake_language_server = fake_language_servers[1].next().await.unwrap(); cx_a.background_executor.run_until_parked(); + cx_b.background_executor.run_until_parked(); buffer_b.read_with(cx_b, |buffer, _| { assert!(!buffer.completion_triggers().is_empty()) @@ -387,58 +436,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu }); cx_b.focus(&editor_b); - // Receive a completion request as the host's language server. - // Return some completions from the host's language server. - cx_a.executor().start_waiting(); - fake_language_server - .set_request_handler::(|params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 14), - ); - - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "first_method(…)".into(), - detail: Some("fn(&mut self, B) -> C".into()), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - new_text: "first_method($1)".to_string(), - range: lsp::Range::new( - lsp::Position::new(0, 14), - lsp::Position::new(0, 14), - ), - })), - insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), - ..Default::default() - }, - lsp::CompletionItem { - label: "second_method(…)".into(), - detail: Some("fn(&mut self, C) -> D".into()), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - new_text: "second_method()".to_string(), - range: lsp::Range::new( - lsp::Position::new(0, 14), - lsp::Position::new(0, 14), - ), - })), - insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), - ..Default::default() - }, - ]))) - }) - .next() - .await - .unwrap(); - second_fake_language_server - .set_request_handler::(|_, _| async move { Ok(None) }) - .next() - .await - .unwrap(); - cx_a.executor().finish_waiting(); + // Allow the completion request to propagate from guest to host to LSP. + cx_b.background_executor.run_until_parked(); + cx_a.background_executor.run_until_parked(); // Open the buffer on the host. let buffer_a = project_a @@ -484,6 +484,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu // The additional edit is applied. cx_a.executor().run_until_parked(); + cx_b.executor().run_until_parked(); buffer_a.read_with(cx_a, |buffer, _| { assert_eq!( @@ -641,13 +642,11 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu ), })), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), - ..Default::default() + ..lsp::CompletionItem::default() }, ]))) }); - cx_b.executor().run_until_parked(); - // Await both language server responses first_lsp_completion.next().await.unwrap(); second_lsp_completion.next().await.unwrap(); diff --git a/crates/context_server/src/transport/stdio_transport.rs b/crates/context_server/src/transport/stdio_transport.rs index 031f348294c04381f1e259b20c7cc818844953b4..e675770e9ee50df9993076e6d71c70befa118c4b 100644 --- a/crates/context_server/src/transport/stdio_transport.rs +++ b/crates/context_server/src/transport/stdio_transport.rs @@ -32,7 +32,7 @@ impl StdioTransport { cx: &AsyncApp, ) -> Result { let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?; - let builder = ShellBuilder::new(&shell, cfg!(windows)); + let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive(); let mut command = builder.build_command(Some(binary.executable.display().to_string()), &binary.args); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 4e6520906074c1384a4e500d89be43659c162718..45f0796bf53acfef1fb1e81146c0de7c5187fb99 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -4,7 +4,7 @@ pub mod copilot_responses; pub mod request; mod sign_in; -use crate::sign_in::initiate_sign_in_within_workspace; +use crate::sign_in::initiate_sign_out; use ::fs::Fs; use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; @@ -28,12 +28,10 @@ use project::DisableAiSettings; use request::StatusNotification; use semver::Version; use serde_json::json; -use settings::Settings; -use settings::SettingsStore; -use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace}; -use std::collections::hash_map::Entry; +use settings::{Settings, SettingsStore}; use std::{ any::TypeId, + collections::hash_map::Entry, env, ffi::OsString, mem, @@ -42,12 +40,14 @@ use std::{ sync::Arc, }; use sum_tree::Dimensions; -use util::rel_path::RelPath; -use util::{ResultExt, fs::remove_matching}; +use util::{ResultExt, fs::remove_matching, rel_path::RelPath}; use workspace::Workspace; pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate; -pub use crate::sign_in::{CopilotCodeVerification, initiate_sign_in, reinstall_and_sign_in}; +pub use crate::sign_in::{ + ConfigurationMode, ConfigurationView, CopilotCodeVerification, initiate_sign_in, + reinstall_and_sign_in, +}; actions!( copilot, @@ -98,21 +98,14 @@ pub fn init( .detach(); cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(|workspace, _: &SignIn, window, cx| { - if let Some(copilot) = Copilot::global(cx) { - let is_reinstall = false; - initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx); - } + workspace.register_action(|_, _: &SignIn, window, cx| { + initiate_sign_in(window, cx); }); - workspace.register_action(|workspace, _: &Reinstall, window, cx| { - if let Some(copilot) = Copilot::global(cx) { - reinstall_and_sign_in_within_workspace(workspace, copilot, window, cx); - } + workspace.register_action(|_, _: &Reinstall, window, cx| { + reinstall_and_sign_in(window, cx); }); - workspace.register_action(|workspace, _: &SignOut, _window, cx| { - if let Some(copilot) = Copilot::global(cx) { - sign_out_within_workspace(workspace, copilot, cx); - } + workspace.register_action(|_, _: &SignOut, window, cx| { + initiate_sign_out(window, cx); }); }) .detach(); @@ -375,7 +368,7 @@ impl Copilot { } } - fn start_copilot( + pub fn start_copilot( &mut self, check_edit_prediction_provider: bool, awaiting_sign_in_after_start: bool, @@ -563,6 +556,14 @@ impl Copilot { let server = start_language_server.await; this.update(cx, |this, cx| { cx.notify(); + + if env::var("ZED_FORCE_COPILOT_ERROR").is_ok() { + this.server = CopilotServer::Error( + "Forced error for testing (ZED_FORCE_COPILOT_ERROR)".into(), + ); + return; + } + match server { Ok((server, status)) => { this.server = CopilotServer::Running(RunningCopilotServer { @@ -584,7 +585,17 @@ impl Copilot { .ok(); } - pub(crate) fn sign_in(&mut self, cx: &mut Context) -> Task> { + pub fn is_authenticated(&self) -> bool { + return matches!( + self.server, + CopilotServer::Running(RunningCopilotServer { + sign_in_status: SignInStatus::Authorized, + .. + }) + ); + } + + pub fn sign_in(&mut self, cx: &mut Context) -> Task> { if let CopilotServer::Running(server) = &mut self.server { let task = match &server.sign_in_status { SignInStatus::Authorized => Task::ready(Ok(())).shared(), diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 464a114d4ea11bca5597a6a91fd831ade050baaa..0bcb11e18be1994ea92703973ad1278c5d5aa4f8 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -1,160 +1,151 @@ use crate::{Copilot, Status, request::PromptUserDeviceFlow}; +use anyhow::Context as _; use gpui::{ - Animation, AnimationExt, App, ClipboardItem, Context, DismissEvent, Element, Entity, - EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, MouseDownEvent, - ParentElement, Render, Styled, Subscription, Transformation, Window, div, percentage, svg, + App, ClipboardItem, Context, DismissEvent, Element, Entity, EventEmitter, FocusHandle, + Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled, + Subscription, Window, WindowBounds, WindowOptions, div, point, }; -use std::time::Duration; -use ui::{Button, Label, Vector, VectorName, prelude::*}; +use ui::{ButtonLike, CommonAnimationExt, ConfiguredApiCard, Vector, VectorName, prelude::*}; use util::ResultExt as _; -use workspace::notifications::NotificationId; -use workspace::{ModalView, Toast, Workspace}; +use workspace::{Toast, Workspace, notifications::NotificationId}; const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot"; +const ERROR_LABEL: &str = + "Copilot had issues starting. You can try reinstalling it and signing in again."; struct CopilotStatusToast; pub fn initiate_sign_in(window: &mut Window, cx: &mut App) { + let is_reinstall = false; + initiate_sign_in_impl(is_reinstall, window, cx) +} + +pub fn initiate_sign_out(window: &mut Window, cx: &mut App) { let Some(copilot) = Copilot::global(cx) else { return; }; - let Some(workspace) = window.root::().flatten() else { - return; - }; - workspace.update(cx, |workspace, cx| { - let is_reinstall = false; - initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx) - }); + + copilot_toast(Some("Signing out of Copilot…"), window, cx); + + let sign_out_task = copilot.update(cx, |copilot, cx| copilot.sign_out(cx)); + window + .spawn(cx, async move |cx| match sign_out_task.await { + Ok(()) => { + cx.update(|window, cx| copilot_toast(Some("Signed out of Copilot"), window, cx)) + } + Err(err) => cx.update(|window, cx| { + if let Some(workspace) = window.root::().flatten() { + workspace.update(cx, |workspace, cx| { + workspace.show_error(&err, cx); + }) + } else { + log::error!("{:?}", err); + } + }), + }) + .detach(); } pub fn reinstall_and_sign_in(window: &mut Window, cx: &mut App) { let Some(copilot) = Copilot::global(cx) else { return; }; + let _ = copilot.update(cx, |copilot, cx| copilot.reinstall(cx)); + let is_reinstall = true; + initiate_sign_in_impl(is_reinstall, window, cx); +} + +fn open_copilot_code_verification_window(copilot: &Entity, window: &Window, cx: &mut App) { + let current_window_center = window.bounds().center(); + let height = px(450.); + let width = px(350.); + let window_bounds = WindowBounds::Windowed(gpui::bounds( + current_window_center - point(height / 2.0, width / 2.0), + gpui::size(height, width), + )); + cx.open_window( + WindowOptions { + kind: gpui::WindowKind::PopUp, + window_bounds: Some(window_bounds), + is_resizable: false, + is_movable: true, + titlebar: Some(gpui::TitlebarOptions { + appears_transparent: true, + ..Default::default() + }), + ..Default::default() + }, + |window, cx| cx.new(|cx| CopilotCodeVerification::new(&copilot, window, cx)), + ) + .context("Failed to open Copilot code verification window") + .log_err(); +} + +fn copilot_toast(message: Option<&'static str>, window: &Window, cx: &mut App) { + const NOTIFICATION_ID: NotificationId = NotificationId::unique::(); + let Some(workspace) = window.root::().flatten() else { return; }; - workspace.update(cx, |workspace, cx| { - reinstall_and_sign_in_within_workspace(workspace, copilot, window, cx); - }); -} -pub fn reinstall_and_sign_in_within_workspace( - workspace: &mut Workspace, - copilot: Entity, - window: &mut Window, - cx: &mut Context, -) { - let _ = copilot.update(cx, |copilot, cx| copilot.reinstall(cx)); - let is_reinstall = true; - initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx); + workspace.update(cx, |workspace, cx| match message { + Some(message) => workspace.show_toast(Toast::new(NOTIFICATION_ID, message), cx), + None => workspace.dismiss_toast(&NOTIFICATION_ID, cx), + }); } -pub fn initiate_sign_in_within_workspace( - workspace: &mut Workspace, - copilot: Entity, - is_reinstall: bool, - window: &mut Window, - cx: &mut Context, -) { +pub fn initiate_sign_in_impl(is_reinstall: bool, window: &mut Window, cx: &mut App) { + let Some(copilot) = Copilot::global(cx) else { + return; + }; if matches!(copilot.read(cx).status(), Status::Disabled) { copilot.update(cx, |copilot, cx| copilot.start_copilot(false, true, cx)); } match copilot.read(cx).status() { Status::Starting { task } => { - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - if is_reinstall { - "Copilot is reinstalling..." - } else { - "Copilot is starting..." - }, - ), + copilot_toast( + Some(if is_reinstall { + "Copilot is reinstalling…" + } else { + "Copilot is starting…" + }), + window, cx, ); - cx.spawn_in(window, async move |workspace, cx| { - task.await; - if let Some(copilot) = cx.update(|_window, cx| Copilot::global(cx)).ok().flatten() { - workspace - .update_in(cx, |workspace, window, cx| { - match copilot.read(cx).status() { - Status::Authorized => workspace.show_toast( - Toast::new( - NotificationId::unique::(), - "Copilot has started.", - ), - cx, - ), - _ => { - workspace.dismiss_toast( - &NotificationId::unique::(), - cx, - ); - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .detach_and_log_err(cx); - workspace.toggle_modal(window, cx, |_, cx| { - CopilotCodeVerification::new(&copilot, cx) - }); - } + window + .spawn(cx, async move |cx| { + task.await; + cx.update(|window, cx| { + let Some(copilot) = Copilot::global(cx) else { + return; + }; + match copilot.read(cx).status() { + Status::Authorized => { + copilot_toast(Some("Copilot has started."), window, cx) } - }) - .log_err(); - } - }) - .detach(); + _ => { + copilot_toast(None, window, cx); + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .detach_and_log_err(cx); + open_copilot_code_verification_window(&copilot, window, cx); + } + } + }) + .log_err(); + }) + .detach(); } _ => { copilot .update(cx, |copilot, cx| copilot.sign_in(cx)) .detach(); - workspace.toggle_modal(window, cx, |_, cx| { - CopilotCodeVerification::new(&copilot, cx) - }); + open_copilot_code_verification_window(&copilot, window, cx); } } } -pub fn sign_out_within_workspace( - workspace: &mut Workspace, - copilot: Entity, - cx: &mut Context, -) { - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - "Signing out of Copilot...", - ), - cx, - ); - let sign_out_task = copilot.update(cx, |copilot, cx| copilot.sign_out(cx)); - cx.spawn(async move |workspace, cx| match sign_out_task.await { - Ok(()) => { - workspace - .update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - "Signed out of Copilot.", - ), - cx, - ) - }) - .ok(); - } - Err(err) => { - workspace - .update(cx, |workspace, cx| { - workspace.show_error(&err, cx); - }) - .ok(); - } - }) - .detach(); -} - pub struct CopilotCodeVerification { status: Status, connect_clicked: bool, @@ -170,23 +161,27 @@ impl Focusable for CopilotCodeVerification { } impl EventEmitter for CopilotCodeVerification {} -impl ModalView for CopilotCodeVerification { - fn on_before_dismiss( - &mut self, - _: &mut Window, - cx: &mut Context, - ) -> workspace::DismissDecision { - self.copilot.update(cx, |copilot, cx| { - if matches!(copilot.status(), Status::SigningIn { .. }) { - copilot.sign_out(cx).detach_and_log_err(cx); + +impl CopilotCodeVerification { + pub fn new(copilot: &Entity, window: &mut Window, cx: &mut Context) -> Self { + window.on_window_should_close(cx, |window, cx| { + if let Some(this) = window.root::().flatten() { + this.update(cx, |this, cx| { + this.before_dismiss(cx); + }); } + true }); - workspace::DismissDecision::Dismiss(true) - } -} + cx.subscribe_in( + &cx.entity(), + window, + |this, _, _: &DismissEvent, window, cx| { + window.remove_window(); + this.before_dismiss(cx); + }, + ) + .detach(); -impl CopilotCodeVerification { - pub fn new(copilot: &Entity, cx: &mut Context) -> Self { let status = copilot.read(cx).status(); Self { status, @@ -215,45 +210,45 @@ impl CopilotCodeVerification { .read_from_clipboard() .map(|item| item.text().as_ref() == Some(&data.user_code)) .unwrap_or(false); - h_flex() - .w_full() - .p_1() - .border_1() - .border_muted(cx) - .rounded_sm() - .cursor_pointer() - .justify_between() - .on_mouse_down(gpui::MouseButton::Left, { + + ButtonLike::new("copy-button") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .size(ButtonSize::Medium) + .child( + h_flex() + .w_full() + .p_1() + .justify_between() + .child(Label::new(data.user_code.clone())) + .child(Label::new(if copied { "Copied!" } else { "Copy" })), + ) + .on_click({ let user_code = data.user_code.clone(); move |_, window, cx| { cx.write_to_clipboard(ClipboardItem::new_string(user_code.clone())); window.refresh(); } }) - .child(div().flex_1().child(Label::new(data.user_code.clone()))) - .child(div().flex_none().px_1().child(Label::new(if copied { - "Copied!" - } else { - "Copy" - }))) } fn render_prompting_modal( connect_clicked: bool, data: &PromptUserDeviceFlow, - cx: &mut Context, ) -> impl Element { let connect_button_label = if connect_clicked { - "Waiting for connection..." + "Waiting for connection…" } else { "Connect to GitHub" }; + v_flex() .flex_1() - .gap_2() + .gap_2p5() .items_center() - .child(Headline::new("Use GitHub Copilot in Zed.").size(HeadlineSize::Large)) + .text_center() + .child(Headline::new("Use GitHub Copilot in Zed").size(HeadlineSize::Large)) .child( Label::new("Using Copilot requires an active subscription on GitHub.") .color(Color::Muted), @@ -261,83 +256,119 @@ impl CopilotCodeVerification { .child(Self::render_device_code(data, cx)) .child( Label::new("Paste this code into GitHub after clicking the button below.") - .size(ui::LabelSize::Small), - ) - .child( - Button::new("connect-button", connect_button_label) - .on_click({ - let verification_uri = data.verification_uri.clone(); - cx.listener(move |this, _, _window, cx| { - cx.open_url(&verification_uri); - this.connect_clicked = true; - }) - }) - .full_width() - .style(ButtonStyle::Filled), + .color(Color::Muted), ) .child( - Button::new("copilot-enable-cancel-button", "Cancel") - .full_width() - .on_click(cx.listener(|_, _, _, cx| { - cx.emit(DismissEvent); - })), + v_flex() + .w_full() + .gap_1() + .child( + Button::new("connect-button", connect_button_label) + .full_width() + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .on_click({ + let verification_uri = data.verification_uri.clone(); + cx.listener(move |this, _, _window, cx| { + cx.open_url(&verification_uri); + this.connect_clicked = true; + }) + }), + ) + .child( + Button::new("copilot-enable-cancel-button", "Cancel") + .full_width() + .size(ButtonSize::Medium) + .on_click(cx.listener(|_, _, _, cx| { + cx.emit(DismissEvent); + })), + ), ) } fn render_enabled_modal(cx: &mut Context) -> impl Element { v_flex() .gap_2() + .text_center() + .justify_center() .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large)) - .child(Label::new( - "You can update your settings or sign out from the Copilot menu in the status bar.", - )) + .child(Label::new("You're all set to use GitHub Copilot.").color(Color::Muted)) .child( Button::new("copilot-enabled-done-button", "Done") .full_width() + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), ) } fn render_unauthorized_modal(cx: &mut Context) -> impl Element { - v_flex() - .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large)) + let description = "Enable Copilot by connecting your existing license once you have subscribed or renewed your subscription."; - .child(Label::new( - "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.", - ).color(Color::Warning)) + v_flex() + .gap_2() + .text_center() + .justify_center() + .child( + Headline::new("You must have an active GitHub Copilot subscription.") + .size(HeadlineSize::Large), + ) + .child(Label::new(description).color(Color::Warning)) .child( Button::new("copilot-subscribe-button", "Subscribe on GitHub") .full_width() + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) .on_click(|_, _, cx| cx.open_url(COPILOT_SIGN_UP_URL)), ) .child( Button::new("copilot-subscribe-cancel-button", "Cancel") .full_width() + .size(ButtonSize::Medium) .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), ) } - fn render_loading(window: &mut Window, _: &mut Context) -> impl Element { - let loading_icon = svg() - .size_8() - .path(IconName::ArrowCircle.path()) - .text_color(window.text_style().color) - .with_animation( - "icon_circle_arrow", - Animation::new(Duration::from_secs(2)).repeat(), - |svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))), - ); + fn render_error_modal(_cx: &mut Context) -> impl Element { + v_flex() + .gap_2() + .text_center() + .justify_center() + .child(Headline::new("An Error Happened").size(HeadlineSize::Large)) + .child(Label::new(ERROR_LABEL).color(Color::Muted)) + .child( + Button::new("copilot-subscribe-button", "Reinstall Copilot and Sign In") + .full_width() + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .icon(IconName::Download) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .on_click(|_, window, cx| reinstall_and_sign_in(window, cx)), + ) + } - h_flex().justify_center().child(loading_icon) + fn before_dismiss( + &mut self, + cx: &mut Context<'_, CopilotCodeVerification>, + ) -> workspace::DismissDecision { + self.copilot.update(cx, |copilot, cx| { + if matches!(copilot.status(), Status::SigningIn { .. }) { + copilot.sign_out(cx).detach_and_log_err(cx); + } + }); + workspace::DismissDecision::Dismiss(true) } } impl Render for CopilotCodeVerification { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let prompt = match &self.status { - Status::SigningIn { prompt: None } => { - Self::render_loading(window, cx).into_any_element() - } + Status::SigningIn { prompt: None } => Icon::new(IconName::ArrowCircle) + .color(Color::Muted) + .with_rotate_animation(2) + .into_any_element(), Status::SigningIn { prompt: Some(prompt), } => Self::render_prompting_modal(self.connect_clicked, prompt, cx).into_any_element(), @@ -349,17 +380,20 @@ impl Render for CopilotCodeVerification { self.connect_clicked = false; Self::render_enabled_modal(cx).into_any_element() } + Status::Error(..) => Self::render_error_modal(cx).into_any_element(), _ => div().into_any_element(), }; v_flex() - .id("copilot code verification") + .id("copilot_code_verification") .track_focus(&self.focus_handle(cx)) - .elevation_3(cx) - .w_96() - .items_center() - .p_4() + .size_full() + .px_4() + .py_8() .gap_2() + .items_center() + .justify_center() + .elevation_3(cx) .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| { cx.emit(DismissEvent); })) @@ -373,3 +407,243 @@ impl Render for CopilotCodeVerification { .child(prompt) } } + +pub struct ConfigurationView { + copilot_status: Option, + is_authenticated: fn(cx: &App) -> bool, + edit_prediction: bool, + _subscription: Option, +} + +pub enum ConfigurationMode { + Chat, + EditPrediction, +} + +impl ConfigurationView { + pub fn new( + is_authenticated: fn(cx: &App) -> bool, + mode: ConfigurationMode, + cx: &mut Context, + ) -> Self { + let copilot = Copilot::global(cx); + + Self { + copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()), + is_authenticated, + edit_prediction: matches!(mode, ConfigurationMode::EditPrediction), + _subscription: copilot.as_ref().map(|copilot| { + cx.observe(copilot, |this, model, cx| { + this.copilot_status = Some(model.read(cx).status()); + cx.notify(); + }) + }), + } + } +} + +impl ConfigurationView { + fn is_starting(&self) -> bool { + matches!(&self.copilot_status, Some(Status::Starting { .. })) + } + + fn is_signing_in(&self) -> bool { + matches!( + &self.copilot_status, + Some(Status::SigningIn { .. }) + | Some(Status::SignedOut { + awaiting_signing_in: true + }) + ) + } + + fn is_error(&self) -> bool { + matches!(&self.copilot_status, Some(Status::Error(_))) + } + + fn has_no_status(&self) -> bool { + self.copilot_status.is_none() + } + + fn loading_message(&self) -> Option { + if self.is_starting() { + Some("Starting Copilot…".into()) + } else if self.is_signing_in() { + Some("Signing into Copilot…".into()) + } else { + None + } + } + + fn render_loading_button( + &self, + label: impl Into, + edit_prediction: bool, + ) -> impl IntoElement { + ButtonLike::new("loading_button") + .disabled(true) + .style(ButtonStyle::Outlined) + .when(edit_prediction, |this| this.size(ButtonSize::Medium)) + .child( + h_flex() + .w_full() + .gap_1() + .justify_center() + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .color(Color::Muted) + .with_rotate_animation(4), + ) + .child(Label::new(label)), + ) + } + + fn render_sign_in_button(&self, edit_prediction: bool) -> impl IntoElement { + let label = if edit_prediction { + "Sign in to GitHub" + } else { + "Sign in to use GitHub Copilot" + }; + + Button::new("sign_in", label) + .map(|this| { + if edit_prediction { + this.size(ButtonSize::Medium) + } else { + this.full_width() + } + }) + .style(ButtonStyle::Outlined) + .icon(IconName::Github) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .on_click(|_, window, cx| initiate_sign_in(window, cx)) + } + + fn render_reinstall_button(&self, edit_prediction: bool) -> impl IntoElement { + let label = if edit_prediction { + "Reinstall and Sign in" + } else { + "Reinstall Copilot and Sign in" + }; + + Button::new("reinstall_and_sign_in", label) + .map(|this| { + if edit_prediction { + this.size(ButtonSize::Medium) + } else { + this.full_width() + } + }) + .style(ButtonStyle::Outlined) + .icon(IconName::Download) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .on_click(|_, window, cx| reinstall_and_sign_in(window, cx)) + } + + fn render_for_edit_prediction(&self) -> impl IntoElement { + let container = |description: SharedString, action: AnyElement| { + h_flex() + .pt_2p5() + .w_full() + .justify_between() + .child( + v_flex() + .w_full() + .max_w_1_2() + .child(Label::new("Authenticate To Use")) + .child( + Label::new(description) + .color(Color::Muted) + .size(LabelSize::Small), + ), + ) + .child(action) + }; + + let start_label = "To use Copilot for edit predictions, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot subscription.".into(); + let no_status_label = "Copilot requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different edit predictions provider.".into(); + + if let Some(msg) = self.loading_message() { + container( + start_label, + self.render_loading_button(msg, true).into_any_element(), + ) + .into_any_element() + } else if self.is_error() { + container( + ERROR_LABEL.into(), + self.render_reinstall_button(true).into_any_element(), + ) + .into_any_element() + } else if self.has_no_status() { + container( + no_status_label, + self.render_sign_in_button(true).into_any_element(), + ) + .into_any_element() + } else { + container( + start_label, + self.render_sign_in_button(true).into_any_element(), + ) + .into_any_element() + } + } + + fn render_for_chat(&self) -> impl IntoElement { + let start_label = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription."; + let no_status_label = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different LLM provider."; + + if let Some(msg) = self.loading_message() { + v_flex() + .gap_2() + .child(Label::new(start_label)) + .child(self.render_loading_button(msg, false)) + .into_any_element() + } else if self.is_error() { + v_flex() + .gap_2() + .child(Label::new(ERROR_LABEL)) + .child(self.render_reinstall_button(false)) + .into_any_element() + } else if self.has_no_status() { + v_flex() + .gap_2() + .child(Label::new(no_status_label)) + .child(self.render_sign_in_button(false)) + .into_any_element() + } else { + v_flex() + .gap_2() + .child(Label::new(start_label)) + .child(self.render_sign_in_button(false)) + .into_any_element() + } + } +} + +impl Render for ConfigurationView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_authenticated = self.is_authenticated; + + if is_authenticated(cx) { + return ConfiguredApiCard::new("Authorized") + .button_label("Sign Out") + .on_click(|_, window, cx| { + initiate_sign_out(window, cx); + }) + .into_any_element(); + } + + if self.edit_prediction { + self.render_for_edit_prediction().into_any_element() + } else { + self.render_for_chat().into_any_element() + } + } +} diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index bdb308aafd0d2899f17bef732ac38239c4df6dda..104a85dc097c575e7a4cd8f4a66a98a8bb6b0d69 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1557,7 +1557,7 @@ impl Panel for DebugPanel { self.sessions_with_children.keys().for_each(|session_item| { session_item.update(cx, |item, cx| { item.running_state() - .update(cx, |state, _| state.invert_axies()) + .update(cx, |state, cx| state.invert_axies(cx)) }) }); } diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 66e9dd7b434e628898add7056b15c1789e32519c..4898ec95ca3c5b55669896b3c1d898326851c0c3 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -348,7 +348,7 @@ pub(crate) fn new_debugger_pane( debug_assert!(_previous_subscription.is_none()); running .panes - .split(&this_pane, &new_pane, split_direction)?; + .split(&this_pane, &new_pane, split_direction, cx)?; anyhow::Ok(new_pane) }) }) @@ -1462,7 +1462,7 @@ impl RunningState { this.serialize_layout(window, cx); match event { Event::Remove { .. } => { - let _did_find_pane = this.panes.remove(source_pane).is_ok(); + let _did_find_pane = this.panes.remove(source_pane, cx).is_ok(); debug_assert!(_did_find_pane); cx.notify(); } @@ -1889,9 +1889,9 @@ impl RunningState { Member::Axis(group_root) } - pub(crate) fn invert_axies(&mut self) { + pub(crate) fn invert_axies(&mut self, cx: &mut App) { self.dock_axis = self.dock_axis.invert(); - self.panes.invert_axies(); + self.panes.invert_axies(cx); } } diff --git a/crates/edit_prediction/Cargo.toml b/crates/edit_prediction/Cargo.toml index 53ddb99bd3f458a540c6593a2b1d6b1b547e463b..2d5fb36a581f7bd17bb76f79791c276c86c9c631 100644 --- a/crates/edit_prediction/Cargo.toml +++ b/crates/edit_prediction/Cargo.toml @@ -23,7 +23,6 @@ client.workspace = true cloud_llm_client.workspace = true collections.workspace = true copilot.workspace = true -credentials_provider.workspace = true db.workspace = true edit_prediction_types.workspace = true edit_prediction_context.workspace = true @@ -42,6 +41,7 @@ open_ai.workspace = true postage.workspace = true pretty_assertions.workspace = true project.workspace = true +pulldown-cmark.workspace = true rand.workspace = true regex.workspace = true release_channel.workspace = true diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index 6a7c6232d08b15fccacdd80a446432e453a80e20..ff15d04cc1c0f8e7bbeb7f2a29b520a8ec32097a 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -25,7 +25,7 @@ use gpui::{ prelude::*, }; use language::language_settings::all_language_settings; -use language::{Anchor, Buffer, File, Point, ToPoint}; +use language::{Anchor, Buffer, File, Point, TextBufferSnapshot, ToPoint}; use language::{BufferSnapshot, OffsetRangeExt}; use language_model::{LlmApiToken, RefreshLlmTokenListener}; use project::{Project, ProjectPath, WorktreeId}; @@ -47,7 +47,8 @@ use thiserror::Error; use util::{RangeExt as _, ResultExt as _}; use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification}; -mod cursor_excerpt; +pub mod cursor_excerpt; +pub mod example_spec; mod license_detection; pub mod mercury; mod onboarding_modal; @@ -72,6 +73,7 @@ pub use crate::prediction::EditPrediction; pub use crate::prediction::EditPredictionId; use crate::prediction::EditPredictionResult; pub use crate::sweep_ai::SweepAi; +pub use language_model::ApiKeyState; pub use telemetry_events::EditPredictionRating; pub use zed_edit_prediction_delegate::ZedEditPredictionDelegate; @@ -88,6 +90,7 @@ actions!( /// Maximum number of events to track. const EVENT_COUNT_MAX: usize = 6; const CHANGE_GROUPING_LINE_SPAN: u32 = 8; +const LAST_CHANGE_GROUPING_TIME: Duration = Duration::from_secs(1); const ZED_PREDICT_DATA_COLLECTION_CHOICE: &str = "zed_predict_data_collection_choice"; const REJECT_REQUEST_DEBOUNCE: Duration = Duration::from_secs(15); @@ -264,6 +267,19 @@ impl ProjectState { .collect() } + pub fn events_split_by_pause(&self, cx: &App) -> Vec> { + self.events + .iter() + .cloned() + .chain(self.last_event.as_ref().iter().flat_map(|event| { + let (one, two) = event.split_by_pause(); + let one = one.finalize(&self.license_detection_watchers, cx); + let two = two.and_then(|two| two.finalize(&self.license_detection_watchers, cx)); + one.into_iter().chain(two) + })) + .collect() + } + fn cancel_pending_prediction( &mut self, pending_prediction: PendingPrediction, @@ -384,15 +400,21 @@ impl std::ops::Deref for BufferEditPrediction<'_> { } struct RegisteredBuffer { - snapshot: BufferSnapshot, + file: Option>, + snapshot: TextBufferSnapshot, last_position: Option, _subscriptions: [gpui::Subscription; 2], } +#[derive(Clone)] struct LastEvent { - old_snapshot: BufferSnapshot, - new_snapshot: BufferSnapshot, + old_snapshot: TextBufferSnapshot, + new_snapshot: TextBufferSnapshot, + old_file: Option>, + new_file: Option>, end_edit_anchor: Option, + snapshot_after_last_editing_pause: Option, + last_edit_time: Option, } impl LastEvent { @@ -401,19 +423,19 @@ impl LastEvent { license_detection_watchers: &HashMap>, cx: &App, ) -> Option> { - let path = buffer_path_with_id_fallback(&self.new_snapshot, cx); - let old_path = buffer_path_with_id_fallback(&self.old_snapshot, cx); - - let file = self.new_snapshot.file(); - let old_file = self.old_snapshot.file(); - - let in_open_source_repo = [file, old_file].iter().all(|file| { - file.is_some_and(|file| { - license_detection_watchers - .get(&file.worktree_id(cx)) - .is_some_and(|watcher| watcher.is_project_open_source()) - }) - }); + let path = buffer_path_with_id_fallback(self.new_file.as_ref(), &self.new_snapshot, cx); + let old_path = buffer_path_with_id_fallback(self.old_file.as_ref(), &self.old_snapshot, cx); + + let in_open_source_repo = + [self.new_file.as_ref(), self.old_file.as_ref()] + .iter() + .all(|file| { + file.is_some_and(|file| { + license_detection_watchers + .get(&file.worktree_id(cx)) + .is_some_and(|watcher| watcher.is_project_open_source()) + }) + }); let diff = language::unified_diff(&self.old_snapshot.text(), &self.new_snapshot.text()); @@ -430,10 +452,42 @@ impl LastEvent { })) } } + + pub fn split_by_pause(&self) -> (LastEvent, Option) { + let Some(boundary_snapshot) = self.snapshot_after_last_editing_pause.as_ref() else { + return (self.clone(), None); + }; + + let before = LastEvent { + old_snapshot: self.old_snapshot.clone(), + new_snapshot: boundary_snapshot.clone(), + old_file: self.old_file.clone(), + new_file: self.new_file.clone(), + end_edit_anchor: self.end_edit_anchor, + snapshot_after_last_editing_pause: None, + last_edit_time: self.last_edit_time, + }; + + let after = LastEvent { + old_snapshot: boundary_snapshot.clone(), + new_snapshot: self.new_snapshot.clone(), + old_file: self.old_file.clone(), + new_file: self.new_file.clone(), + end_edit_anchor: self.end_edit_anchor, + snapshot_after_last_editing_pause: None, + last_edit_time: self.last_edit_time, + }; + + (before, Some(after)) + } } -fn buffer_path_with_id_fallback(snapshot: &BufferSnapshot, cx: &App) -> Arc { - if let Some(file) = snapshot.file() { +fn buffer_path_with_id_fallback( + file: Option<&Arc>, + snapshot: &TextBufferSnapshot, + cx: &App, +) -> Arc { + if let Some(file) = file { file.full_path(cx).into() } else { Path::new(&format!("untitled-{}", snapshot.remote_id())).into() @@ -536,22 +590,12 @@ impl EditPredictionStore { self.edit_prediction_model = model; } - pub fn has_sweep_api_token(&self) -> bool { - self.sweep_ai - .api_token - .clone() - .now_or_never() - .flatten() - .is_some() + pub fn has_sweep_api_token(&self, cx: &App) -> bool { + self.sweep_ai.api_token.read(cx).has_key() } - pub fn has_mercury_api_token(&self) -> bool { - self.mercury - .api_token - .clone() - .now_or_never() - .flatten() - .is_some() + pub fn has_mercury_api_token(&self, cx: &App) -> bool { + self.mercury.api_token.read(cx).has_key() } #[cfg(feature = "cli-support")] @@ -586,10 +630,22 @@ impl EditPredictionStore { pub fn edit_history_for_project( &self, project: &Entity, + cx: &App, + ) -> Vec> { + self.projects + .get(&project.entity_id()) + .map(|project_state| project_state.events(cx)) + .unwrap_or_default() + } + + pub fn edit_history_for_project_with_pause_split_last_event( + &self, + project: &Entity, + cx: &App, ) -> Vec> { self.projects .get(&project.entity_id()) - .map(|project_state| project_state.events.iter().cloned().collect()) + .map(|project_state| project_state.events_split_by_pause(cx)) .unwrap_or_default() } @@ -810,10 +866,13 @@ impl EditPredictionStore { match project_state.registered_buffers.entry(buffer_id) { hash_map::Entry::Occupied(entry) => entry.into_mut(), hash_map::Entry::Vacant(entry) => { - let snapshot = buffer.read(cx).snapshot(); + let buf = buffer.read(cx); + let snapshot = buf.text_snapshot(); + let file = buf.file().cloned(); let project_entity_id = project.entity_id(); entry.insert(RegisteredBuffer { snapshot, + file, last_position: None, _subscriptions: [ cx.subscribe(buffer, { @@ -848,11 +907,14 @@ impl EditPredictionStore { let project_state = self.get_or_init_project(project, cx); let registered_buffer = Self::register_buffer_impl(project_state, buffer, project, cx); - let new_snapshot = buffer.read(cx).snapshot(); + let buf = buffer.read(cx); + let new_file = buf.file().cloned(); + let new_snapshot = buf.text_snapshot(); if new_snapshot.version == registered_buffer.snapshot.version { return; } + let old_file = mem::replace(&mut registered_buffer.file, new_file.clone()); let old_snapshot = mem::replace(&mut registered_buffer.snapshot, new_snapshot.clone()); let end_edit_anchor = new_snapshot .anchored_edits_since::(&old_snapshot.version) @@ -860,20 +922,16 @@ impl EditPredictionStore { .map(|(_, range)| range.end); let events = &mut project_state.events; - if let Some(LastEvent { - new_snapshot: last_new_snapshot, - end_edit_anchor: last_end_edit_anchor, - .. - }) = project_state.last_event.as_mut() - { + let now = cx.background_executor().now(); + if let Some(last_event) = project_state.last_event.as_mut() { let is_next_snapshot_of_same_buffer = old_snapshot.remote_id() - == last_new_snapshot.remote_id() - && old_snapshot.version == last_new_snapshot.version; + == last_event.new_snapshot.remote_id() + && old_snapshot.version == last_event.new_snapshot.version; let should_coalesce = is_next_snapshot_of_same_buffer && end_edit_anchor .as_ref() - .zip(last_end_edit_anchor.as_ref()) + .zip(last_event.end_edit_anchor.as_ref()) .is_some_and(|(a, b)| { let a = a.to_point(&new_snapshot); let b = b.to_point(&new_snapshot); @@ -881,8 +939,18 @@ impl EditPredictionStore { }); if should_coalesce { - *last_end_edit_anchor = end_edit_anchor; - *last_new_snapshot = new_snapshot; + let pause_elapsed = last_event + .last_edit_time + .map(|t| now.duration_since(t) >= LAST_CHANGE_GROUPING_TIME) + .unwrap_or(false); + if pause_elapsed { + last_event.snapshot_after_last_editing_pause = + Some(last_event.new_snapshot.clone()); + } + + last_event.end_edit_anchor = end_edit_anchor; + last_event.new_snapshot = new_snapshot; + last_event.last_edit_time = Some(now); return; } } @@ -896,9 +964,13 @@ impl EditPredictionStore { } project_state.last_event = Some(LastEvent { + old_file, + new_file, old_snapshot, new_snapshot, end_edit_anchor, + snapshot_after_last_editing_pause: None, + last_edit_time: Some(now), }); } diff --git a/crates/edit_prediction/src/edit_prediction_tests.rs b/crates/edit_prediction/src/edit_prediction_tests.rs index 9e4baa78ef4564ce4348ef1b51085ba0a6abdffc..5067aa0050d7a0831ca7668d17188fa6d41637b9 100644 --- a/crates/edit_prediction/src/edit_prediction_tests.rs +++ b/crates/edit_prediction/src/edit_prediction_tests.rs @@ -304,11 +304,102 @@ async fn test_request_events(cx: &mut TestAppContext) { let prediction = prediction_task.await.unwrap().unwrap().prediction.unwrap(); assert_eq!(prediction.edits.len(), 1); + assert_eq!(prediction.edits[0].1.as_ref(), " are you?"); +} + +#[gpui::test] +async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContext) { + let (ep_store, _requests) = init_test_with_fake_client(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/root", + json!({ + "foo.md": "Hello!\n\nBye\n" + }), + ) + .await; + let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + let path = project.find_project_path(path!("root/foo.md"), cx).unwrap(); + project.open_buffer(path, cx) + }) + .await + .unwrap(); + + ep_store.update(cx, |ep_store, cx| { + ep_store.register_buffer(&buffer, &project, cx); + }); + + // First burst: insert "How" + buffer.update(cx, |buffer, cx| { + buffer.edit(vec![(7..7, "How")], None, cx); + }); + + // Simulate a pause longer than the grouping threshold (e.g. 500ms). + cx.executor().advance_clock(LAST_CHANGE_GROUPING_TIME * 2); + cx.run_until_parked(); + + // Second burst: append " are you?" immediately after "How" on the same line. + // + // Keeping both bursts on the same line ensures the existing line-span coalescing logic + // groups them into a single `LastEvent`, allowing the pause-split getter to return two diffs. + buffer.update(cx, |buffer, cx| { + buffer.edit(vec![(10..10, " are you?")], None, cx); + }); + + // A second edit shortly after the first post-pause edit ensures the last edit timestamp is + // advanced after the pause boundary is recorded, making pause-splitting deterministic. + buffer.update(cx, |buffer, cx| { + buffer.edit(vec![(19..19, "!")], None, cx); + }); + + // Without time-based splitting, there is one event. + let events = ep_store.update(cx, |ep_store, cx| { + ep_store.edit_history_for_project(&project, cx) + }); + assert_eq!(events.len(), 1); + let zeta_prompt::Event::BufferChange { diff, .. } = events[0].as_ref(); assert_eq!( - prediction.edits[0].0.to_point(&snapshot).start, - language::Point::new(1, 3) + diff.as_str(), + indoc! {" + @@ -1,3 +1,3 @@ + Hello! + - + +How are you?! + Bye + "} + ); + + // With time-based splitting, there are two distinct events. + let events = ep_store.update(cx, |ep_store, cx| { + ep_store.edit_history_for_project_with_pause_split_last_event(&project, cx) + }); + assert_eq!(events.len(), 2); + let zeta_prompt::Event::BufferChange { diff, .. } = events[0].as_ref(); + assert_eq!( + diff.as_str(), + indoc! {" + @@ -1,3 +1,3 @@ + Hello! + - + +How + Bye + "} + ); + + let zeta_prompt::Event::BufferChange { diff, .. } = events[1].as_ref(); + assert_eq!( + diff.as_str(), + indoc! {" + @@ -1,3 +1,3 @@ + Hello! + -How + +How are you?! + Bye + "} ); - assert_eq!(prediction.edits[0].1.as_ref(), " are you?"); } #[gpui::test] diff --git a/crates/edit_prediction/src/example_spec.rs b/crates/edit_prediction/src/example_spec.rs new file mode 100644 index 0000000000000000000000000000000000000000..bf221b576b890f1200c4ee3c095f73edaea71462 --- /dev/null +++ b/crates/edit_prediction/src/example_spec.rs @@ -0,0 +1,212 @@ +use serde::{Deserialize, Serialize}; +use std::{fmt::Write as _, mem, path::Path, sync::Arc}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ExampleSpec { + #[serde(default)] + pub name: String, + pub repository_url: String, + pub revision: String, + #[serde(default)] + pub uncommitted_diff: String, + pub cursor_path: Arc, + pub cursor_position: String, + pub edit_history: String, + pub expected_patch: String, +} + +const UNCOMMITTED_DIFF_HEADING: &str = "Uncommitted Diff"; +const EDIT_HISTORY_HEADING: &str = "Edit History"; +const CURSOR_POSITION_HEADING: &str = "Cursor Position"; +const EXPECTED_PATCH_HEADING: &str = "Expected Patch"; +const EXPECTED_CONTEXT_HEADING: &str = "Expected Context"; +const REPOSITORY_URL_FIELD: &str = "repository_url"; +const REVISION_FIELD: &str = "revision"; + +impl ExampleSpec { + /// Format this example spec as markdown. + pub fn to_markdown(&self) -> String { + let mut markdown = String::new(); + + _ = writeln!(markdown, "# {}", self.name); + markdown.push('\n'); + + _ = writeln!(markdown, "repository_url = {}", self.repository_url); + _ = writeln!(markdown, "revision = {}", self.revision); + markdown.push('\n'); + + if !self.uncommitted_diff.is_empty() { + _ = writeln!(markdown, "## {}", UNCOMMITTED_DIFF_HEADING); + _ = writeln!(markdown); + _ = writeln!(markdown, "```diff"); + markdown.push_str(&self.uncommitted_diff); + if !markdown.ends_with('\n') { + markdown.push('\n'); + } + _ = writeln!(markdown, "```"); + markdown.push('\n'); + } + + _ = writeln!(markdown, "## {}", EDIT_HISTORY_HEADING); + _ = writeln!(markdown); + + if self.edit_history.is_empty() { + _ = writeln!(markdown, "(No edit history)"); + _ = writeln!(markdown); + } else { + _ = writeln!(markdown, "```diff"); + markdown.push_str(&self.edit_history); + if !markdown.ends_with('\n') { + markdown.push('\n'); + } + _ = writeln!(markdown, "```"); + markdown.push('\n'); + } + + _ = writeln!(markdown, "## {}", CURSOR_POSITION_HEADING); + _ = writeln!(markdown); + _ = writeln!(markdown, "```{}", self.cursor_path.to_string_lossy()); + markdown.push_str(&self.cursor_position); + if !markdown.ends_with('\n') { + markdown.push('\n'); + } + _ = writeln!(markdown, "```"); + markdown.push('\n'); + + _ = writeln!(markdown, "## {}", EXPECTED_PATCH_HEADING); + markdown.push('\n'); + _ = writeln!(markdown, "```diff"); + markdown.push_str(&self.expected_patch); + if !markdown.ends_with('\n') { + markdown.push('\n'); + } + _ = writeln!(markdown, "```"); + markdown.push('\n'); + + markdown + } + + /// Parse an example spec from markdown. + pub fn from_markdown(name: String, input: &str) -> anyhow::Result { + use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Parser, Tag, TagEnd}; + + let parser = Parser::new(input); + + let mut spec = ExampleSpec { + name, + repository_url: String::new(), + revision: String::new(), + uncommitted_diff: String::new(), + cursor_path: Path::new("").into(), + cursor_position: String::new(), + edit_history: String::new(), + expected_patch: String::new(), + }; + + let mut text = String::new(); + let mut block_info: CowStr = "".into(); + + #[derive(PartialEq)] + enum Section { + Start, + UncommittedDiff, + EditHistory, + CursorPosition, + ExpectedExcerpts, + ExpectedPatch, + Other, + } + + let mut current_section = Section::Start; + + for event in parser { + match event { + Event::Text(line) => { + text.push_str(&line); + + if let Section::Start = current_section + && let Some((field, value)) = line.split_once('=') + { + match field.trim() { + REPOSITORY_URL_FIELD => { + spec.repository_url = value.trim().to_string(); + } + REVISION_FIELD => { + spec.revision = value.trim().to_string(); + } + _ => {} + } + } + } + Event::End(TagEnd::Heading(HeadingLevel::H2)) => { + let title = mem::take(&mut text); + current_section = if title.eq_ignore_ascii_case(UNCOMMITTED_DIFF_HEADING) { + Section::UncommittedDiff + } else if title.eq_ignore_ascii_case(EDIT_HISTORY_HEADING) { + Section::EditHistory + } else if title.eq_ignore_ascii_case(CURSOR_POSITION_HEADING) { + Section::CursorPosition + } else if title.eq_ignore_ascii_case(EXPECTED_PATCH_HEADING) { + Section::ExpectedPatch + } else if title.eq_ignore_ascii_case(EXPECTED_CONTEXT_HEADING) { + Section::ExpectedExcerpts + } else { + Section::Other + }; + } + Event::End(TagEnd::Heading(HeadingLevel::H3)) => { + mem::take(&mut text); + } + Event::End(TagEnd::Heading(HeadingLevel::H4)) => { + mem::take(&mut text); + } + Event::End(TagEnd::Heading(level)) => { + anyhow::bail!("Unexpected heading level: {level}"); + } + Event::Start(Tag::CodeBlock(kind)) => { + match kind { + CodeBlockKind::Fenced(info) => { + block_info = info; + } + CodeBlockKind::Indented => { + anyhow::bail!("Unexpected indented codeblock"); + } + }; + } + Event::Start(_) => { + text.clear(); + block_info = "".into(); + } + Event::End(TagEnd::CodeBlock) => { + let block_info = block_info.trim(); + match current_section { + Section::UncommittedDiff => { + spec.uncommitted_diff = mem::take(&mut text); + } + Section::EditHistory => { + spec.edit_history.push_str(&mem::take(&mut text)); + } + Section::CursorPosition => { + spec.cursor_path = Path::new(block_info).into(); + spec.cursor_position = mem::take(&mut text); + } + Section::ExpectedExcerpts => { + mem::take(&mut text); + } + Section::ExpectedPatch => { + spec.expected_patch = mem::take(&mut text); + } + Section::Start | Section::Other => {} + } + } + _ => {} + } + } + + if spec.cursor_path.as_ref() == Path::new("") || spec.cursor_position.is_empty() { + anyhow::bail!("Missing cursor position codeblock"); + } + + Ok(spec) + } +} diff --git a/crates/edit_prediction/src/mercury.rs b/crates/edit_prediction/src/mercury.rs index f3a3afc53fc5e175fdbda2dc6b5867da6fd38feb..ac9f8f535572dddb56ffcfde9a5f2040a65cf168 100644 --- a/crates/edit_prediction/src/mercury.rs +++ b/crates/edit_prediction/src/mercury.rs @@ -1,40 +1,34 @@ +use crate::{ + DebugEvent, EditPredictionFinishedDebugEvent, EditPredictionId, EditPredictionModelInput, + EditPredictionStartedDebugEvent, open_ai_response::text_from_response, + prediction::EditPredictionResult, +}; use anyhow::{Context as _, Result}; -use credentials_provider::CredentialsProvider; -use futures::{AsyncReadExt as _, FutureExt, future::Shared}; +use futures::AsyncReadExt as _; use gpui::{ - App, AppContext as _, Task, + App, AppContext as _, Entity, SharedString, Task, http_client::{self, AsyncBody, Method}, }; use language::{OffsetRangeExt as _, ToOffset, ToPoint as _}; +use language_model::{ApiKeyState, EnvVar, env_var}; use std::{mem, ops::Range, path::Path, sync::Arc, time::Instant}; use zeta_prompt::ZetaPromptInput; -use crate::{ - DebugEvent, EditPredictionFinishedDebugEvent, EditPredictionId, EditPredictionModelInput, - EditPredictionStartedDebugEvent, open_ai_response::text_from_response, - prediction::EditPredictionResult, -}; - const MERCURY_API_URL: &str = "https://api.inceptionlabs.ai/v1/edit/completions"; const MAX_CONTEXT_TOKENS: usize = 150; const MAX_REWRITE_TOKENS: usize = 350; pub struct Mercury { - pub api_token: Shared>>, + pub api_token: Entity, } impl Mercury { - pub fn new(cx: &App) -> Self { + pub fn new(cx: &mut App) -> Self { Mercury { - api_token: load_api_token(cx).shared(), + api_token: mercury_api_token(cx), } } - pub fn set_api_token(&mut self, api_token: Option, cx: &mut App) -> Task> { - self.api_token = Task::ready(api_token.clone()).shared(); - store_api_token_in_keychain(api_token, cx) - } - pub(crate) fn request_prediction( &self, EditPredictionModelInput { @@ -48,7 +42,10 @@ impl Mercury { }: EditPredictionModelInput, cx: &mut App, ) -> Task>> { - let Some(api_token) = self.api_token.clone().now_or_never().flatten() else { + self.api_token.update(cx, |key_state, cx| { + _ = key_state.load_if_needed(MERCURY_CREDENTIALS_URL, |s| s, cx); + }); + let Some(api_token) = self.api_token.read(cx).key(&MERCURY_CREDENTIALS_URL) else { return Task::ready(Ok(None)); }; let full_path: Arc = snapshot @@ -299,45 +296,16 @@ fn push_delimited(prompt: &mut String, delimiters: Range<&str>, cb: impl FnOnce( prompt.push_str(delimiters.end); } -pub const MERCURY_CREDENTIALS_URL: &str = "https://api.inceptionlabs.ai/v1/edit/completions"; +pub const MERCURY_CREDENTIALS_URL: SharedString = + SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions"); pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token"; +pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock = env_var!("MERCURY_AI_TOKEN"); +pub static MERCURY_API_KEY: std::sync::OnceLock> = std::sync::OnceLock::new(); -pub fn load_api_token(cx: &App) -> Task> { - if let Some(api_token) = std::env::var("MERCURY_AI_TOKEN") - .ok() - .filter(|value| !value.is_empty()) - { - return Task::ready(Some(api_token)); - } - let credentials_provider = ::global(cx); - cx.spawn(async move |cx| { - let (_, credentials) = credentials_provider - .read_credentials(MERCURY_CREDENTIALS_URL, &cx) - .await - .ok()??; - String::from_utf8(credentials).ok() - }) -} - -fn store_api_token_in_keychain(api_token: Option, cx: &App) -> Task> { - let credentials_provider = ::global(cx); - - cx.spawn(async move |cx| { - if let Some(api_token) = api_token { - credentials_provider - .write_credentials( - MERCURY_CREDENTIALS_URL, - MERCURY_CREDENTIALS_USERNAME, - api_token.as_bytes(), - cx, - ) - .await - .context("Failed to save Mercury API token to system keychain") - } else { - credentials_provider - .delete_credentials(MERCURY_CREDENTIALS_URL, cx) - .await - .context("Failed to delete Mercury API token from system keychain") - } - }) +pub fn mercury_api_token(cx: &mut App) -> Entity { + MERCURY_API_KEY + .get_or_init(|| { + cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone())) + }) + .clone() } diff --git a/crates/edit_prediction/src/sweep_ai.rs b/crates/edit_prediction/src/sweep_ai.rs index f65749ceadf6e05fc3b56838c03234b2f83dc51e..7d020c219b47aa8bcf6fb89e516b7f8ff93da497 100644 --- a/crates/edit_prediction/src/sweep_ai.rs +++ b/crates/edit_prediction/src/sweep_ai.rs @@ -1,11 +1,11 @@ -use anyhow::{Context as _, Result}; -use credentials_provider::CredentialsProvider; -use futures::{AsyncReadExt as _, FutureExt, future::Shared}; +use anyhow::Result; +use futures::AsyncReadExt as _; use gpui::{ - App, AppContext as _, Task, + App, AppContext as _, Entity, SharedString, Task, http_client::{self, AsyncBody, Method}, }; use language::{Point, ToOffset as _}; +use language_model::{ApiKeyState, EnvVar, env_var}; use lsp::DiagnosticSeverity; use serde::{Deserialize, Serialize}; use std::{ @@ -20,30 +20,28 @@ use crate::{EditPredictionId, EditPredictionModelInput, prediction::EditPredicti const SWEEP_API_URL: &str = "https://autocomplete.sweep.dev/backend/next_edit_autocomplete"; pub struct SweepAi { - pub api_token: Shared>>, + pub api_token: Entity, pub debug_info: Arc, } impl SweepAi { - pub fn new(cx: &App) -> Self { + pub fn new(cx: &mut App) -> Self { SweepAi { - api_token: load_api_token(cx).shared(), + api_token: sweep_api_token(cx), debug_info: debug_info(cx), } } - pub fn set_api_token(&mut self, api_token: Option, cx: &mut App) -> Task> { - self.api_token = Task::ready(api_token.clone()).shared(); - store_api_token_in_keychain(api_token, cx) - } - pub fn request_prediction_with_sweep( &self, inputs: EditPredictionModelInput, cx: &mut App, ) -> Task>> { let debug_info = self.debug_info.clone(); - let Some(api_token) = self.api_token.clone().now_or_never().flatten() else { + self.api_token.update(cx, |key_state, cx| { + _ = key_state.load_if_needed(SWEEP_CREDENTIALS_URL, |s| s, cx); + }); + let Some(api_token) = self.api_token.read(cx).key(&SWEEP_CREDENTIALS_URL) else { return Task::ready(Ok(None)); }; let full_path: Arc = inputs @@ -270,47 +268,18 @@ impl SweepAi { } } -pub const SWEEP_CREDENTIALS_URL: &str = "https://autocomplete.sweep.dev"; +pub const SWEEP_CREDENTIALS_URL: SharedString = + SharedString::new_static("https://autocomplete.sweep.dev"); pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token"; +pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock = env_var!("SWEEP_AI_TOKEN"); +pub static SWEEP_API_KEY: std::sync::OnceLock> = std::sync::OnceLock::new(); -pub fn load_api_token(cx: &App) -> Task> { - if let Some(api_token) = std::env::var("SWEEP_AI_TOKEN") - .ok() - .filter(|value| !value.is_empty()) - { - return Task::ready(Some(api_token)); - } - let credentials_provider = ::global(cx); - cx.spawn(async move |cx| { - let (_, credentials) = credentials_provider - .read_credentials(SWEEP_CREDENTIALS_URL, &cx) - .await - .ok()??; - String::from_utf8(credentials).ok() - }) -} - -fn store_api_token_in_keychain(api_token: Option, cx: &App) -> Task> { - let credentials_provider = ::global(cx); - - cx.spawn(async move |cx| { - if let Some(api_token) = api_token { - credentials_provider - .write_credentials( - SWEEP_CREDENTIALS_URL, - SWEEP_CREDENTIALS_USERNAME, - api_token.as_bytes(), - cx, - ) - .await - .context("Failed to save Sweep API token to system keychain") - } else { - credentials_provider - .delete_credentials(SWEEP_CREDENTIALS_URL, cx) - .await - .context("Failed to delete Sweep API token from system keychain") - } - }) +pub fn sweep_api_token(cx: &mut App) -> Entity { + SWEEP_API_KEY + .get_or_init(|| { + cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone())) + }) + .clone() } #[derive(Debug, Clone, Serialize)] diff --git a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs index 6dcf7092240de64381ded611b47c2dd5940d6770..0a87ca661435de4d22e6f258c30ff406f0deecc2 100644 --- a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs +++ b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs @@ -100,7 +100,7 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate { ) -> bool { let store = self.store.read(cx); if store.edit_prediction_model == EditPredictionModel::Sweep { - store.has_sweep_api_token() + store.has_sweep_api_token(cx) } else { true } diff --git a/crates/edit_prediction/src/zeta2.rs b/crates/edit_prediction/src/zeta2.rs index 8586e6caaea1fdc9c865ddba8894f680d766b4a9..9706e2b9ecd03f6e8ba592210722725f420643d3 100644 --- a/crates/edit_prediction/src/zeta2.rs +++ b/crates/edit_prediction/src/zeta2.rs @@ -228,13 +228,16 @@ pub fn zeta2_prompt_input( } #[cfg(feature = "cli-support")] -pub fn zeta2_output_for_patch(input: &zeta_prompt::ZetaPromptInput, patch: &str) -> String { - eprintln!("{}", patch); - eprintln!("---------------------"); - eprintln!("{}", input.cursor_excerpt); - crate::udiff::apply_diff_to_string( - patch, - &input.cursor_excerpt[input.editable_range_in_excerpt.clone()], - ) - .unwrap() +pub fn zeta2_output_for_patch(input: &zeta_prompt::ZetaPromptInput, patch: &str) -> Result { + let text = &input.cursor_excerpt; + let editable_region = input.editable_range_in_excerpt.clone(); + let old_prefix = &text[..editable_region.start]; + let old_suffix = &text[editable_region.end..]; + + let new = crate::udiff::apply_diff_to_string(patch, text)?; + if !new.starts_with(old_prefix) || !new.ends_with(old_suffix) { + anyhow::bail!("Patch shouldn't affect text outside of editable region"); + } + + Ok(new[editable_region.start..new.len() - old_suffix.len()].to_string()) } diff --git a/crates/edit_prediction_cli/Cargo.toml b/crates/edit_prediction_cli/Cargo.toml index 811808c72304f4c11a9858e61395e46024b83f1e..b6bace2a2c080626126af96f9ef51e435d6ab8fa 100644 --- a/crates/edit_prediction_cli/Cargo.toml +++ b/crates/edit_prediction_cli/Cargo.toml @@ -40,7 +40,6 @@ node_runtime.workspace = true paths.workspace = true project.workspace = true prompt_store.workspace = true -pulldown-cmark.workspace = true release_channel.workspace = true reqwest_client.workspace = true serde.workspace = true diff --git a/crates/edit_prediction_cli/src/distill.rs b/crates/edit_prediction_cli/src/distill.rs index 495b3cd88cbd05ad1917517580b913aacf4fb107..abfe178ae61b6da522f43c93d40b6000800d0e4d 100644 --- a/crates/edit_prediction_cli/src/distill.rs +++ b/crates/edit_prediction_cli/src/distill.rs @@ -1,14 +1,22 @@ +use anyhow::{Result, anyhow}; use std::mem; use crate::example::Example; -pub async fn run_distill(example: &mut Example) { - let [prediction]: [_; 1] = mem::take(&mut example.predictions) - .try_into() - .expect("Run predict first with a single repetition"); +pub async fn run_distill(example: &mut Example) -> Result<()> { + let [prediction]: [_; 1] = + mem::take(&mut example.predictions) + .try_into() + .map_err(|preds: Vec<_>| { + anyhow!( + "Example has {} predictions, but it should have exactly one", + preds.len() + ) + })?; - example.expected_patch = prediction.actual_patch; + example.spec.expected_patch = prediction.actual_patch; example.prompt = None; example.predictions = Vec::new(); example.score = Vec::new(); + Ok(()) } diff --git a/crates/edit_prediction_cli/src/example.rs b/crates/edit_prediction_cli/src/example.rs index 9499aae0c1ebce7eeca3ef05fedbcf09c960e131..e37619bf224b3fa506516714856cfbc5024ece14 100644 --- a/crates/edit_prediction_cli/src/example.rs +++ b/crates/edit_prediction_cli/src/example.rs @@ -1,6 +1,7 @@ use crate::{PredictionProvider, PromptFormat, metrics::ClassificationMetrics}; use anyhow::{Context as _, Result}; use collections::HashMap; +use edit_prediction::example_spec::ExampleSpec; use edit_prediction::udiff::OpenedBuffers; use gpui::Entity; use http_client::Url; @@ -11,23 +12,14 @@ use std::sync::Arc; use std::{ borrow::Cow, io::{Read, Write}, - mem, path::{Path, PathBuf}, }; use zeta_prompt::RelatedFile; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Example { - #[serde(default)] - pub name: String, - pub repository_url: String, - pub revision: String, - #[serde(default)] - pub uncommitted_diff: String, - pub cursor_path: Arc, - pub cursor_position: String, - pub edit_history: String, - pub expected_patch: String, + #[serde(flatten)] + pub spec: ExampleSpec, /// The full content of the file where an edit is being predicted, and the /// actual cursor offset. @@ -101,8 +93,9 @@ pub struct ExampleScore { impl Example { pub fn repo_name(&self) -> Result<(Cow<'_, str>, Cow<'_, str>)> { // git@github.com:owner/repo.git - if self.repository_url.contains('@') { + if self.spec.repository_url.contains('@') { let (owner, repo) = self + .spec .repository_url .split_once(':') .context("expected : in git url")? @@ -115,7 +108,7 @@ impl Example { )) // http://github.com/owner/repo.git } else { - let url = Url::parse(&self.repository_url)?; + let url = Url::parse(&self.spec.repository_url)?; let mut segments = url.path_segments().context("empty http url")?; let owner = segments .next() @@ -171,8 +164,8 @@ pub fn read_examples(inputs: &[PathBuf]) -> Vec { serde_json::from_str::(&content).unwrap_or_else(|error| { panic!("Failed to parse example file: {}\n{error}", path.display()) }); - if example.name.is_empty() { - example.name = filename; + if example.spec.name.is_empty() { + example.spec.name = filename; } examples.push(example); } @@ -189,8 +182,8 @@ pub fn read_examples(inputs: &[PathBuf]) -> Vec { line_ix + 1 ) }); - if example.name.is_empty() { - example.name = format!("{filename}-{line_ix}") + if example.spec.name.is_empty() { + example.spec.name = format!("{filename}-{line_ix}") } example }) @@ -225,9 +218,10 @@ pub fn write_examples(examples: &[Example], output_path: Option<&PathBuf>) { pub fn sort_examples_by_repo_and_rev(examples: &mut [Example]) { examples.sort_by(|a, b| { - a.repository_url - .cmp(&b.repository_url) - .then(b.revision.cmp(&a.revision)) + a.spec + .repository_url + .cmp(&b.spec.repository_url) + .then(b.spec.revision.cmp(&a.spec.revision)) }); } @@ -235,145 +229,22 @@ pub fn group_examples_by_repo(examples: &mut [Example]) -> Vec let mut examples_by_repo = HashMap::default(); for example in examples.iter_mut() { examples_by_repo - .entry(example.repository_url.clone()) + .entry(example.spec.repository_url.clone()) .or_insert_with(Vec::new) .push(example); } examples_by_repo.into_values().collect() } -fn parse_markdown_example(id: String, input: &str) -> Result { - use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Parser, Tag, TagEnd}; - - const UNCOMMITTED_DIFF_HEADING: &str = "Uncommitted Diff"; - const EDIT_HISTORY_HEADING: &str = "Edit History"; - const CURSOR_POSITION_HEADING: &str = "Cursor Position"; - const EXPECTED_PATCH_HEADING: &str = "Expected Patch"; - const EXPECTED_CONTEXT_HEADING: &str = "Expected Context"; - const REPOSITORY_URL_FIELD: &str = "repository_url"; - const REVISION_FIELD: &str = "revision"; - - let parser = Parser::new(input); - - let mut example = Example { - name: id, - repository_url: String::new(), - revision: String::new(), - uncommitted_diff: String::new(), - cursor_path: PathBuf::new().into(), - cursor_position: String::new(), - edit_history: String::new(), - expected_patch: String::new(), +fn parse_markdown_example(name: String, input: &str) -> Result { + let spec = ExampleSpec::from_markdown(name, input)?; + Ok(Example { + spec, buffer: None, context: None, prompt: None, predictions: Vec::new(), score: Vec::new(), state: None, - }; - - let mut text = String::new(); - let mut block_info: CowStr = "".into(); - - #[derive(PartialEq)] - enum Section { - Start, - UncommittedDiff, - EditHistory, - CursorPosition, - ExpectedExcerpts, - ExpectedPatch, - Other, - } - - let mut current_section = Section::Start; - - for event in parser { - match event { - Event::Text(line) => { - text.push_str(&line); - - if let Section::Start = current_section - && let Some((field, value)) = line.split_once('=') - { - match field.trim() { - REPOSITORY_URL_FIELD => { - example.repository_url = value.trim().to_string(); - } - REVISION_FIELD => { - example.revision = value.trim().to_string(); - } - _ => {} - } - } - } - Event::End(TagEnd::Heading(HeadingLevel::H2)) => { - let title = mem::take(&mut text); - current_section = if title.eq_ignore_ascii_case(UNCOMMITTED_DIFF_HEADING) { - Section::UncommittedDiff - } else if title.eq_ignore_ascii_case(EDIT_HISTORY_HEADING) { - Section::EditHistory - } else if title.eq_ignore_ascii_case(CURSOR_POSITION_HEADING) { - Section::CursorPosition - } else if title.eq_ignore_ascii_case(EXPECTED_PATCH_HEADING) { - Section::ExpectedPatch - } else if title.eq_ignore_ascii_case(EXPECTED_CONTEXT_HEADING) { - Section::ExpectedExcerpts - } else { - Section::Other - }; - } - Event::End(TagEnd::Heading(HeadingLevel::H3)) => { - mem::take(&mut text); - } - Event::End(TagEnd::Heading(HeadingLevel::H4)) => { - mem::take(&mut text); - } - Event::End(TagEnd::Heading(level)) => { - anyhow::bail!("Unexpected heading level: {level}"); - } - Event::Start(Tag::CodeBlock(kind)) => { - match kind { - CodeBlockKind::Fenced(info) => { - block_info = info; - } - CodeBlockKind::Indented => { - anyhow::bail!("Unexpected indented codeblock"); - } - }; - } - Event::Start(_) => { - text.clear(); - block_info = "".into(); - } - Event::End(TagEnd::CodeBlock) => { - let block_info = block_info.trim(); - match current_section { - Section::UncommittedDiff => { - example.uncommitted_diff = mem::take(&mut text); - } - Section::EditHistory => { - example.edit_history.push_str(&mem::take(&mut text)); - } - Section::CursorPosition => { - example.cursor_path = Path::new(block_info).into(); - example.cursor_position = mem::take(&mut text); - } - Section::ExpectedExcerpts => { - mem::take(&mut text); - } - Section::ExpectedPatch => { - example.expected_patch = mem::take(&mut text); - } - Section::Start | Section::Other => {} - } - } - _ => {} - } - } - if example.cursor_path.as_ref() == Path::new("") || example.cursor_position.is_empty() { - anyhow::bail!("Missing cursor position codeblock"); - } - - Ok(example) + }) } diff --git a/crates/edit_prediction_cli/src/format_prompt.rs b/crates/edit_prediction_cli/src/format_prompt.rs index 017e11a54c77e06bde7b74ed3f924692e33cd480..f543d0799b379403f0caa980df76954649e1aceb 100644 --- a/crates/edit_prediction_cli/src/format_prompt.rs +++ b/crates/edit_prediction_cli/src/format_prompt.rs @@ -6,6 +6,7 @@ use crate::{ progress::{Progress, Step}, retrieve_context::run_context_retrieval, }; +use anyhow::{Context as _, Result, ensure}; use edit_prediction::{ EditPredictionStore, zeta2::{zeta2_output_for_patch, zeta2_prompt_input}, @@ -19,46 +20,51 @@ pub async fn run_format_prompt( prompt_format: PromptFormat, app_state: Arc, mut cx: AsyncApp, -) { - run_context_retrieval(example, app_state.clone(), cx.clone()).await; +) -> Result<()> { + run_context_retrieval(example, app_state.clone(), cx.clone()).await?; - let _step_progress = Progress::global().start(Step::FormatPrompt, &example.name); + let _step_progress = Progress::global().start(Step::FormatPrompt, &example.spec.name); match prompt_format { PromptFormat::Teacher => { let prompt = TeacherPrompt::format_prompt(example); example.prompt = Some(ExamplePrompt { input: prompt, - expected_output: example.expected_patch.clone(), // TODO + expected_output: example.spec.expected_patch.clone(), // TODO format: prompt_format, }); } PromptFormat::Zeta2 => { - run_load_project(example, app_state, cx.clone()).await; + run_load_project(example, app_state, cx.clone()).await?; - let ep_store = cx - .update(|cx| EditPredictionStore::try_global(cx).unwrap()) - .unwrap(); + let ep_store = cx.update(|cx| { + EditPredictionStore::try_global(cx).context("EditPredictionStore not initialized") + })??; - let state = example.state.as_ref().unwrap(); - let snapshot = state - .buffer - .read_with(&cx, |buffer, _| buffer.snapshot()) - .unwrap(); + let state = example.state.as_ref().context("state must be set")?; + let snapshot = state.buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; let project = state.project.clone(); - let (_, input) = ep_store - .update(&mut cx, |ep_store, _cx| { - zeta2_prompt_input( - &snapshot, - example.context.as_ref().unwrap().files.clone(), - ep_store.edit_history_for_project(&project), - example.cursor_path.clone(), - example.buffer.as_ref().unwrap().cursor_offset, - ) - }) - .unwrap(); + let (_, input) = ep_store.update(&mut cx, |ep_store, cx| { + anyhow::Ok(zeta2_prompt_input( + &snapshot, + example + .context + .as_ref() + .context("context must be set")? + .files + .clone(), + ep_store.edit_history_for_project(&project, cx), + example.spec.cursor_path.clone(), + example + .buffer + .as_ref() + .context("buffer must be set")? + .cursor_offset, + )) + })??; let prompt = format_zeta_prompt(&input); - let expected_output = zeta2_output_for_patch(&input, &example.expected_patch.clone()); + let expected_output = + zeta2_output_for_patch(&input, &example.spec.expected_patch.clone())?; example.prompt = Some(ExamplePrompt { input: prompt, expected_output, @@ -66,6 +72,7 @@ pub async fn run_format_prompt( }); } }; + Ok(()) } pub struct TeacherPrompt; @@ -79,7 +86,7 @@ impl TeacherPrompt { const MAX_HISTORY_LINES: usize = 128; pub fn format_prompt(example: &Example) -> String { - let edit_history = Self::format_edit_history(&example.edit_history); + let edit_history = Self::format_edit_history(&example.spec.edit_history); let context = Self::format_context(example); let editable_region = Self::format_editable_region(example); @@ -91,7 +98,7 @@ impl TeacherPrompt { prompt } - pub fn parse(example: &Example, response: &str) -> String { + pub fn parse(example: &Example, response: &str) -> Result { // Ideally, we should always be able to find cursor position in the retrieved context. // In reality, sometimes we don't find it for these reasons: // 1. `example.cursor_position` contains _more_ context than included in the retrieved context @@ -102,7 +109,7 @@ impl TeacherPrompt { let cursor_file = &example .buffer .as_ref() - .expect("`buffer` should be filled in in the context collection step") + .context("`buffer` should be filled in in the context collection step")? .content; // Extract updated (new) editable region from the model response @@ -111,9 +118,10 @@ impl TeacherPrompt { // Reconstruct old editable region we sent to the model let old_editable_region = Self::format_editable_region(example); let old_editable_region = Self::extract_editable_region(&old_editable_region); - if !cursor_file.contains(&old_editable_region) { - panic!("Something's wrong: editable_region is not found in the cursor file") - } + ensure!( + cursor_file.contains(&old_editable_region), + "Something's wrong: editable_region is not found in the cursor file" + ); // Apply editable region to a larger context and compute diff. // This is needed to get a better context lines around the editable region @@ -124,11 +132,11 @@ impl TeacherPrompt { --- a/{path} +++ b/{path} {diff}", - path = example.cursor_path.to_string_lossy(), + path = example.spec.cursor_path.to_string_lossy(), diff = diff, }; - diff + Ok(diff) } fn format_edit_history(edit_history: &str) -> String { @@ -152,9 +160,7 @@ impl TeacherPrompt { } fn format_context(example: &Example) -> String { - if example.context.is_none() { - panic!("Missing context retriever step"); - } + assert!(example.context.is_some(), "Missing context retriever step"); let mut prompt = String::new(); zeta_prompt::write_related_files(&mut prompt, &example.context.as_ref().unwrap().files); @@ -165,13 +171,13 @@ impl TeacherPrompt { fn format_editable_region(example: &Example) -> String { let mut result = String::new(); - let path_str = example.cursor_path.to_string_lossy(); + let path_str = example.spec.cursor_path.to_string_lossy(); result.push_str(&format!("`````path=\"{path_str}\"\n")); result.push_str(Self::EDITABLE_REGION_START); // TODO: control number of lines around cursor - result.push_str(&example.cursor_position); - if !example.cursor_position.ends_with('\n') { + result.push_str(&example.spec.cursor_position); + if !example.spec.cursor_position.ends_with('\n') { result.push('\n'); } diff --git a/crates/edit_prediction_cli/src/load_project.rs b/crates/edit_prediction_cli/src/load_project.rs index 4d98ae9f3b85f4e6253d9ead4d846ed3d9deee89..38f114d726d3626fac89982b7f3a98c55e92ac07 100644 --- a/crates/edit_prediction_cli/src/load_project.rs +++ b/crates/edit_prediction_cli/src/load_project.rs @@ -4,7 +4,7 @@ use crate::{ paths::{REPOS_DIR, WORKTREES_DIR}, progress::{InfoStyle, Progress, Step, StepProgress}, }; -use anyhow::{Result, anyhow}; +use anyhow::{Context as _, Result}; use collections::HashMap; use edit_prediction::EditPredictionStore; use edit_prediction::udiff::OpenedBuffers; @@ -25,38 +25,38 @@ use std::{ use util::{paths::PathStyle, rel_path::RelPath}; use zeta_prompt::CURSOR_MARKER; -pub async fn run_load_project(example: &mut Example, app_state: Arc, mut cx: AsyncApp) { +pub async fn run_load_project( + example: &mut Example, + app_state: Arc, + mut cx: AsyncApp, +) -> Result<()> { if example.state.is_some() { - return; + return Ok(()); } - let progress = Progress::global().start(Step::LoadProject, &example.name); - - let project = setup_project(example, &app_state, &progress, &mut cx).await; - - let _open_buffers = apply_edit_history(example, &project, &mut cx) - .await - .unwrap(); - - let (buffer, cursor_position) = cursor_position(example, &project, &mut cx).await; - let (example_buffer, language_name) = buffer - .read_with(&cx, |buffer, _cx| { - let cursor_point = cursor_position.to_point(&buffer); - let language_name = buffer - .language() - .map(|l| l.name().to_string()) - .unwrap_or_else(|| "Unknown".to_string()); - ( - ExampleBuffer { - content: buffer.text(), - cursor_row: cursor_point.row, - cursor_column: cursor_point.column, - cursor_offset: cursor_position.to_offset(&buffer), - }, - language_name, - ) - }) - .unwrap(); + let progress = Progress::global().start(Step::LoadProject, &example.spec.name); + + let project = setup_project(example, &app_state, &progress, &mut cx).await?; + + let _open_buffers = apply_edit_history(example, &project, &mut cx).await?; + + let (buffer, cursor_position) = cursor_position(example, &project, &mut cx).await?; + let (example_buffer, language_name) = buffer.read_with(&cx, |buffer, _cx| { + let cursor_point = cursor_position.to_point(&buffer); + let language_name = buffer + .language() + .map(|l| l.name().to_string()) + .unwrap_or_else(|| "Unknown".to_string()); + ( + ExampleBuffer { + content: buffer.text(), + cursor_row: cursor_point.row, + cursor_column: cursor_point.column, + cursor_offset: cursor_position.to_offset(&buffer), + }, + language_name, + ) + })?; progress.set_info(language_name, InfoStyle::Normal); @@ -67,34 +67,34 @@ pub async fn run_load_project(example: &mut Example, app_state: Arc, cursor_position, _open_buffers, }); + Ok(()) } async fn cursor_position( example: &Example, project: &Entity, cx: &mut AsyncApp, -) -> (Entity, Anchor) { - let language_registry = project - .read_with(cx, |project, _| project.languages().clone()) - .unwrap(); +) -> Result<(Entity, Anchor)> { + let language_registry = project.read_with(cx, |project, _| project.languages().clone())?; let result = language_registry - .load_language_for_file_path(&example.cursor_path) + .load_language_for_file_path(&example.spec.cursor_path) .await; if let Err(error) = result && !error.is::() { - panic!("Failed to load language for file path: {}", error); + return Err(error); } - let worktree = project - .read_with(cx, |project, cx| { - project.visible_worktrees(cx).next().unwrap() - }) - .unwrap(); + let worktree = project.read_with(cx, |project, cx| { + project + .visible_worktrees(cx) + .next() + .context("No visible worktrees") + })??; - let cursor_path = RelPath::new(&example.cursor_path, PathStyle::Posix) - .unwrap() + let cursor_path = RelPath::new(&example.spec.cursor_path, PathStyle::Posix) + .context("Failed to create RelPath")? .into_arc(); let cursor_buffer = project .update(cx, |project, cx| { @@ -105,16 +105,14 @@ async fn cursor_position( }, cx, ) - }) - .unwrap() - .await - .unwrap(); + })? + .await?; let cursor_offset_within_excerpt = example + .spec .cursor_position .find(CURSOR_MARKER) - .ok_or_else(|| anyhow!("missing cursor marker")) - .unwrap(); - let mut cursor_excerpt = example.cursor_position.clone(); + .context("missing cursor marker")?; + let mut cursor_excerpt = example.spec.cursor_position.clone(); cursor_excerpt.replace_range( cursor_offset_within_excerpt..(cursor_offset_within_excerpt + CURSOR_MARKER.len()), "", @@ -123,22 +121,25 @@ async fn cursor_position( let text = buffer.text(); let mut matches = text.match_indices(&cursor_excerpt); - let (excerpt_offset, _) = matches.next().unwrap_or_else(|| { - panic!( + let (excerpt_offset, _) = matches.next().with_context(|| { + format!( "\nExcerpt:\n\n{cursor_excerpt}\nBuffer text:\n{text}\n.Example: {}\nCursor excerpt did not exist in buffer.", - example.name - ); - }); - assert!(matches.next().is_none(), "More than one cursor position match found for {}", &example.name); - excerpt_offset - }).unwrap(); + example.spec.name + ) + })?; + anyhow::ensure!( + matches.next().is_none(), + "More than one cursor position match found for {}", + &example.spec.name + ); + Ok(excerpt_offset) + })??; let cursor_offset = excerpt_offset + cursor_offset_within_excerpt; - let cursor_anchor = cursor_buffer - .read_with(cx, |buffer, _| buffer.anchor_after(cursor_offset)) - .unwrap(); + let cursor_anchor = + cursor_buffer.read_with(cx, |buffer, _| buffer.anchor_after(cursor_offset))?; - (cursor_buffer, cursor_anchor) + Ok((cursor_buffer, cursor_anchor)) } async fn setup_project( @@ -146,67 +147,54 @@ async fn setup_project( app_state: &Arc, step_progress: &StepProgress, cx: &mut AsyncApp, -) -> Entity { +) -> Result> { let ep_store = cx - .update(|cx| EditPredictionStore::try_global(cx).unwrap()) - .unwrap(); - - let worktree_path = setup_worktree(example, step_progress).await; - - if let Some(project) = app_state.project_cache.get(&example.repository_url) { - ep_store - .update(cx, |ep_store, _| { - ep_store.clear_history_for_project(&project); - }) - .unwrap(); - let buffer_store = project - .read_with(cx, |project, _| project.buffer_store().clone()) - .unwrap(); - let buffers = buffer_store - .read_with(cx, |buffer_store, _| { - buffer_store.buffers().collect::>() - }) - .unwrap(); + .update(|cx| EditPredictionStore::try_global(cx))? + .context("Store should be initialized at init")?; + + let worktree_path = setup_worktree(example, step_progress).await?; + + if let Some(project) = app_state.project_cache.get(&example.spec.repository_url) { + ep_store.update(cx, |ep_store, _| { + ep_store.clear_history_for_project(&project); + })?; + let buffer_store = project.read_with(cx, |project, _| project.buffer_store().clone())?; + let buffers = buffer_store.read_with(cx, |buffer_store, _| { + buffer_store.buffers().collect::>() + })?; for buffer in buffers { buffer - .update(cx, |buffer, cx| buffer.reload(cx)) - .unwrap() + .update(cx, |buffer, cx| buffer.reload(cx))? .await .ok(); } - return project; + return Ok(project); } - let project = cx - .update(|cx| { - Project::local( - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - None, - cx, - ) - }) - .unwrap(); + let project = cx.update(|cx| { + Project::local( + app_state.client.clone(), + app_state.node_runtime.clone(), + app_state.user_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + None, + cx, + ) + })?; project .update(cx, |project, cx| { project.disable_worktree_scanner(cx); project.create_worktree(&worktree_path, true, cx) - }) - .unwrap() - .await - .unwrap(); + })? + .await?; app_state .project_cache - .insert(example.repository_url.clone(), project.clone()); + .insert(example.spec.repository_url.clone(), project.clone()); - let buffer_store = project - .read_with(cx, |project, _| project.buffer_store().clone()) - .unwrap(); + let buffer_store = project.read_with(cx, |project, _| project.buffer_store().clone())?; cx.subscribe(&buffer_store, { let project = project.clone(); move |_, event, cx| match event { @@ -215,15 +203,14 @@ async fn setup_project( } _ => {} } - }) - .unwrap() + })? .detach(); - project + Ok(project) } -async fn setup_worktree(example: &Example, step_progress: &StepProgress) -> PathBuf { - let (repo_owner, repo_name) = example.repo_name().expect("failed to get repo name"); +async fn setup_worktree(example: &Example, step_progress: &StepProgress) -> Result { + let (repo_owner, repo_name) = example.repo_name().context("failed to get repo name")?; let repo_dir = REPOS_DIR.join(repo_owner.as_ref()).join(repo_name.as_ref()); let worktree_path = WORKTREES_DIR .join(repo_owner.as_ref()) @@ -232,20 +219,22 @@ async fn setup_worktree(example: &Example, step_progress: &StepProgress) -> Path if !repo_dir.is_dir() { step_progress.set_substatus(format!("cloning {}", repo_name)); - fs::create_dir_all(&repo_dir).unwrap(); - run_git(&repo_dir, &["init"]).await.unwrap(); + fs::create_dir_all(&repo_dir)?; + run_git(&repo_dir, &["init"]).await?; run_git( &repo_dir, - &["remote", "add", "origin", &example.repository_url], + &["remote", "add", "origin", &example.spec.repository_url], ) - .await - .unwrap(); + .await?; } // Resolve the example to a revision, fetching it if needed. let revision = run_git( &repo_dir, - &["rev-parse", &format!("{}^{{commit}}", example.revision)], + &[ + "rev-parse", + &format!("{}^{{commit}}", example.spec.revision), + ], ) .await; let revision = if let Ok(revision) = revision { @@ -254,39 +243,30 @@ async fn setup_worktree(example: &Example, step_progress: &StepProgress) -> Path step_progress.set_substatus("fetching"); if run_git( &repo_dir, - &["fetch", "--depth", "1", "origin", &example.revision], + &["fetch", "--depth", "1", "origin", &example.spec.revision], ) .await .is_err() { - run_git(&repo_dir, &["fetch", "origin"]).await.unwrap(); + run_git(&repo_dir, &["fetch", "origin"]).await?; } - let revision = run_git(&repo_dir, &["rev-parse", "FETCH_HEAD"]) - .await - .unwrap(); + let revision = run_git(&repo_dir, &["rev-parse", "FETCH_HEAD"]).await?; revision }; // Create the worktree for this example if needed. step_progress.set_substatus("preparing worktree"); if worktree_path.is_dir() { - run_git(&worktree_path, &["clean", "--force", "-d"]) - .await - .unwrap(); - run_git(&worktree_path, &["reset", "--hard", "HEAD"]) - .await - .unwrap(); - run_git(&worktree_path, &["checkout", revision.as_str()]) - .await - .unwrap(); + run_git(&worktree_path, &["clean", "--force", "-d"]).await?; + run_git(&worktree_path, &["reset", "--hard", "HEAD"]).await?; + run_git(&worktree_path, &["checkout", revision.as_str()]).await?; } else { let worktree_path_string = worktree_path.to_string_lossy(); run_git( &repo_dir, - &["branch", "-f", &example.name, revision.as_str()], + &["branch", "-f", &example.spec.name, revision.as_str()], ) - .await - .unwrap(); + .await?; run_git( &repo_dir, &[ @@ -294,45 +274,41 @@ async fn setup_worktree(example: &Example, step_progress: &StepProgress) -> Path "add", "-f", &worktree_path_string, - &example.name, + &example.spec.name, ], ) - .await - .unwrap(); + .await?; } drop(repo_lock); // Apply the uncommitted diff for this example. - if !example.uncommitted_diff.is_empty() { + if !example.spec.uncommitted_diff.is_empty() { step_progress.set_substatus("applying diff"); let mut apply_process = smol::process::Command::new("git") .current_dir(&worktree_path) .args(&["apply", "-"]) .stdin(std::process::Stdio::piped()) - .spawn() - .unwrap(); + .spawn()?; - let mut stdin = apply_process.stdin.take().unwrap(); + let mut stdin = apply_process.stdin.take().context("Failed to get stdin")?; stdin - .write_all(example.uncommitted_diff.as_bytes()) - .await - .unwrap(); - stdin.close().await.unwrap(); + .write_all(example.spec.uncommitted_diff.as_bytes()) + .await?; + stdin.close().await?; drop(stdin); - let apply_result = apply_process.output().await.unwrap(); - if !apply_result.status.success() { - panic!( - "Failed to apply uncommitted diff patch with status: {}\nstderr:\n{}\nstdout:\n{}", - apply_result.status, - String::from_utf8_lossy(&apply_result.stderr), - String::from_utf8_lossy(&apply_result.stdout), - ); - } + let apply_result = apply_process.output().await?; + anyhow::ensure!( + apply_result.status.success(), + "Failed to apply uncommitted diff patch with status: {}\nstderr:\n{}\nstdout:\n{}", + apply_result.status, + String::from_utf8_lossy(&apply_result.stderr), + String::from_utf8_lossy(&apply_result.stdout), + ); } step_progress.clear_substatus(); - worktree_path + Ok(worktree_path) } async fn apply_edit_history( @@ -340,7 +316,7 @@ async fn apply_edit_history( project: &Entity, cx: &mut AsyncApp, ) -> Result { - edit_prediction::udiff::apply_diff(&example.edit_history, project, cx).await + edit_prediction::udiff::apply_diff(&example.spec.edit_history, project, cx).await } thread_local! { diff --git a/crates/edit_prediction_cli/src/main.rs b/crates/edit_prediction_cli/src/main.rs index 075f8862e6f86276a0df550c6d27f8c15a5d1293..dce0fbbed57dbc4b18faf93787cfb8f2341a126a 100644 --- a/crates/edit_prediction_cli/src/main.rs +++ b/crates/edit_prediction_cli/src/main.rs @@ -16,12 +16,14 @@ use edit_prediction::EditPredictionStore; use gpui::Application; use reqwest_client::ReqwestClient; use serde::{Deserialize, Serialize}; +use std::fmt::Display; use std::{path::PathBuf, sync::Arc}; use crate::distill::run_distill; use crate::example::{group_examples_by_repo, read_examples, write_examples}; use crate::format_prompt::run_format_prompt; use crate::load_project::run_load_project; +use crate::paths::FAILED_EXAMPLES_DIR; use crate::predict::run_prediction; use crate::progress::Progress; use crate::retrieve_context::run_context_retrieval; @@ -42,6 +44,8 @@ struct EpArgs { output: Option, #[arg(long, short, global = true)] in_place: bool, + #[arg(long, short, global = true)] + failfast: bool, } #[derive(Subcommand, Debug)] @@ -67,6 +71,58 @@ enum Command { Clean, } +impl Display for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Command::ParseExample => write!(f, "parse-example"), + Command::LoadProject => write!(f, "load-project"), + Command::Context => write!(f, "context"), + Command::FormatPrompt(format_prompt_args) => write!( + f, + "format-prompt --prompt-format={}", + format_prompt_args + .prompt_format + .to_possible_value() + .unwrap() + .get_name() + ), + Command::Predict(predict_args) => { + write!( + f, + "predict --provider={:?}", + predict_args + .provider + .to_possible_value() + .unwrap() + .get_name() + ) + } + Command::Score(predict_args) => { + write!( + f, + "score --provider={:?}", + predict_args + .provider + .to_possible_value() + .unwrap() + .get_name() + ) + } + Command::Distill => write!(f, "distill"), + Command::Eval(predict_args) => write!( + f, + "eval --provider={:?}", + predict_args + .provider + .to_possible_value() + .unwrap() + .get_name() + ), + Command::Clean => write!(f, "clean"), + } + } +} + #[derive(Debug, Args)] struct FormatPromptArgs { #[clap(long)] @@ -145,71 +201,140 @@ fn main() { EditPredictionStore::global(&app_state.client, &app_state.user_store, cx); cx.spawn(async move |cx| { - if let Command::Predict(args) = &command { - predict::sync_batches(&args.provider).await - }; - - let total_examples = examples.len(); - Progress::global().set_total_examples(total_examples); - - let mut grouped_examples = group_examples_by_repo(&mut examples); - let example_batches = grouped_examples.chunks_mut(args.max_parallelism); - - for example_batch in example_batches { - let futures = example_batch.into_iter().map(|repo_examples| async { - for example in repo_examples.iter_mut() { - match &command { - Command::ParseExample => {} - Command::LoadProject => { - run_load_project(example, app_state.clone(), cx.clone()).await; - } - Command::Context => { - run_context_retrieval(example, app_state.clone(), cx.clone()).await; - } - Command::FormatPrompt(args) => { - run_format_prompt( - example, - args.prompt_format, - app_state.clone(), - cx.clone(), - ) - .await; - } - Command::Predict(args) => { - run_prediction( - example, - Some(args.provider), - args.repetitions, - app_state.clone(), - cx.clone(), - ) - .await; - } - Command::Distill => { - run_distill(example).await; - } - Command::Score(args) | Command::Eval(args) => { - run_scoring(example, &args, app_state.clone(), cx.clone()).await; + let result = async { + if let Command::Predict(args) = &command { + predict::sync_batches(&args.provider).await?; + } + + let total_examples = examples.len(); + Progress::global().set_total_examples(total_examples); + + let mut grouped_examples = group_examples_by_repo(&mut examples); + let example_batches = grouped_examples.chunks_mut(args.max_parallelism); + + for example_batch in example_batches { + let futures = example_batch.into_iter().map(|repo_examples| async { + for example in repo_examples.iter_mut() { + let result = async { + match &command { + Command::ParseExample => {} + Command::LoadProject => { + run_load_project(example, app_state.clone(), cx.clone()) + .await?; + } + Command::Context => { + run_context_retrieval( + example, + app_state.clone(), + cx.clone(), + ) + .await?; + } + Command::FormatPrompt(args) => { + run_format_prompt( + example, + args.prompt_format, + app_state.clone(), + cx.clone(), + ) + .await?; + } + Command::Predict(args) => { + run_prediction( + example, + Some(args.provider), + args.repetitions, + app_state.clone(), + cx.clone(), + ) + .await?; + } + Command::Distill => { + run_distill(example).await?; + } + Command::Score(args) | Command::Eval(args) => { + run_scoring(example, &args, app_state.clone(), cx.clone()) + .await?; + } + Command::Clean => { + unreachable!() + } + } + anyhow::Ok(()) } - Command::Clean => { - unreachable!() + .await; + + if let Err(e) = result { + Progress::global().increment_failed(); + let failed_example_path = + FAILED_EXAMPLES_DIR.join(format!("{}.json", example.spec.name)); + app_state + .fs + .write( + &failed_example_path, + &serde_json::to_vec_pretty(&example).unwrap(), + ) + .await + .unwrap(); + let err_path = FAILED_EXAMPLES_DIR + .join(format!("{}_err.txt", example.spec.name)); + app_state + .fs + .write(&err_path, e.to_string().as_bytes()) + .await + .unwrap(); + + let msg = format!( + indoc::indoc! {" + While processing {}: + + {:?} + + Written to: \x1b[36m{}\x1b[0m + + Explore this example data with: + fx \x1b[36m{}\x1b[0m + + Re-run this example with: + cargo run -p edit_prediction_cli -- {} \x1b[36m{}\x1b[0m + "}, + example.spec.name, + e, + err_path.display(), + failed_example_path.display(), + command, + failed_example_path.display(), + ); + if args.failfast || total_examples == 1 { + Progress::global().finalize(); + panic!("{}", msg); + } else { + log::error!("{}", msg); + } } } - } - }); - futures::future::join_all(futures).await; - } - Progress::global().clear(); + }); + futures::future::join_all(futures).await; + } + Progress::global().finalize(); - if args.output.is_some() || !matches!(command, Command::Eval(_)) { - write_examples(&examples, output.as_ref()); + if args.output.is_some() || !matches!(command, Command::Eval(_)) { + write_examples(&examples, output.as_ref()); + } + + match &command { + Command::Predict(args) => predict::sync_batches(&args.provider).await?, + Command::Eval(_) => score::print_report(&examples), + _ => (), + }; + + anyhow::Ok(()) } + .await; - match &command { - Command::Predict(args) => predict::sync_batches(&args.provider).await, - Command::Eval(_) => score::print_report(&examples), - _ => (), - }; + if let Err(e) = result { + panic!("Fatal error: {:?}", e); + } let _ = cx.update(|cx| cx.quit()); }) diff --git a/crates/edit_prediction_cli/src/paths.rs b/crates/edit_prediction_cli/src/paths.rs index 0f470fae556b6d61739ab77083d7edbedf77ef89..e5d420d0e3dbeda9c50b8e5a3683238149dbc604 100644 --- a/crates/edit_prediction_cli/src/paths.rs +++ b/crates/edit_prediction_cli/src/paths.rs @@ -18,6 +18,8 @@ pub static RUN_DIR: LazyLock = LazyLock::new(|| { }); pub static LATEST_EXAMPLE_RUN_DIR: LazyLock = LazyLock::new(|| DATA_DIR.join("latest")); pub static LLM_CACHE_DB: LazyLock = LazyLock::new(|| CACHE_DIR.join("llm_cache.sqlite")); +pub static FAILED_EXAMPLES_DIR: LazyLock = + LazyLock::new(|| ensure_dir(&RUN_DIR.join("failed"))); fn ensure_dir(path: &Path) -> PathBuf { std::fs::create_dir_all(path).expect("Failed to create directory"); diff --git a/crates/edit_prediction_cli/src/predict.rs b/crates/edit_prediction_cli/src/predict.rs index 3f690266e3165b2d52f642457e7aebf959a40a03..aa93c5415dea091164a68b76a34242697aac70e3 100644 --- a/crates/edit_prediction_cli/src/predict.rs +++ b/crates/edit_prediction_cli/src/predict.rs @@ -9,6 +9,7 @@ use crate::{ progress::{InfoStyle, Progress, Step}, retrieve_context::run_context_retrieval, }; +use anyhow::Context as _; use edit_prediction::{DebugEvent, EditPredictionStore}; use futures::{FutureExt as _, StreamExt as _, future::Shared}; use gpui::{AppContext as _, AsyncApp, Task}; @@ -26,32 +27,32 @@ pub async fn run_prediction( repetition_count: usize, app_state: Arc, mut cx: AsyncApp, -) { +) -> anyhow::Result<()> { if !example.predictions.is_empty() { - return; + return Ok(()); } - let provider = provider.unwrap(); + let provider = provider.context("provider is required")?; - run_context_retrieval(example, app_state.clone(), cx.clone()).await; + run_context_retrieval(example, app_state.clone(), cx.clone()).await?; if matches!( provider, PredictionProvider::Teacher | PredictionProvider::TeacherNonBatching ) { - let _step_progress = Progress::global().start(Step::Predict, &example.name); + let _step_progress = Progress::global().start(Step::Predict, &example.spec.name); if example.prompt.is_none() { - run_format_prompt(example, PromptFormat::Teacher, app_state.clone(), cx).await; + run_format_prompt(example, PromptFormat::Teacher, app_state.clone(), cx).await?; } let batched = matches!(provider, PredictionProvider::Teacher); return predict_anthropic(example, repetition_count, batched).await; } - run_load_project(example, app_state.clone(), cx.clone()).await; + run_load_project(example, app_state.clone(), cx.clone()).await?; - let _step_progress = Progress::global().start(Step::Predict, &example.name); + let _step_progress = Progress::global().start(Step::Predict, &example.spec.name); if matches!( provider, @@ -62,10 +63,9 @@ pub async fn run_prediction( .get_or_init(|| { let client = app_state.client.clone(); cx.spawn(async move |cx| { - client - .sign_in_with_optional_connect(true, cx) - .await - .unwrap(); + if let Err(e) = client.sign_in_with_optional_connect(true, cx).await { + eprintln!("Authentication failed: {}", e); + } }) .shared() }) @@ -73,33 +73,30 @@ pub async fn run_prediction( .await; } - let ep_store = cx - .update(|cx| EditPredictionStore::try_global(cx).unwrap()) - .unwrap(); - - ep_store - .update(&mut cx, |store, _cx| { - let model = match provider { - PredictionProvider::Zeta1 => edit_prediction::EditPredictionModel::Zeta1, - PredictionProvider::Zeta2 => edit_prediction::EditPredictionModel::Zeta2, - PredictionProvider::Sweep => edit_prediction::EditPredictionModel::Sweep, - PredictionProvider::Mercury => edit_prediction::EditPredictionModel::Mercury, - PredictionProvider::Teacher | PredictionProvider::TeacherNonBatching => { - unreachable!() - } - }; - store.set_edit_prediction_model(model); - }) - .unwrap(); - let state = example.state.as_ref().unwrap(); - let run_dir = RUN_DIR.join(&example.name); + let ep_store = cx.update(|cx| { + EditPredictionStore::try_global(cx).context("EditPredictionStore not initialized") + })??; + + ep_store.update(&mut cx, |store, _cx| { + let model = match provider { + PredictionProvider::Zeta1 => edit_prediction::EditPredictionModel::Zeta1, + PredictionProvider::Zeta2 => edit_prediction::EditPredictionModel::Zeta2, + PredictionProvider::Sweep => edit_prediction::EditPredictionModel::Sweep, + PredictionProvider::Mercury => edit_prediction::EditPredictionModel::Mercury, + PredictionProvider::Teacher | PredictionProvider::TeacherNonBatching => { + unreachable!() + } + }; + store.set_edit_prediction_model(model); + })?; + let state = example.state.as_ref().context("state must be set")?; + let run_dir = RUN_DIR.join(&example.spec.name); let updated_example = Arc::new(Mutex::new(example.clone())); let current_run_ix = Arc::new(AtomicUsize::new(0)); - let mut debug_rx = ep_store - .update(&mut cx, |store, cx| store.debug_info(&state.project, cx)) - .unwrap(); + let mut debug_rx = + ep_store.update(&mut cx, |store, cx| store.debug_info(&state.project, cx))?; let debug_task = cx.background_spawn({ let updated_example = updated_example.clone(); let current_run_ix = current_run_ix.clone(); @@ -153,14 +150,14 @@ pub async fn run_prediction( run_dir.clone() }; - fs::create_dir_all(&run_dir).unwrap(); + fs::create_dir_all(&run_dir)?; if LATEST_EXAMPLE_RUN_DIR.is_symlink() { - fs::remove_file(&*LATEST_EXAMPLE_RUN_DIR).unwrap(); + fs::remove_file(&*LATEST_EXAMPLE_RUN_DIR)?; } #[cfg(unix)] - std::os::unix::fs::symlink(&run_dir, &*LATEST_EXAMPLE_RUN_DIR).unwrap(); + std::os::unix::fs::symlink(&run_dir, &*LATEST_EXAMPLE_RUN_DIR)?; #[cfg(windows)] - std::os::windows::fs::symlink_dir(&run_dir, &*LATEST_EXAMPLE_RUN_DIR).unwrap(); + std::os::windows::fs::symlink_dir(&run_dir, &*LATEST_EXAMPLE_RUN_DIR)?; updated_example .lock() @@ -181,10 +178,8 @@ pub async fn run_prediction( cloud_llm_client::PredictEditsRequestTrigger::Cli, cx, ) - }) - .unwrap() - .await - .unwrap(); + })? + .await?; let actual_patch = prediction .and_then(|prediction| { @@ -213,20 +208,23 @@ pub async fn run_prediction( } } - ep_store - .update(&mut cx, |store, _| { - store.remove_project(&state.project); - }) - .unwrap(); - debug_task.await.unwrap(); + ep_store.update(&mut cx, |store, _| { + store.remove_project(&state.project); + })?; + debug_task.await?; *example = Arc::into_inner(updated_example) - .unwrap() + .ok_or_else(|| anyhow::anyhow!("Failed to unwrap Arc"))? .into_inner() - .unwrap(); + .map_err(|_| anyhow::anyhow!("Failed to unwrap Mutex"))?; + Ok(()) } -async fn predict_anthropic(example: &mut Example, _repetition_count: usize, batched: bool) { +async fn predict_anthropic( + example: &mut Example, + _repetition_count: usize, + batched: bool, +) -> anyhow::Result<()> { let llm_model_name = "claude-sonnet-4-5"; let max_tokens = 16384; let llm_client = if batched { @@ -234,12 +232,9 @@ async fn predict_anthropic(example: &mut Example, _repetition_count: usize, batc } else { AnthropicClient::plain() }; - let llm_client = llm_client.expect("Failed to create LLM client"); + let llm_client = llm_client.context("Failed to create LLM client")?; - let prompt = example - .prompt - .as_ref() - .unwrap_or_else(|| panic!("Prompt is required for an example {}", &example.name)); + let prompt = example.prompt.as_ref().context("Prompt is required")?; let messages = vec![anthropic::Message { role: anthropic::Role::User, @@ -251,11 +246,10 @@ async fn predict_anthropic(example: &mut Example, _repetition_count: usize, batc let Some(response) = llm_client .generate(llm_model_name, max_tokens, messages) - .await - .unwrap() + .await? else { // Request stashed for batched processing - return; + return Ok(()); }; let actual_output = response @@ -268,7 +262,7 @@ async fn predict_anthropic(example: &mut Example, _repetition_count: usize, batc .collect::>() .join("\n"); - let actual_patch = TeacherPrompt::parse(example, &actual_output); + let actual_patch = TeacherPrompt::parse(example, &actual_output)?; let prediction = ExamplePrediction { actual_patch, @@ -277,19 +271,21 @@ async fn predict_anthropic(example: &mut Example, _repetition_count: usize, batc }; example.predictions.push(prediction); + Ok(()) } -pub async fn sync_batches(provider: &PredictionProvider) { +pub async fn sync_batches(provider: &PredictionProvider) -> anyhow::Result<()> { match provider { PredictionProvider::Teacher => { let cache_path = crate::paths::LLM_CACHE_DB.as_ref(); let llm_client = - AnthropicClient::batch(cache_path).expect("Failed to create LLM client"); + AnthropicClient::batch(cache_path).context("Failed to create LLM client")?; llm_client .sync_batches() .await - .expect("Failed to sync batches"); + .context("Failed to sync batches")?; } _ => (), - } + }; + Ok(()) } diff --git a/crates/edit_prediction_cli/src/progress.rs b/crates/edit_prediction_cli/src/progress.rs index 8195485d70c70c0cbfb38e2de83a055598d5e4e5..ddc710f202cc98e5932c234cb6bebcc93b28171c 100644 --- a/crates/edit_prediction_cli/src/progress.rs +++ b/crates/edit_prediction_cli/src/progress.rs @@ -20,6 +20,7 @@ struct ProgressInner { max_example_name_len: usize, status_lines_displayed: usize, total_examples: usize, + failed_examples: usize, last_line_is_logging: bool, } @@ -78,7 +79,7 @@ impl Step { static GLOBAL: OnceLock> = OnceLock::new(); static LOGGER: ProgressLogger = ProgressLogger; -const RIGHT_MARGIN: usize = 4; +const MARGIN: usize = 4; const MAX_STATUS_LINES: usize = 10; impl Progress { @@ -95,6 +96,7 @@ impl Progress { max_example_name_len: 0, status_lines_displayed: 0, total_examples: 0, + failed_examples: 0, last_line_is_logging: false, }), }); @@ -110,6 +112,11 @@ impl Progress { inner.total_examples = total; } + pub fn increment_failed(&self) { + let mut inner = self.inner.lock().unwrap(); + inner.failed_examples += 1; + } + /// Prints a message to stderr, clearing and redrawing status lines to avoid corruption. /// This should be used for any output that needs to appear above the status lines. fn log(&self, message: &str) { @@ -119,7 +126,7 @@ impl Progress { if !inner.last_line_is_logging { let reset = "\x1b[0m"; let dim = "\x1b[2m"; - let divider = "─".repeat(inner.terminal_width.saturating_sub(RIGHT_MARGIN)); + let divider = "─".repeat(inner.terminal_width.saturating_sub(MARGIN)); eprintln!("{dim}{divider}{reset}"); inner.last_line_is_logging = true; } @@ -180,7 +187,7 @@ impl Progress { if inner.last_line_is_logging { let reset = "\x1b[0m"; let dim = "\x1b[2m"; - let divider = "─".repeat(inner.terminal_width.saturating_sub(RIGHT_MARGIN)); + let divider = "─".repeat(inner.terminal_width.saturating_sub(MARGIN)); eprintln!("{dim}{divider}{reset}"); inner.last_line_is_logging = false; } @@ -229,7 +236,7 @@ impl Progress { let duration_with_margin = format!("{duration} "); let padding_needed = inner .terminal_width - .saturating_sub(RIGHT_MARGIN) + .saturating_sub(MARGIN) .saturating_sub(duration_with_margin.len()) .saturating_sub(strip_ansi_len(&prefix)); let padding = " ".repeat(padding_needed); @@ -263,20 +270,33 @@ impl Progress { // Build the done/in-progress/total label let done_count = inner.completed.len(); let in_progress_count = inner.in_progress.len(); + let failed_count = inner.failed_examples; + + let failed_label = if failed_count > 0 { + format!(" {} failed ", failed_count) + } else { + String::new() + }; + let range_label = format!( " {}/{}/{} ", done_count, in_progress_count, inner.total_examples ); - // Print a divider line with range label aligned with timestamps + // Print a divider line with failed count on left, range label on right + let failed_visible_len = strip_ansi_len(&failed_label); let range_visible_len = range_label.len(); - let left_divider_len = inner + let middle_divider_len = inner .terminal_width - .saturating_sub(RIGHT_MARGIN) + .saturating_sub(MARGIN * 2) + .saturating_sub(failed_visible_len) .saturating_sub(range_visible_len); - let left_divider = "─".repeat(left_divider_len); - let right_divider = "─".repeat(RIGHT_MARGIN); - eprintln!("{dim}{left_divider}{reset}{range_label}{dim}{right_divider}{reset}"); + let left_divider = "─".repeat(MARGIN); + let middle_divider = "─".repeat(middle_divider_len); + let right_divider = "─".repeat(MARGIN); + eprintln!( + "{dim}{left_divider}{reset}{failed_label}{dim}{middle_divider}{reset}{range_label}{dim}{right_divider}{reset}" + ); let mut tasks: Vec<_> = inner.in_progress.iter().collect(); tasks.sort_by_key(|(name, _)| *name); @@ -304,7 +324,7 @@ impl Progress { let duration_with_margin = format!("{elapsed} "); let padding_needed = inner .terminal_width - .saturating_sub(RIGHT_MARGIN) + .saturating_sub(MARGIN) .saturating_sub(duration_with_margin.len()) .saturating_sub(strip_ansi_len(&prefix)); let padding = " ".repeat(padding_needed); @@ -324,9 +344,23 @@ impl Progress { let _ = std::io::stderr().flush(); } - pub fn clear(&self) { + pub fn finalize(&self) { let mut inner = self.inner.lock().unwrap(); Self::clear_status_lines(&mut inner); + + // Print summary if there were failures + if inner.failed_examples > 0 { + let total_processed = inner.completed.len() + inner.failed_examples; + let percentage = if total_processed > 0 { + inner.failed_examples as f64 / total_processed as f64 * 100.0 + } else { + 0.0 + }; + eprintln!( + "\n{} of {} examples failed ({:.1}%)", + inner.failed_examples, total_processed, percentage + ); + } } } diff --git a/crates/edit_prediction_cli/src/retrieve_context.rs b/crates/edit_prediction_cli/src/retrieve_context.rs index c066cf3caa9ece27144222ef94e3ac72c2285be8..abba4504edc6c0733ffd8c0677e2e3304d8100fa 100644 --- a/crates/edit_prediction_cli/src/retrieve_context.rs +++ b/crates/edit_prediction_cli/src/retrieve_context.rs @@ -4,6 +4,7 @@ use crate::{ load_project::run_load_project, progress::{InfoStyle, Progress, Step, StepProgress}, }; +use anyhow::Context as _; use collections::HashSet; use edit_prediction::{DebugEvent, EditPredictionStore}; use futures::{FutureExt as _, StreamExt as _, channel::mpsc}; @@ -17,39 +18,35 @@ pub async fn run_context_retrieval( example: &mut Example, app_state: Arc, mut cx: AsyncApp, -) { +) -> anyhow::Result<()> { if example.context.is_some() { - return; + return Ok(()); } - run_load_project(example, app_state.clone(), cx.clone()).await; + run_load_project(example, app_state.clone(), cx.clone()).await?; let step_progress: Arc = Progress::global() - .start(Step::Context, &example.name) + .start(Step::Context, &example.spec.name) .into(); let state = example.state.as_ref().unwrap(); let project = state.project.clone(); - let _lsp_handle = project - .update(&mut cx, |project, cx| { - project.register_buffer_with_language_servers(&state.buffer, cx) - }) - .unwrap(); - wait_for_language_servers_to_start(&project, &state.buffer, &step_progress, &mut cx).await; - - let ep_store = cx - .update(|cx| EditPredictionStore::try_global(cx).unwrap()) - .unwrap(); - - let mut events = ep_store - .update(&mut cx, |store, cx| { - store.register_buffer(&state.buffer, &project, cx); - store.set_use_context(true); - store.refresh_context(&project, &state.buffer, state.cursor_position, cx); - store.debug_info(&project, cx) - }) - .unwrap(); + let _lsp_handle = project.update(&mut cx, |project, cx| { + project.register_buffer_with_language_servers(&state.buffer, cx) + })?; + wait_for_language_servers_to_start(&project, &state.buffer, &step_progress, &mut cx).await?; + + let ep_store = cx.update(|cx| { + EditPredictionStore::try_global(cx).context("EditPredictionStore not initialized") + })??; + + let mut events = ep_store.update(&mut cx, |store, cx| { + store.register_buffer(&state.buffer, &project, cx); + store.set_use_context(true); + store.refresh_context(&project, &state.buffer, state.cursor_position, cx); + store.debug_info(&project, cx) + })?; while let Some(event) = events.next().await { match event { @@ -60,9 +57,8 @@ pub async fn run_context_retrieval( } } - let context_files = ep_store - .update(&mut cx, |store, cx| store.context_for_project(&project, cx)) - .unwrap(); + let context_files = + ep_store.update(&mut cx, |store, cx| store.context_for_project(&project, cx))?; let excerpt_count: usize = context_files.iter().map(|f| f.excerpts.len()).sum(); step_progress.set_info(format!("{} excerpts", excerpt_count), InfoStyle::Normal); @@ -70,6 +66,7 @@ pub async fn run_context_retrieval( example.context = Some(ExampleContext { files: context_files, }); + Ok(()) } async fn wait_for_language_servers_to_start( @@ -77,10 +74,8 @@ async fn wait_for_language_servers_to_start( buffer: &Entity, step_progress: &Arc, cx: &mut AsyncApp, -) { - let lsp_store = project - .read_with(cx, |project, _| project.lsp_store()) - .unwrap(); +) -> anyhow::Result<()> { + let lsp_store = project.read_with(cx, |project, _| project.lsp_store())?; let (language_server_ids, mut starting_language_server_ids) = buffer .update(cx, |buffer, cx| { @@ -123,7 +118,7 @@ async fn wait_for_language_servers_to_start( } }, _ = timeout.clone().fuse() => { - panic!("LSP wait timed out after 5 minutes"); + return Err(anyhow::anyhow!("LSP wait timed out after 5 minutes")); } } } @@ -132,8 +127,7 @@ async fn wait_for_language_servers_to_start( if !language_server_ids.is_empty() { project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .unwrap() + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? .detach(); } @@ -175,10 +169,8 @@ async fn wait_for_language_servers_to_start( ]; project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .unwrap() - .await - .unwrap(); + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? + .await?; let mut pending_language_server_ids = HashSet::from_iter(language_server_ids.into_iter()); while !pending_language_server_ids.is_empty() { @@ -189,11 +181,12 @@ async fn wait_for_language_servers_to_start( } }, _ = timeout.clone().fuse() => { - panic!("LSP wait timed out after 5 minutes"); + return Err(anyhow::anyhow!("LSP wait timed out after 5 minutes")); } } } drop(subscriptions); step_progress.clear_substatus(); + Ok(()) } diff --git a/crates/edit_prediction_cli/src/score.rs b/crates/edit_prediction_cli/src/score.rs index b87d8e4df24c8cb12676ed71fe1ea930a841791d..7b507e6d19c943de92eb0b22c7d24d4026789fed 100644 --- a/crates/edit_prediction_cli/src/score.rs +++ b/crates/edit_prediction_cli/src/score.rs @@ -15,7 +15,7 @@ pub async fn run_scoring( args: &PredictArgs, app_state: Arc, cx: AsyncApp, -) { +) -> anyhow::Result<()> { run_prediction( example, Some(args.provider), @@ -23,11 +23,11 @@ pub async fn run_scoring( app_state, cx, ) - .await; + .await?; - let _progress = Progress::global().start(Step::Score, &example.name); + let _progress = Progress::global().start(Step::Score, &example.spec.name); - let expected_patch = parse_patch(&example.expected_patch); + let expected_patch = parse_patch(&example.spec.expected_patch); let mut scores = vec![]; @@ -43,6 +43,7 @@ pub async fn run_scoring( } example.score = scores; + Ok(()) } fn parse_patch(patch: &str) -> Vec> { @@ -70,7 +71,7 @@ pub fn print_report(examples: &[Example]) { eprintln!( "{:<30} {:>4} {:>4} {:>4} {:>9.2}% {:>7.2}% {:>7.2}% {:>9.2}", - truncate_name(&example.name, 30), + truncate_name(&example.spec.name, 30), line_match.true_positives, line_match.false_positives, line_match.false_negatives, diff --git a/crates/edit_prediction_ui/Cargo.toml b/crates/edit_prediction_ui/Cargo.toml index d6fc45512132197a3b9e7bd200c3005efa52ae10..b406a450601bef908c27a48be14fe9b1f2204c08 100644 --- a/crates/edit_prediction_ui/Cargo.toml +++ b/crates/edit_prediction_ui/Cargo.toml @@ -15,13 +15,16 @@ doctest = false [dependencies] anyhow.workspace = true buffer_diff.workspace = true +git.workspace = true +log.workspace = true +time.workspace = true client.workspace = true cloud_llm_client.workspace = true codestral.workspace = true command_palette_hooks.workspace = true copilot.workspace = true -edit_prediction.workspace = true edit_prediction_types.workspace = true +edit_prediction.workspace = true editor.workspace = true feature_flags.workspace = true fs.workspace = true @@ -41,7 +44,6 @@ telemetry.workspace = true text.workspace = true theme.workspace = true ui.workspace = true -ui_input.workspace = true util.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/edit_prediction_ui/src/edit_prediction_button.rs b/crates/edit_prediction_ui/src/edit_prediction_button.rs index 04c7614689c5fdc076ab0aa9c4b4fe7d68e2f582..bbf9f4677df278c014379964e7bdc714e6ce78d8 100644 --- a/crates/edit_prediction_ui/src/edit_prediction_button.rs +++ b/crates/edit_prediction_ui/src/edit_prediction_button.rs @@ -3,7 +3,9 @@ use client::{Client, UserStore, zed_urls}; use cloud_llm_client::UsageLimit; use codestral::CodestralEditPredictionDelegate; use copilot::{Copilot, Status}; -use edit_prediction::{MercuryFeatureFlag, SweepFeatureFlag, Zeta2FeatureFlag}; +use edit_prediction::{ + EditPredictionStore, MercuryFeatureFlag, SweepFeatureFlag, Zeta2FeatureFlag, +}; use edit_prediction_types::EditPredictionDelegateHandle; use editor::{ Editor, MultiBufferOffset, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll, @@ -42,11 +44,10 @@ use workspace::{ StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle, notifications::NotificationId, }; -use zed_actions::OpenBrowser; +use zed_actions::{OpenBrowser, OpenSettingsAt}; use crate::{ - ExternalProviderApiKeyModal, RatePredictions, - rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag, + CaptureExample, RatePredictions, rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag, }; actions!( @@ -248,45 +249,21 @@ impl Render for EditPredictionButton { EditPredictionProvider::Codestral => { let enabled = self.editor_enabled.unwrap_or(true); let has_api_key = CodestralEditPredictionDelegate::has_api_key(cx); - let fs = self.fs.clone(); let this = cx.weak_entity(); + let tooltip_meta = if has_api_key { + "Powered by Codestral" + } else { + "Missing API key for Codestral" + }; + div().child( PopoverMenu::new("codestral") .menu(move |window, cx| { - if has_api_key { - this.update(cx, |this, cx| { - this.build_codestral_context_menu(window, cx) - }) - .ok() - } else { - Some(ContextMenu::build(window, cx, |menu, _, _| { - let fs = fs.clone(); - - menu.entry( - "Configure Codestral API Key", - None, - move |window, cx| { - window.dispatch_action( - zed_actions::agent::OpenSettings.boxed_clone(), - cx, - ); - }, - ) - .separator() - .entry( - "Use Zed AI instead", - None, - move |_, cx| { - set_completion_provider( - fs.clone(), - cx, - EditPredictionProvider::Zed, - ) - }, - ) - })) - } + this.update(cx, |this, cx| { + this.build_codestral_context_menu(window, cx) + }) + .ok() }) .anchor(Corner::BottomRight) .trigger_with_tooltip( @@ -304,7 +281,14 @@ impl Render for EditPredictionButton { cx.theme().colors().status_bar_background, )) }), - move |_window, cx| Tooltip::for_action("Codestral", &ToggleMenu, cx), + move |_window, cx| { + Tooltip::with_meta( + "Edit Prediction", + Some(&ToggleMenu), + tooltip_meta, + cx, + ) + }, ) .with_handle(self.popover_menu_handle.clone()), ) @@ -313,6 +297,7 @@ impl Render for EditPredictionButton { let enabled = self.editor_enabled.unwrap_or(true); let ep_icon; + let tooltip_meta; let mut missing_token = false; match provider { @@ -320,15 +305,25 @@ impl Render for EditPredictionButton { EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME, ) => { ep_icon = IconName::SweepAi; + tooltip_meta = if missing_token { + "Missing API key for Sweep" + } else { + "Powered by Sweep" + }; missing_token = edit_prediction::EditPredictionStore::try_global(cx) - .is_some_and(|ep_store| !ep_store.read(cx).has_sweep_api_token()); + .is_some_and(|ep_store| !ep_store.read(cx).has_sweep_api_token(cx)); } EditPredictionProvider::Experimental( EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME, ) => { ep_icon = IconName::Inception; missing_token = edit_prediction::EditPredictionStore::try_global(cx) - .is_some_and(|ep_store| !ep_store.read(cx).has_mercury_api_token()); + .is_some_and(|ep_store| !ep_store.read(cx).has_mercury_api_token(cx)); + tooltip_meta = if missing_token { + "Missing API key for Mercury" + } else { + "Powered by Mercury" + }; } _ => { ep_icon = if enabled { @@ -336,6 +331,7 @@ impl Render for EditPredictionButton { } else { IconName::ZedPredictDisabled }; + tooltip_meta = "Powered by Zeta" } }; @@ -400,33 +396,26 @@ impl Render for EditPredictionButton { }) .when(!self.popover_menu_handle.is_deployed(), |element| { let user = user.clone(); + element.tooltip(move |_window, cx| { - if enabled { + let description = if enabled { if show_editor_predictions { - Tooltip::for_action("Edit Prediction", &ToggleMenu, cx) + tooltip_meta } else if user.is_none() { - Tooltip::with_meta( - "Edit Prediction", - Some(&ToggleMenu), - "Sign In To Use", - cx, - ) + "Sign In To Use" } else { - Tooltip::with_meta( - "Edit Prediction", - Some(&ToggleMenu), - "Hidden For This File", - cx, - ) + "Hidden For This File" } } else { - Tooltip::with_meta( - "Edit Prediction", - Some(&ToggleMenu), - "Disabled For This File", - cx, - ) - } + "Disabled For This File" + }; + + Tooltip::with_meta( + "Edit Prediction", + Some(&ToggleMenu), + description, + cx, + ) }) }); @@ -519,6 +508,12 @@ impl EditPredictionButton { providers.push(EditPredictionProvider::Zed); + if cx.has_flag::() { + providers.push(EditPredictionProvider::Experimental( + EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, + )); + } + if let Some(copilot) = Copilot::global(cx) { if matches!(copilot.read(cx).status(), Status::Authorized) { providers.push(EditPredictionProvider::Copilot); @@ -537,24 +532,28 @@ impl EditPredictionButton { providers.push(EditPredictionProvider::Codestral); } - if cx.has_flag::() { + let ep_store = EditPredictionStore::try_global(cx); + + if cx.has_flag::() + && ep_store + .as_ref() + .is_some_and(|ep_store| ep_store.read(cx).has_sweep_api_token(cx)) + { providers.push(EditPredictionProvider::Experimental( EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME, )); } - if cx.has_flag::() { + if cx.has_flag::() + && ep_store + .as_ref() + .is_some_and(|ep_store| ep_store.read(cx).has_mercury_api_token(cx)) + { providers.push(EditPredictionProvider::Experimental( EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME, )); } - if cx.has_flag::() { - providers.push(EditPredictionProvider::Experimental( - EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, - )); - } - providers } @@ -562,13 +561,10 @@ impl EditPredictionButton { &self, mut menu: ContextMenu, current_provider: EditPredictionProvider, - cx: &App, + cx: &mut App, ) -> ContextMenu { let available_providers = self.get_available_providers(cx); - const ZED_AI_CALLOUT: &str = - "Zed's edit prediction is powered by Zeta, an open-source, dataset mode."; - let providers: Vec<_> = available_providers .into_iter() .filter(|p| *p != EditPredictionProvider::None) @@ -581,153 +577,32 @@ impl EditPredictionButton { let is_current = provider == current_provider; let fs = self.fs.clone(); - menu = match provider { - EditPredictionProvider::Zed => menu.item( - ContextMenuEntry::new("Zed AI") - .toggleable(IconPosition::Start, is_current) - .documentation_aside( - DocumentationSide::Left, - DocumentationEdge::Bottom, - |_| Label::new(ZED_AI_CALLOUT).into_any_element(), - ) - .handler(move |_, cx| { - set_completion_provider(fs.clone(), cx, provider); - }), - ), - EditPredictionProvider::Copilot => menu.item( - ContextMenuEntry::new("GitHub Copilot") - .toggleable(IconPosition::Start, is_current) - .handler(move |_, cx| { - set_completion_provider(fs.clone(), cx, provider); - }), - ), - EditPredictionProvider::Supermaven => menu.item( - ContextMenuEntry::new("Supermaven") - .toggleable(IconPosition::Start, is_current) - .handler(move |_, cx| { - set_completion_provider(fs.clone(), cx, provider); - }), - ), - EditPredictionProvider::Codestral => menu.item( - ContextMenuEntry::new("Codestral") - .toggleable(IconPosition::Start, is_current) - .handler(move |_, cx| { - set_completion_provider(fs.clone(), cx, provider); - }), - ), + let name = match provider { + EditPredictionProvider::Zed => "Zed AI", + EditPredictionProvider::Copilot => "GitHub Copilot", + EditPredictionProvider::Supermaven => "Supermaven", + EditPredictionProvider::Codestral => "Codestral", EditPredictionProvider::Experimental( EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME, - ) => { - let has_api_token = edit_prediction::EditPredictionStore::try_global(cx) - .map_or(false, |ep_store| ep_store.read(cx).has_sweep_api_token()); - - let should_open_modal = !has_api_token || is_current; - - let entry = if has_api_token { - ContextMenuEntry::new("Sweep") - .toggleable(IconPosition::Start, is_current) - } else { - ContextMenuEntry::new("Sweep") - .icon(IconName::XCircle) - .icon_color(Color::Error) - .documentation_aside( - DocumentationSide::Left, - DocumentationEdge::Bottom, - |_| { - Label::new("Click to configure your Sweep API token") - .into_any_element() - }, - ) - }; - - let entry = entry.handler(move |window, cx| { - if should_open_modal { - if let Some(workspace) = window.root::().flatten() { - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - ExternalProviderApiKeyModal::new( - window, - cx, - |api_key, store, cx| { - store - .sweep_ai - .set_api_token(api_key, cx) - .detach_and_log_err(cx); - }, - ) - }); - }); - }; - } else { - set_completion_provider(fs.clone(), cx, provider); - } - }); - - menu.item(entry) - } + ) => "Sweep", EditPredictionProvider::Experimental( EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME, - ) => { - let has_api_token = edit_prediction::EditPredictionStore::try_global(cx) - .map_or(false, |ep_store| ep_store.read(cx).has_mercury_api_token()); - - let should_open_modal = !has_api_token || is_current; - - let entry = if has_api_token { - ContextMenuEntry::new("Mercury") - .toggleable(IconPosition::Start, is_current) - } else { - ContextMenuEntry::new("Mercury") - .icon(IconName::XCircle) - .icon_color(Color::Error) - .documentation_aside( - DocumentationSide::Left, - DocumentationEdge::Bottom, - |_| { - Label::new("Click to configure your Mercury API token") - .into_any_element() - }, - ) - }; - - let entry = entry.handler(move |window, cx| { - if should_open_modal { - if let Some(workspace) = window.root::().flatten() { - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - ExternalProviderApiKeyModal::new( - window, - cx, - |api_key, store, cx| { - store - .mercury - .set_api_token(api_key, cx) - .detach_and_log_err(cx); - }, - ) - }); - }); - }; - } else { - set_completion_provider(fs.clone(), cx, provider); - } - }); - - menu.item(entry) - } + ) => "Mercury", EditPredictionProvider::Experimental( EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, - ) => menu.item( - ContextMenuEntry::new("Zeta2") - .toggleable(IconPosition::Start, is_current) - .handler(move |_, cx| { - set_completion_provider(fs.clone(), cx, provider); - }), - ), + ) => "Zeta2", EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => { continue; } }; + + menu = menu.item( + ContextMenuEntry::new(name) + .toggleable(IconPosition::Start, is_current) + .handler(move |_, cx| { + set_completion_provider(fs.clone(), cx, provider); + }), + ) } } @@ -832,14 +707,7 @@ impl EditPredictionButton { let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle); let eager_mode = matches!(current_mode, EditPredictionsMode::Eager); - if matches!( - provider, - EditPredictionProvider::Zed - | EditPredictionProvider::Copilot - | EditPredictionProvider::Supermaven - | EditPredictionProvider::Codestral - ) { - menu = menu + menu = menu .separator() .header("Display Modes") .item( @@ -868,104 +736,111 @@ impl EditPredictionButton { } }), ); - } menu = menu.separator().header("Privacy"); - if let Some(provider) = &self.edit_prediction_provider { - let data_collection = provider.data_collection_state(cx); - - if data_collection.is_supported() { - let provider = provider.clone(); - let enabled = data_collection.is_enabled(); - let is_open_source = data_collection.is_project_open_source(); - let is_collecting = data_collection.is_enabled(); - let (icon_name, icon_color) = if is_open_source && is_collecting { - (IconName::Check, Color::Success) - } else { - (IconName::Check, Color::Accent) - }; - - menu = menu.item( - ContextMenuEntry::new("Training Data Collection") - .toggleable(IconPosition::Start, data_collection.is_enabled()) - .icon(icon_name) - .icon_color(icon_color) - .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |cx| { - let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) { - (true, true) => ( - "Project identified as open source, and you're sharing data.", - Color::Default, - IconName::Check, - Color::Success, - ), - (true, false) => ( - "Project identified as open source, but you're not sharing data.", - Color::Muted, - IconName::Close, - Color::Muted, - ), - (false, true) => ( - "Project not identified as open source. No data captured.", - Color::Muted, - IconName::Close, - Color::Muted, - ), - (false, false) => ( - "Project not identified as open source, and setting turned off.", - Color::Muted, - IconName::Close, - Color::Muted, - ), - }; - v_flex() - .gap_2() - .child( - Label::new(indoc!{ - "Help us improve our open dataset model by sharing data from open source repositories. \ - Zed must detect a license file in your repo for this setting to take effect. \ - Files with sensitive data and secrets are excluded by default." - }) - ) - .child( - h_flex() - .items_start() - .pt_2() - .pr_1() - .flex_1() - .gap_1p5() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child(h_flex().flex_shrink_0().h(line_height).child(Icon::new(icon_name).size(IconSize::XSmall).color(icon_color))) - .child(div().child(msg).w_full().text_sm().text_color(label_color.color(cx))) - ) - .into_any_element() - }) - .handler(move |_, cx| { - provider.toggle_data_collection(cx); - - if !enabled { - telemetry::event!( - "Data Collection Enabled", - source = "Edit Prediction Status Menu" - ); - } else { - telemetry::event!( - "Data Collection Disabled", - source = "Edit Prediction Status Menu" - ); - } - }) - ); + if matches!( + provider, + EditPredictionProvider::Zed + | EditPredictionProvider::Experimental( + EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, + ) + ) { + if let Some(provider) = &self.edit_prediction_provider { + let data_collection = provider.data_collection_state(cx); + + if data_collection.is_supported() { + let provider = provider.clone(); + let enabled = data_collection.is_enabled(); + let is_open_source = data_collection.is_project_open_source(); + let is_collecting = data_collection.is_enabled(); + let (icon_name, icon_color) = if is_open_source && is_collecting { + (IconName::Check, Color::Success) + } else { + (IconName::Check, Color::Accent) + }; - if is_collecting && !is_open_source { menu = menu.item( - ContextMenuEntry::new("No data captured.") - .disabled(true) - .icon(IconName::Close) - .icon_color(Color::Error) - .icon_size(IconSize::Small), + ContextMenuEntry::new("Training Data Collection") + .toggleable(IconPosition::Start, data_collection.is_enabled()) + .icon(icon_name) + .icon_color(icon_color) + .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |cx| { + let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) { + (true, true) => ( + "Project identified as open source, and you're sharing data.", + Color::Default, + IconName::Check, + Color::Success, + ), + (true, false) => ( + "Project identified as open source, but you're not sharing data.", + Color::Muted, + IconName::Close, + Color::Muted, + ), + (false, true) => ( + "Project not identified as open source. No data captured.", + Color::Muted, + IconName::Close, + Color::Muted, + ), + (false, false) => ( + "Project not identified as open source, and setting turned off.", + Color::Muted, + IconName::Close, + Color::Muted, + ), + }; + v_flex() + .gap_2() + .child( + Label::new(indoc!{ + "Help us improve our open dataset model by sharing data from open source repositories. \ + Zed must detect a license file in your repo for this setting to take effect. \ + Files with sensitive data and secrets are excluded by default." + }) + ) + .child( + h_flex() + .items_start() + .pt_2() + .pr_1() + .flex_1() + .gap_1p5() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child(h_flex().flex_shrink_0().h(line_height).child(Icon::new(icon_name).size(IconSize::XSmall).color(icon_color))) + .child(div().child(msg).w_full().text_sm().text_color(label_color.color(cx))) + ) + .into_any_element() + }) + .handler(move |_, cx| { + provider.toggle_data_collection(cx); + + if !enabled { + telemetry::event!( + "Data Collection Enabled", + source = "Edit Prediction Status Menu" + ); + } else { + telemetry::event!( + "Data Collection Disabled", + source = "Edit Prediction Status Menu" + ); + } + }) ); + + if is_collecting && !is_open_source { + menu = menu.item( + ContextMenuEntry::new("No data captured.") + .disabled(true) + .icon(IconName::Close) + .icon_color(Color::Error) + .icon_size(IconSize::Small), + ); + } } } } @@ -1026,7 +901,13 @@ impl EditPredictionButton { .context(editor_focus_handle) .when( cx.has_flag::(), - |this| this.action("Rate Predictions", RatePredictions.boxed_clone()), + |this| { + this.action( + "Capture Edit Prediction Example", + CaptureExample.boxed_clone(), + ) + .action("Rate Predictions", RatePredictions.boxed_clone()) + }, ); } @@ -1087,10 +968,7 @@ impl EditPredictionButton { let menu = self.add_provider_switching_section(menu, EditPredictionProvider::Codestral, cx); - menu.separator() - .entry("Configure Codestral API Key", None, move |window, cx| { - window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx); - }) + menu }) } @@ -1210,6 +1088,22 @@ impl EditPredictionButton { } menu = self.add_provider_switching_section(menu, provider, cx); + menu = menu.separator().item( + ContextMenuEntry::new("Configure Providers") + .icon(IconName::Settings) + .icon_position(IconPosition::Start) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + OpenSettingsAt { + path: "edit_predictions.providers".to_string(), + } + .boxed_clone(), + cx, + ); + }), + ); + menu }) } diff --git a/crates/edit_prediction_ui/src/edit_prediction_ui.rs b/crates/edit_prediction_ui/src/edit_prediction_ui.rs index c177b5233c33feb4f5ff82f60bf3fb6981cf3ee8..a762fd22aa7c32779a096fa97b2ea20ef3c9b744 100644 --- a/crates/edit_prediction_ui/src/edit_prediction_ui.rs +++ b/crates/edit_prediction_ui/src/edit_prediction_ui.rs @@ -1,23 +1,30 @@ mod edit_prediction_button; mod edit_prediction_context_view; -mod external_provider_api_token_modal; mod rate_prediction_modal; use std::any::{Any as _, TypeId}; +use std::path::Path; +use std::sync::Arc; use command_palette_hooks::CommandPaletteFilter; -use edit_prediction::{ResetOnboarding, Zeta2FeatureFlag}; +use edit_prediction::{ + EditPredictionStore, ResetOnboarding, Zeta2FeatureFlag, example_spec::ExampleSpec, +}; use edit_prediction_context_view::EditPredictionContextView; +use editor::Editor; use feature_flags::FeatureFlagAppExt as _; -use gpui::actions; +use git::repository::DiffType; +use gpui::{Window, actions}; +use language::ToPoint as _; +use log; use project::DisableAiSettings; use rate_prediction_modal::RatePredictionsModal; use settings::{Settings as _, SettingsStore}; +use text::ToOffset as _; use ui::{App, prelude::*}; use workspace::{SplitDirection, Workspace}; pub use edit_prediction_button::{EditPredictionButton, ToggleMenu}; -pub use external_provider_api_token_modal::ExternalProviderApiKeyModal; use crate::rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag; @@ -34,6 +41,8 @@ actions!( [ /// Opens the rate completions modal. RatePredictions, + /// Captures an ExampleSpec from the current editing session and opens it as Markdown. + CaptureExample, ] ); @@ -47,6 +56,7 @@ pub fn init(cx: &mut App) { } }); + workspace.register_action(capture_edit_prediction_example); workspace.register_action_renderer(|div, _, _, cx| { let has_flag = cx.has_flag::(); div.when(has_flag, |div| { @@ -80,6 +90,7 @@ fn feature_gate_predict_edits_actions(cx: &mut App) { let reset_onboarding_action_types = [TypeId::of::()]; let all_action_types = [ TypeId::of::(), + TypeId::of::(), TypeId::of::(), zed_actions::OpenZedPredictOnboarding.type_id(), TypeId::of::(), @@ -126,3 +137,194 @@ fn feature_gate_predict_edits_actions(cx: &mut App) { }) .detach(); } + +fn capture_edit_prediction_example( + workspace: &mut Workspace, + _: &CaptureExample, + window: &mut Window, + cx: &mut Context, +) { + let Some(ep_store) = EditPredictionStore::try_global(cx) else { + return; + }; + + let project = workspace.project().clone(); + + let (worktree_root, repository) = { + let project_ref = project.read(cx); + let worktree_root = project_ref + .visible_worktrees(cx) + .next() + .map(|worktree| worktree.read(cx).abs_path()); + let repository = project_ref.active_repository(cx); + (worktree_root, repository) + }; + + let (Some(worktree_root), Some(repository)) = (worktree_root, repository) else { + log::error!("CaptureExampleSpec: missing worktree or active repository"); + return; + }; + + let repository_snapshot = repository.read(cx).snapshot(); + if worktree_root.as_ref() != repository_snapshot.work_directory_abs_path.as_ref() { + log::error!( + "repository is not at worktree root (repo={:?}, worktree={:?})", + repository_snapshot.work_directory_abs_path, + worktree_root + ); + return; + } + + let Some(repository_url) = repository_snapshot + .remote_origin_url + .clone() + .or_else(|| repository_snapshot.remote_upstream_url.clone()) + else { + log::error!("active repository has no origin/upstream remote url"); + return; + }; + + let Some(revision) = repository_snapshot + .head_commit + .as_ref() + .map(|commit| commit.sha.to_string()) + else { + log::error!("active repository has no head commit"); + return; + }; + + let mut events = ep_store.update(cx, |store, cx| { + store.edit_history_for_project_with_pause_split_last_event(&project, cx) + }); + + let Some(editor) = workspace.active_item_as::(cx) else { + log::error!("no active editor"); + return; + }; + + let Some(project_path) = editor.read(cx).project_path(cx) else { + log::error!("active editor has no project path"); + return; + }; + + let Some((buffer, cursor_anchor)) = editor + .read(cx) + .buffer() + .read(cx) + .text_anchor_for_position(editor.read(cx).selections.newest_anchor().head(), cx) + else { + log::error!("failed to resolve cursor buffer/anchor"); + return; + }; + + let snapshot = buffer.read(cx).snapshot(); + let cursor_point = cursor_anchor.to_point(&snapshot); + let (_editable_range, context_range) = + edit_prediction::cursor_excerpt::editable_and_context_ranges_for_cursor_position( + cursor_point, + &snapshot, + 100, + 50, + ); + + let cursor_path: Arc = repository + .read(cx) + .project_path_to_repo_path(&project_path, cx) + .map(|repo_path| Path::new(repo_path.as_unix_str()).into()) + .unwrap_or_else(|| Path::new(project_path.path.as_unix_str()).into()); + + let cursor_position = { + let context_start_offset = context_range.start.to_offset(&snapshot); + let cursor_offset = cursor_anchor.to_offset(&snapshot); + let cursor_offset_in_excerpt = cursor_offset.saturating_sub(context_start_offset); + let mut excerpt = snapshot.text_for_range(context_range).collect::(); + if cursor_offset_in_excerpt <= excerpt.len() { + excerpt.insert_str(cursor_offset_in_excerpt, zeta_prompt::CURSOR_MARKER); + } + excerpt + }; + + let markdown_language = workspace + .app_state() + .languages + .language_for_name("Markdown"); + + cx.spawn_in(window, async move |workspace_entity, cx| { + let markdown_language = markdown_language.await?; + + let uncommitted_diff_rx = repository.update(cx, |repository, cx| { + repository.diff(DiffType::HeadToWorktree, cx) + })?; + + let uncommitted_diff = match uncommitted_diff_rx.await { + Ok(Ok(diff)) => diff, + Ok(Err(error)) => { + log::error!("failed to compute uncommitted diff: {error:#}"); + return Ok(()); + } + Err(error) => { + log::error!("uncommitted diff channel dropped: {error:#}"); + return Ok(()); + } + }; + + let mut edit_history = String::new(); + let mut expected_patch = String::new(); + if let Some(last_event) = events.pop() { + for event in &events { + zeta_prompt::write_event(&mut edit_history, event); + if !edit_history.ends_with('\n') { + edit_history.push('\n'); + } + edit_history.push('\n'); + } + + zeta_prompt::write_event(&mut expected_patch, &last_event); + } + + let format = + time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]"); + let name = match format { + Ok(format) => { + let now = time::OffsetDateTime::now_local() + .unwrap_or_else(|_| time::OffsetDateTime::now_utc()); + now.format(&format) + .unwrap_or_else(|_| "unknown-time".to_string()) + } + Err(_) => "unknown-time".to_string(), + }; + + let markdown = ExampleSpec { + name, + repository_url, + revision, + uncommitted_diff, + cursor_path, + cursor_position, + edit_history, + expected_patch, + } + .to_markdown(); + + let buffer = project + .update(cx, |project, cx| project.create_buffer(false, cx))? + .await?; + buffer.update(cx, |buffer, cx| { + buffer.set_text(markdown, cx); + buffer.set_language(Some(markdown_language), cx); + })?; + + workspace_entity.update_in(cx, |workspace, window, cx| { + workspace.add_item_to_active_pane( + Box::new( + cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)), + ), + None, + true, + window, + cx, + ); + }) + }) + .detach_and_log_err(cx); +} diff --git a/crates/edit_prediction_ui/src/external_provider_api_token_modal.rs b/crates/edit_prediction_ui/src/external_provider_api_token_modal.rs deleted file mode 100644 index bc312836e9fdd30237156ac532a055d1e23a2589..0000000000000000000000000000000000000000 --- a/crates/edit_prediction_ui/src/external_provider_api_token_modal.rs +++ /dev/null @@ -1,86 +0,0 @@ -use edit_prediction::EditPredictionStore; -use gpui::{ - DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, -}; -use ui::{Button, ButtonStyle, Clickable, Headline, HeadlineSize, prelude::*}; -use ui_input::InputField; -use workspace::ModalView; - -pub struct ExternalProviderApiKeyModal { - api_key_input: Entity, - focus_handle: FocusHandle, - on_confirm: Box, &mut EditPredictionStore, &mut App)>, -} - -impl ExternalProviderApiKeyModal { - pub fn new( - window: &mut Window, - cx: &mut Context, - on_confirm: impl Fn(Option, &mut EditPredictionStore, &mut App) + 'static, - ) -> Self { - let api_key_input = cx.new(|cx| InputField::new(window, cx, "Enter your API key")); - - Self { - api_key_input, - focus_handle: cx.focus_handle(), - on_confirm: Box::new(on_confirm), - } - } - - fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent); - } - - fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { - let api_key = self.api_key_input.read(cx).text(cx); - let api_key = (!api_key.trim().is_empty()).then_some(api_key); - - if let Some(ep_store) = EditPredictionStore::try_global(cx) { - ep_store.update(cx, |ep_store, cx| (self.on_confirm)(api_key, ep_store, cx)) - } - - cx.emit(DismissEvent); - } -} - -impl EventEmitter for ExternalProviderApiKeyModal {} - -impl ModalView for ExternalProviderApiKeyModal {} - -impl Focusable for ExternalProviderApiKeyModal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for ExternalProviderApiKeyModal { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("ExternalApiKeyModal") - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::confirm)) - .elevation_2(cx) - .w(px(400.)) - .p_4() - .gap_3() - .child(Headline::new("API Token").size(HeadlineSize::Small)) - .child(self.api_key_input.clone()) - .child( - h_flex() - .justify_end() - .gap_2() - .child(Button::new("cancel", "Cancel").on_click(cx.listener( - |_, _, _window, cx| { - cx.emit(DismissEvent); - }, - ))) - .child( - Button::new("save", "Save") - .style(ButtonStyle::Filled) - .on_click(cx.listener(|this, _, window, cx| { - this.confirm(&menu::Confirm, window, cx); - })), - ), - ) - } -} diff --git a/crates/edit_prediction_ui/src/rate_prediction_modal.rs b/crates/edit_prediction_ui/src/rate_prediction_modal.rs index 54933fbf904f8fc7146dcce9a6bd3340884cc8bf..22e82bc445b394cc122e1cb1aa3604b45c25d1d1 100644 --- a/crates/edit_prediction_ui/src/rate_prediction_modal.rs +++ b/crates/edit_prediction_ui/src/rate_prediction_modal.rs @@ -510,13 +510,13 @@ impl RatePredictionsModal { base_text_style: window.text_style(), syntax: cx.theme().syntax().clone(), code_block: StyleRefinement { - text: Some(TextStyleRefinement { + text: TextStyleRefinement { font_family: Some( theme_settings.buffer_font.family.clone(), ), font_size: Some(buffer_font_size.into()), ..Default::default() - }), + }, padding: EdgesRefinement { top: Some(DefiniteLength::Absolute( AbsoluteLength::Pixels(px(8.)), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cddb20d83e0b9066fcfd882aa5325624cbadf92e..afa62e5ff31436ef178a94dc0ff8bedfc2691e60 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -107,10 +107,11 @@ use gpui::{ AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers, - MouseButton, MouseDownEvent, MouseMoveEvent, PaintQuad, ParentElement, Pixels, Render, - ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, TextRun, TextStyle, - TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, - WeakFocusHandle, Window, div, point, prelude::*, pulsating_between, px, relative, size, + MouseButton, MouseDownEvent, MouseMoveEvent, PaintQuad, ParentElement, Pixels, PressureStage, + Render, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, TextRun, + TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, + WeakEntity, WeakFocusHandle, Window, div, point, prelude::*, pulsating_between, px, relative, + size, }; use hover_links::{HoverLink, HoveredLinkState, find_file}; use hover_popover::{HoverState, hide_hover}; @@ -351,8 +352,8 @@ pub fn init(cx: &mut App) { ) .detach(); } - }); - cx.on_action(move |_: &workspace::NewWindow, cx| { + }) + .on_action(move |_: &workspace::NewWindow, cx| { let app_state = workspace::AppState::global(cx); if let Some(app_state) = app_state.upgrade() { workspace::open_new( @@ -1107,6 +1108,9 @@ pub struct Editor { pending_rename: Option, searchable: bool, cursor_shape: CursorShape, + /// Whether the cursor is offset one character to the left when something is + /// selected (needed for vim visual mode) + cursor_offset_on_selection: bool, current_line_highlight: Option, pub collapse_matches: bool, autoindent_mode: Option, @@ -1118,6 +1122,7 @@ pub struct Editor { remote_id: Option, pub hover_state: HoverState, pending_mouse_down: Option>>>, + prev_pressure_stage: Option, gutter_hovered: bool, hovered_link_state: Option, edit_prediction_provider: Option, @@ -2281,6 +2286,7 @@ impl Editor { cursor_shape: EditorSettings::get_global(cx) .cursor_shape .unwrap_or_default(), + cursor_offset_on_selection: false, current_line_highlight: None, autoindent_mode: Some(AutoindentMode::EachLine), collapse_matches: false, @@ -2296,6 +2302,7 @@ impl Editor { remote_id: None, hover_state: HoverState::default(), pending_mouse_down: None, + prev_pressure_stage: None, hovered_link_state: None, edit_prediction_provider: None, active_edit_prediction: None, @@ -3095,6 +3102,10 @@ impl Editor { self.cursor_shape } + pub fn set_cursor_offset_on_selection(&mut self, set_cursor_offset_on_selection: bool) { + self.cursor_offset_on_selection = set_cursor_offset_on_selection; + } + pub fn set_current_line_highlight( &mut self, current_line_highlight: Option, @@ -5010,6 +5021,9 @@ impl Editor { this.change_selections(Default::default(), window, cx, |s| s.select(new_selections)); this.refresh_edit_prediction(true, false, window, cx); + if let Some(task) = this.trigger_on_type_formatting("\n".to_owned(), window, cx) { + task.detach_and_log_err(cx); + } }); } @@ -5074,6 +5088,9 @@ impl Editor { } } editor.edit(indent_edits, cx); + if let Some(format) = editor.trigger_on_type_formatting("\n".to_owned(), window, cx) { + format.detach_and_log_err(cx); + } }); } @@ -5136,6 +5153,9 @@ impl Editor { } } editor.edit(indent_edits, cx); + if let Some(format) = editor.trigger_on_type_formatting("\n".to_owned(), window, cx) { + format.detach_and_log_err(cx); + } }); } @@ -5446,7 +5466,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option>> { - if input.len() != 1 { + if input.chars().count() != 1 { return None; } @@ -22956,10 +22976,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let workspace = self.workspace(); - let project = self.project(); - let save_tasks = self.buffer().update(cx, |multi_buffer, cx| { - let mut tasks = Vec::new(); + self.buffer().update(cx, |multi_buffer, cx| { for (buffer_id, changes) in revert_changes { if let Some(buffer) = multi_buffer.buffer(buffer_id) { buffer.update(cx, |buffer, cx| { @@ -22971,44 +22988,9 @@ impl Editor { cx, ); }); - - if let Some(project) = - project.filter(|_| multi_buffer.all_diff_hunks_expanded()) - { - project.update(cx, |project, cx| { - tasks.push((buffer.clone(), project.save_buffer(buffer, cx))); - }) - } } } - tasks }); - cx.spawn_in(window, async move |_, cx| { - for (buffer, task) in save_tasks { - let result = task.await; - if result.is_err() { - let Some(path) = buffer - .read_with(cx, |buffer, cx| buffer.project_path(cx)) - .ok() - else { - continue; - }; - if let Some((workspace, path)) = workspace.as_ref().zip(path) { - let Some(task) = cx - .update_window_entity(workspace, |workspace, window, cx| { - workspace - .open_path_preview(path, None, false, false, false, window, cx) - }) - .ok() - else { - continue; - }; - task.await.log_err(); - } - } - } - }) - .detach(); self.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.refresh() }); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 653cf291a7ff2ea79152535392241ae94eaf05f3..3b16fa1be173ab1a5edbc9bbaad20a3d6b1493e7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -48,11 +48,11 @@ use gpui::{ DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, KeybindingKeystroke, Length, Modifiers, 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, + MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement, + Pixels, PressureStage, 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, }; use itertools::Itertools; use language::{IndentGuideSettings, language_settings::ShowWhitespaceSetting}; @@ -132,6 +132,7 @@ impl SelectionLayout { fn new( selection: Selection, line_mode: bool, + cursor_offset: bool, cursor_shape: CursorShape, map: &DisplaySnapshot, is_newest: bool, @@ -152,12 +153,9 @@ impl SelectionLayout { } // any vim visual mode (including line mode) - if (cursor_shape == CursorShape::Block || cursor_shape == CursorShape::Hollow) - && !range.is_empty() - && !selection.reversed - { + if cursor_offset && !range.is_empty() && !selection.reversed { if head.column() > 0 { - head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left) + head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left); } else if head.row().0 > 0 && head != map.max_point() { head = map.clip_point( DisplayPoint::new( @@ -1017,10 +1015,16 @@ impl EditorElement { let pending_nonempty_selections = editor.has_pending_nonempty_selection(); let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&event.modifiers(), cx); + let mouse_down_hovered_link_modifier = if let ClickEvent::Mouse(mouse_event) = event { + Editor::is_cmd_or_ctrl_pressed(&mouse_event.down.modifiers, cx) + } else { + true + }; if let Some(mouse_position) = event.mouse_position() && !pending_nonempty_selections && hovered_link_modifier + && mouse_down_hovered_link_modifier && text_hitbox.is_hovered(window) { let point = position_map.point_for_position(mouse_position); @@ -1031,6 +1035,28 @@ impl EditorElement { } } + fn pressure_click( + editor: &mut Editor, + event: &MousePressureEvent, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + let text_hitbox = &position_map.text_hitbox; + let force_click_possible = + matches!(editor.prev_pressure_stage, Some(PressureStage::Normal)) + && event.stage == PressureStage::Force; + + editor.prev_pressure_stage = Some(event.stage); + + if force_click_possible && text_hitbox.is_hovered(window) { + let point = position_map.point_for_position(event.position); + editor.handle_click_hovered_link(point, event.modifiers, window, cx); + editor.selection_drag_state = SelectionDragState::None; + cx.stop_propagation(); + } + } + fn mouse_dragged( editor: &mut Editor, event: &MouseMoveEvent, @@ -1435,6 +1461,7 @@ impl EditorElement { let layout = SelectionLayout::new( selection, editor.selections.line_mode(), + editor.cursor_offset_on_selection, editor.cursor_shape, &snapshot.display_snapshot, is_newest, @@ -1481,6 +1508,7 @@ impl EditorElement { let drag_cursor_layout = SelectionLayout::new( drop_cursor.clone(), false, + editor.cursor_offset_on_selection, CursorShape::Bar, &snapshot.display_snapshot, false, @@ -1544,6 +1572,7 @@ impl EditorElement { .push(SelectionLayout::new( selection.selection, selection.line_mode, + editor.cursor_offset_on_selection, selection.cursor_shape, &snapshot.display_snapshot, false, @@ -1554,6 +1583,8 @@ impl EditorElement { selections.extend(remote_selections.into_values()); } else if !editor.is_focused(window) && editor.show_cursor_when_unfocused { + let cursor_offset_on_selection = editor.cursor_offset_on_selection; + let layouts = snapshot .buffer_snapshot() .selections_in_range(&(start_anchor..end_anchor), true) @@ -1561,6 +1592,7 @@ impl EditorElement { SelectionLayout::new( selection, line_mode, + cursor_offset_on_selection, cursor_shape, &snapshot.display_snapshot, false, @@ -3284,6 +3316,7 @@ impl EditorElement { SelectionLayout::new( newest, editor.selections.line_mode(), + editor.cursor_offset_on_selection, editor.cursor_shape, &snapshot.display_snapshot, true, @@ -7758,6 +7791,19 @@ impl EditorElement { } }); + window.on_mouse_event({ + let position_map = layout.position_map.clone(); + let editor = self.editor.clone(); + + move |event: &MousePressureEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble { + editor.update(cx, |editor, cx| { + Self::pressure_click(editor, &event, &position_map, window, cx); + }) + } + } + }); + window.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); @@ -11852,7 +11898,7 @@ mod tests { window .update(cx, |editor, window, cx| { - editor.cursor_shape = CursorShape::Block; + editor.cursor_offset_on_selection = true; editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ Point::new(0, 0)..Point::new(1, 0), diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 9d0261f00f8f7258023b092d4f55d40ac8abcf40..ba361aa04dee3bfa3a819c8afb7061c238681b77 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -735,7 +735,7 @@ mod tests { test::editor_lsp_test_context::EditorLspTestContext, }; use futures::StreamExt; - use gpui::Modifiers; + use gpui::{Modifiers, MousePressureEvent, PressureStage}; use indoc::indoc; use lsp::request::{GotoDefinition, GotoTypeDefinition}; use multi_buffer::MultiBufferOffset; @@ -1706,4 +1706,77 @@ mod tests { cx.simulate_click(screen_coord, Modifiers::secondary_key()); cx.update_workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1)); } + + #[gpui::test] + async fn test_pressure_links(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + definition_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + fn ˇtest() { do_work(); } + fn do_work() { test(); } + "}); + + // Position the mouse over a symbol that has a definition + let hover_point = cx.pixel_position(indoc! {" + fn test() { do_wˇork(); } + fn do_work() { test(); } + "}); + let symbol_range = cx.lsp_range(indoc! {" + fn test() { «do_work»(); } + fn do_work() { test(); } + "}); + let target_range = cx.lsp_range(indoc! {" + fn test() { do_work(); } + fn «do_work»() { test(); } + "}); + + let mut requests = + cx.set_request_handler::(move |url, _, _| async move { + Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ + lsp::LocationLink { + origin_selection_range: Some(symbol_range), + target_uri: url.clone(), + target_range, + target_selection_range: target_range, + }, + ]))) + }); + + cx.simulate_mouse_move(hover_point, None, Modifiers::none()); + + // First simulate Normal pressure to set up the previous stage + cx.simulate_event(MousePressureEvent { + pressure: 0.5, + stage: PressureStage::Normal, + position: hover_point, + modifiers: Modifiers::none(), + }); + cx.background_executor.run_until_parked(); + + // Now simulate Force pressure to trigger the force click and go-to definition + cx.simulate_event(MousePressureEvent { + pressure: 1.0, + stage: PressureStage::Force, + position: hover_point, + modifiers: Modifiers::none(), + }); + requests.next().await; + cx.background_executor.run_until_parked(); + + // Assert that we navigated to the definition + cx.assert_editor_state(indoc! {" + fn test() { do_work(); } + fn «do_workˇ»() { test(); } + "}); + } } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index edf10671b9e4c63e2918f6e144ba1b553e44daca..7c3e41e8c2edf721fbcae729069eecb640e2246c 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -151,7 +151,7 @@ pub fn hover_at_inlay( false }) { - hide_hover(editor, cx); + return; } let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 3b9c17f80f10116f2302bab203966922cbf0bcb2..cfbb7c975c844f08d76a5568f1e02dfe3d7d74f1 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -842,7 +842,6 @@ impl Item for Editor { .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone())) .collect::>(); - // let mut buffers_to_save = let buffers_to_save = if self.buffer.read(cx).is_singleton() && !options.autosave { buffers } else { diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index 8a413a376f2296acdacddc97707a6112e8cd5185..b5090f06dc1e68d609413db31112775e56559689 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -194,7 +194,7 @@ impl SplittableEditor { }); let primary_pane = self.panes.first_pane(); self.panes - .split(&primary_pane, &secondary_pane, SplitDirection::Left) + .split(&primary_pane, &secondary_pane, SplitDirection::Left, cx) .unwrap(); cx.notify(); } @@ -203,7 +203,7 @@ impl SplittableEditor { let Some(secondary) = self.secondary.take() else { return; }; - self.panes.remove(&secondary.pane).unwrap(); + self.panes.remove(&secondary.pane, cx).unwrap(); self.primary_editor.update(cx, |primary, cx| { primary.buffer().update(cx, |buffer, _| { buffer.set_filter_mode(None); diff --git a/crates/eval_utils/src/eval_utils.rs b/crates/eval_utils/src/eval_utils.rs index 880b1a97e414bbc3219bdf8f7163dbf9b6c9c82b..be3294ed1490d6a602c3a5282d25dbba7d065443 100644 --- a/crates/eval_utils/src/eval_utils.rs +++ b/crates/eval_utils/src/eval_utils.rs @@ -40,6 +40,24 @@ pub struct EvalOutput { pub metadata: M, } +impl EvalOutput { + pub fn passed(message: impl Into) -> Self { + EvalOutput { + outcome: OutcomeKind::Passed, + data: message.into(), + metadata: M::default(), + } + } + + pub fn failed(message: impl Into) -> Self { + EvalOutput { + outcome: OutcomeKind::Failed, + data: message.into(), + metadata: M::default(), + } + } +} + pub struct NoProcessor; impl EvalOutputProcessor for NoProcessor { type Metadata = (); diff --git a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit index 6ce2a344d245a7297cae002ab529fd39937e00cc..04d3861be0cc483978e11108e9bfca921f07d37c 100644 --- a/crates/extension_api/wit/since_v0.8.0/llm-provider.wit +++ b/crates/extension_api/wit/since_v0.8.0/llm-provider.wit @@ -107,6 +107,8 @@ interface llm-provider { name: string, /// JSON string of the tool input arguments. input: string, + /// Whether the input JSON is complete (false while streaming, true when done). + is-input-complete: bool, /// Thought signature for providers that support it (e.g., Anthropic). thought-signature: option, } diff --git a/crates/extension_host/src/wasm_host/llm_provider.rs b/crates/extension_host/src/wasm_host/llm_provider.rs index 68e48e825f3430345ba6398011816a8448ef40f6..100190ed1335f9cff01b2fc0cce00b221aef61ef 100644 --- a/crates/extension_host/src/wasm_host/llm_provider.rs +++ b/crates/extension_host/src/wasm_host/llm_provider.rs @@ -1424,6 +1424,7 @@ fn convert_request_to_wit(request: LanguageModelRequest) -> LlmCompletionRequest id: tool_use.id.to_string(), name: tool_use.name.to_string(), input: serde_json::to_string(&tool_use.input).unwrap_or_default(), + is_input_complete: tool_use.is_input_complete, thought_signature: tool_use.thought_signature, }), MessageContent::ToolResult(tool_result) => { @@ -1514,7 +1515,7 @@ fn convert_completion_event( name: tool_use.name.into(), raw_input, input, - is_input_complete: true, + is_input_complete: tool_use.is_input_complete, thought_signature: tool_use.thought_signature, }, )) diff --git a/crates/feature_flags/src/flags.rs b/crates/feature_flags/src/flags.rs index 61d9a34e38de546c79a2dbb5f889e2fddad38480..1768e43d1d0a88433d61c6390f912377c2ba55e3 100644 --- a/crates/feature_flags/src/flags.rs +++ b/crates/feature_flags/src/flags.rs @@ -12,12 +12,14 @@ impl FeatureFlag for PanicFeatureFlag { const NAME: &'static str = "panic"; } -pub struct InlineAssistantV2FeatureFlag; +pub struct InlineAssistantUseToolFeatureFlag; -impl FeatureFlag for InlineAssistantV2FeatureFlag { - const NAME: &'static str = "inline-assistant-v2"; +impl FeatureFlag for InlineAssistantUseToolFeatureFlag { + const NAME: &'static str = "inline-assistant-use-tool"; +} + +pub struct AgentV2FeatureFlag; - fn enabled_for_staff() -> bool { - false - } +impl FeatureFlag for AgentV2FeatureFlag { + const NAME: &'static str = "agent-v2"; } diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 2ae0c47776acb5c58b7d0919aa7522fb64d923d0..f75d0ee99dc32bc1a1ab812328bba3d36fcb2953 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -44,8 +44,9 @@ impl OpenPathDelegate { tx: oneshot::Sender>>, lister: DirectoryLister, creating_path: bool, - path_style: PathStyle, + cx: &App, ) -> Self { + let path_style = lister.path_style(cx); Self { tx: Some(tx), lister, @@ -216,8 +217,7 @@ impl OpenPathPrompt { cx: &mut Context, ) { workspace.toggle_modal(window, cx, |window, cx| { - let delegate = - OpenPathDelegate::new(tx, lister.clone(), creating_path, PathStyle::local()); + let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, cx); let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.)); let query = lister.default_query(cx); picker.set_query(query, window, cx); diff --git a/crates/file_finder/src/open_path_prompt_tests.rs b/crates/file_finder/src/open_path_prompt_tests.rs index dea188034bfa7ae46f5b17c50424b40331fadb75..9af18c8a6bd82b389d4d18a997c3b5fe4a088730 100644 --- a/crates/file_finder/src/open_path_prompt_tests.rs +++ b/crates/file_finder/src/open_path_prompt_tests.rs @@ -5,7 +5,7 @@ use picker::{Picker, PickerDelegate}; use project::Project; use serde_json::json; use ui::rems; -use util::{path, paths::PathStyle}; +use util::path; use workspace::{AppState, Workspace}; use crate::OpenPathDelegate; @@ -37,7 +37,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx); + let (picker, cx) = build_open_path_prompt(project, false, cx); insert_query(path!("sadjaoislkdjasldj"), &picker, cx).await; assert_eq!(collect_match_candidates(&picker, cx), Vec::::new()); @@ -119,7 +119,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx); + let (picker, cx) = build_open_path_prompt(project, false, cx); // Confirm completion for the query "/root", since it's a directory, it should add a trailing slash. let query = path!("/root"); @@ -227,7 +227,7 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (picker, cx) = build_open_path_prompt(project, false, PathStyle::local(), cx); + let (picker, cx) = build_open_path_prompt(project, false, cx); // Support both forward and backward slashes. let query = "C:/root/"; @@ -295,56 +295,6 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) { ); } -#[gpui::test] -#[cfg_attr(not(target_os = "windows"), ignore)] -async fn test_open_path_prompt_on_windows_with_remote(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "a": "A", - "dir1": {}, - "dir2": {} - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - - let (picker, cx) = build_open_path_prompt(project, false, PathStyle::Posix, cx); - - let query = "/root/"; - insert_query(query, &picker, cx).await; - assert_eq!( - collect_match_candidates(&picker, cx), - vec!["./", "a", "dir1", "dir2"] - ); - assert_eq!( - confirm_completion(query, 1, &picker, cx).unwrap(), - "/root/a" - ); - - // Confirm completion for the query "/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash. - let query = "/root/d"; - insert_query(query, &picker, cx).await; - assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]); - assert_eq!( - confirm_completion(query, 1, &picker, cx).unwrap(), - "/root/dir2/" - ); - - let query = "/root/d"; - insert_query(query, &picker, cx).await; - assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]); - assert_eq!( - confirm_completion(query, 0, &picker, cx).unwrap(), - "/root/dir1/" - ); -} - #[gpui::test] async fn test_new_path_prompt(cx: &mut TestAppContext) { let app_state = init_test(cx); @@ -372,7 +322,7 @@ async fn test_new_path_prompt(cx: &mut TestAppContext) { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (picker, cx) = build_open_path_prompt(project, true, PathStyle::local(), cx); + let (picker, cx) = build_open_path_prompt(project, true, cx); insert_query(path!("/root"), &picker, cx).await; assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]); @@ -406,16 +356,15 @@ fn init_test(cx: &mut TestAppContext) -> Arc { fn build_open_path_prompt( project: Entity, creating_path: bool, - path_style: PathStyle, cx: &mut TestAppContext, ) -> (Entity>, &mut VisualTestContext) { let (tx, _) = futures::channel::oneshot::channel(); let lister = project::DirectoryLister::Project(project.clone()); - let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, path_style); let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); ( workspace.update_in(cx, |_, window, cx| { + let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, cx); cx.new(|cx| { let picker = Picker::uniform_list(delegate, window, cx) .width(rems(34.)) diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index beaf192b0ef538fb524ff4986710255040b89f27..6747daa09d2801ad8c05c17fb04cb3ab235cdbff 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -74,6 +74,7 @@ gpui = { workspace = true, features = ["test-support"] } indoc.workspace = true pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } +rand.workspace = true settings = { workspace = true, features = ["test-support"] } unindent.workspace = true workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 90b5c4bb284112c8a13ad406da2b7424e982298a..79cd89d1485f6d99349b43d92c17261cf8a644e2 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -6,7 +6,7 @@ use collections::HashSet; use git::repository::Branch; use gpui::http_client::Url; use gpui::{ - Action, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems, }; @@ -17,8 +17,8 @@ use settings::Settings; use std::sync::Arc; use time::OffsetDateTime; use ui::{ - CommonAnimationExt, Divider, HighlightedLabel, KeyBinding, ListHeader, ListItem, - ListItemSpacing, Tooltip, prelude::*, + Divider, HighlightedLabel, KeyBinding, ListHeader, ListItem, ListItemSpacing, Tooltip, + prelude::*, }; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; @@ -232,21 +232,12 @@ impl BranchList { window: &mut Window, cx: &mut Context, ) { - self.picker.update(cx, |this, cx| { - this.delegate.display_remotes = !this.delegate.display_remotes; - cx.spawn_in(window, async move |this, cx| { - this.update_in(cx, |picker, window, cx| { - let last_query = picker.delegate.last_query.clone(); - picker.delegate.update_matches(last_query, window, cx) - })? - .await; - - Result::Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); + self.picker.update(cx, |picker, cx| { + picker.delegate.branch_filter = picker.delegate.branch_filter.invert(); + picker.update_matches(picker.query(cx), window, cx); + picker.refresh_placeholder(window, cx); + cx.notify(); }); - - cx.notify(); } } impl ModalView for BranchList {} @@ -289,6 +280,10 @@ enum Entry { NewBranch { name: String, }, + NewRemoteName { + name: String, + url: SharedString, + }, } impl Entry { @@ -304,6 +299,7 @@ impl Entry { Entry::Branch { branch, .. } => branch.name(), Entry::NewUrl { url, .. } => url.as_str(), Entry::NewBranch { name, .. } => name.as_str(), + Entry::NewRemoteName { name, .. } => name.as_str(), } } @@ -318,6 +314,23 @@ impl Entry { } } +#[derive(Clone, Copy, PartialEq)] +enum BranchFilter { + /// Only show local branches + Local, + /// Only show remote branches + Remote, +} + +impl BranchFilter { + fn invert(&self) -> Self { + match self { + BranchFilter::Local => BranchFilter::Remote, + BranchFilter::Remote => BranchFilter::Local, + } + } +} + pub struct BranchListDelegate { workspace: Option>, matches: Vec, @@ -328,9 +341,8 @@ pub struct BranchListDelegate { selected_index: usize, last_query: String, modifiers: Modifiers, - display_remotes: bool, + branch_filter: BranchFilter, state: PickerState, - loading: bool, focus_handle: FocusHandle, } @@ -363,9 +375,8 @@ impl BranchListDelegate { selected_index: 0, last_query: Default::default(), modifiers: Default::default(), - display_remotes: false, + branch_filter: BranchFilter::Local, state: PickerState::List, - loading: false, focus_handle: cx.focus_handle(), } } @@ -406,37 +417,13 @@ impl BranchListDelegate { let Some(repo) = self.repo.clone() else { return; }; - cx.spawn(async move |this, cx| { - this.update(cx, |picker, cx| { - picker.delegate.loading = true; - cx.notify(); - }) - .log_err(); - let stop_loader = |this: &WeakEntity>, cx: &mut AsyncApp| { - this.update(cx, |picker, cx| { - picker.delegate.loading = false; - cx.notify(); - }) - .log_err(); - }; - repo.update(cx, |repo, _| repo.create_remote(remote_name, remote_url)) - .inspect_err(|_err| { - stop_loader(&this, cx); - })? - .await - .inspect_err(|_err| { - stop_loader(&this, cx); - })? - .inspect_err(|_err| { - stop_loader(&this, cx); - })?; - stop_loader(&this, cx); - Ok(()) - }) - .detach_and_prompt_err("Failed to create remote", window, cx, |e, _, _cx| { - Some(e.to_string()) - }); + let receiver = repo.update(cx, |repo, _| repo.create_remote(remote_name, remote_url)); + + cx.background_spawn(async move { receiver.await? }) + .detach_and_prompt_err("Failed to create remote", window, cx, |e, _, _cx| { + Some(e.to_string()) + }); cx.emit(DismissEvent); } @@ -528,29 +515,33 @@ impl PickerDelegate for BranchListDelegate { type ListItem = ListItem; fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select branch…".into() + match self.state { + PickerState::List | PickerState::NewRemote | PickerState::NewBranch => { + match self.branch_filter { + BranchFilter::Local => "Select branch…", + BranchFilter::Remote => "Select remote…", + } + } + PickerState::CreateRemote(_) => "Enter a name for this remote…", + } + .into() + } + + fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { + match self.state { + PickerState::CreateRemote(_) => { + Some(SharedString::new_static("Remote name can't be empty")) + } + _ => None, + } } fn render_editor( &self, editor: &Entity, - window: &mut Window, - cx: &mut Context>, + _window: &mut Window, + _cx: &mut Context>, ) -> Div { - cx.update_entity(editor, move |editor, cx| { - let placeholder = match self.state { - PickerState::List | PickerState::NewRemote | PickerState::NewBranch => { - if self.display_remotes { - "Select remote…" - } else { - "Select branch…" - } - } - PickerState::CreateRemote(_) => "Choose a name…", - }; - editor.set_placeholder_text(placeholder, window, cx); - }); - let focus_handle = self.focus_handle.clone(); v_flex() @@ -568,16 +559,14 @@ impl PickerDelegate for BranchListDelegate { .when( self.editor_position() == PickerEditorPosition::End, |this| { - let tooltip_label = if self.display_remotes { - "Turn Off Remote Filter" - } else { - "Filter Remote Branches" + let tooltip_label = match self.branch_filter { + BranchFilter::Local => "Turn Off Remote Filter", + BranchFilter::Remote => "Filter Remote Branches", }; this.gap_1().justify_between().child({ IconButton::new("filter-remotes", IconName::Filter) - .disabled(self.loading) - .toggle_state(self.display_remotes) + .toggle_state(self.branch_filter == BranchFilter::Remote) .tooltip(move |_, cx| { Tooltip::for_action_in( tooltip_label, @@ -636,20 +625,18 @@ impl PickerDelegate for BranchListDelegate { return Task::ready(()); }; - const RECENT_BRANCHES_COUNT: usize = 10; - let display_remotes = self.display_remotes; + let display_remotes = self.branch_filter; cx.spawn_in(window, async move |picker, cx| { let mut matches: Vec = if query.is_empty() { all_branches .into_iter() .filter(|branch| { - if display_remotes { + if display_remotes == BranchFilter::Remote { branch.is_remote() } else { !branch.is_remote() } }) - .take(RECENT_BRANCHES_COUNT) .map(|branch| Entry::Branch { branch, positions: Vec::new(), @@ -659,7 +646,7 @@ impl PickerDelegate for BranchListDelegate { let branches = all_branches .iter() .filter(|branch| { - if display_remotes { + if display_remotes == BranchFilter::Remote { branch.is_remote() } else { !branch.is_remote() @@ -690,11 +677,19 @@ impl PickerDelegate for BranchListDelegate { }; picker .update(cx, |picker, _| { - if matches!(picker.delegate.state, PickerState::CreateRemote(_)) { + if let PickerState::CreateRemote(url) = &picker.delegate.state { + let query = query.replace(' ', "-"); + if !query.is_empty() { + picker.delegate.matches = vec![Entry::NewRemoteName { + name: query.clone(), + url: url.clone(), + }]; + picker.delegate.selected_index = 0; + } else { + picker.delegate.matches = Vec::new(); + picker.delegate.selected_index = 0; + } picker.delegate.last_query = query; - picker.delegate.matches = Vec::new(); - picker.delegate.selected_index = 0; - return; } @@ -738,13 +733,6 @@ impl PickerDelegate for BranchListDelegate { } fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { - if let PickerState::CreateRemote(remote_url) = &self.state { - self.create_remote(self.last_query.clone(), remote_url.to_string(), window, cx); - self.state = PickerState::List; - cx.notify(); - return; - } - let Some(entry) = self.matches.get(self.selected_index()) else { return; }; @@ -787,13 +775,19 @@ impl PickerDelegate for BranchListDelegate { self.state = PickerState::CreateRemote(url.clone().into()); self.matches = Vec::new(); self.selected_index = 0; - cx.spawn_in(window, async move |this, cx| { - this.update_in(cx, |picker, window, cx| { - picker.set_query("", window, cx); - }) - }) - .detach_and_log_err(cx); - cx.notify(); + + cx.defer_in(window, |picker, window, cx| { + picker.refresh_placeholder(window, cx); + picker.set_query("", window, cx); + cx.notify(); + }); + + // returning early to prevent dismissing the modal, so a user can enter + // a remote name first. + return; + } + Entry::NewRemoteName { name, url } => { + self.create_remote(name.clone(), url.to_string(), window, cx); } Entry::NewBranch { name } => { let from_branch = if secondary { @@ -844,17 +838,13 @@ impl PickerDelegate for BranchListDelegate { .unwrap_or_else(|| (None, None, None)); let entry_icon = match entry { - Entry::NewUrl { .. } | Entry::NewBranch { .. } => { + Entry::NewUrl { .. } | Entry::NewBranch { .. } | Entry::NewRemoteName { .. } => { Icon::new(IconName::Plus).color(Color::Muted) } - - Entry::Branch { .. } => { - if self.display_remotes { - Icon::new(IconName::Screen).color(Color::Muted) - } else { - Icon::new(IconName::GitBranchAlt).color(Color::Muted) - } - } + Entry::Branch { .. } => match self.branch_filter { + BranchFilter::Local => Icon::new(IconName::GitBranchAlt).color(Color::Muted), + BranchFilter::Remote => Icon::new(IconName::Screen).color(Color::Muted), + }, }; let entry_title = match entry { @@ -866,6 +856,10 @@ impl PickerDelegate for BranchListDelegate { .single_line() .truncate() .into_any_element(), + Entry::NewRemoteName { name, .. } => Label::new(format!("Create Remote: \"{name}\"")) + .single_line() + .truncate() + .into_any_element(), Entry::Branch { branch, positions } => { HighlightedLabel::new(branch.name().to_string(), positions.clone()) .single_line() @@ -875,7 +869,10 @@ impl PickerDelegate for BranchListDelegate { }; let focus_handle = self.focus_handle.clone(); - let is_new_items = matches!(entry, Entry::NewUrl { .. } | Entry::NewBranch { .. }); + let is_new_items = matches!( + entry, + Entry::NewUrl { .. } | Entry::NewBranch { .. } | Entry::NewRemoteName { .. } + ); let delete_branch_button = IconButton::new("delete", IconName::Trash) .tooltip(move |_, cx| { @@ -937,6 +934,9 @@ impl PickerDelegate for BranchListDelegate { Entry::NewUrl { url } => { format!("Based off {url}") } + Entry::NewRemoteName { url, .. } => { + format!("Based off {url}") + } Entry::NewBranch { .. } => { if let Some(current_branch) = self.repo.as_ref().and_then(|repo| { @@ -1035,10 +1035,9 @@ impl PickerDelegate for BranchListDelegate { _cx: &mut Context>, ) -> Option { matches!(self.state, PickerState::List).then(|| { - let label = if self.display_remotes { - "Remote" - } else { - "Local" + let label = match self.branch_filter { + BranchFilter::Local => "Local", + BranchFilter::Remote => "Remote", }; ListHeader::new(label).inset(true).into_any_element() @@ -1049,11 +1048,7 @@ impl PickerDelegate for BranchListDelegate { if self.editor_position() == PickerEditorPosition::End { return None; } - let focus_handle = self.focus_handle.clone(); - let loading_icon = Icon::new(IconName::LoadCircle) - .size(IconSize::Small) - .with_rotate_animation(3); let footer_container = || { h_flex() @@ -1092,7 +1087,6 @@ impl PickerDelegate for BranchListDelegate { .gap_1() .child( Button::new("delete-branch", "Delete") - .disabled(self.loading) .key_binding( KeyBinding::for_action_in( &branch_picker::DeleteBranch, @@ -1140,17 +1134,15 @@ impl PickerDelegate for BranchListDelegate { ) }, ) - } else if self.loading { - this.justify_between() - .child(loading_icon) - .child(delete_and_select_btns) } else { this.justify_between() .child({ let focus_handle = focus_handle.clone(); Button::new("filter-remotes", "Filter Remotes") - .disabled(self.loading) - .toggle_state(self.display_remotes) + .toggle_state(matches!( + self.branch_filter, + BranchFilter::Remote + )) .key_binding( KeyBinding::for_action_in( &branch_picker::FilterRemotes, @@ -1215,14 +1207,15 @@ impl PickerDelegate for BranchListDelegate { footer_container() .justify_end() .child( - Label::new("Choose a name for this remote repository") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Label::new("Save") - .size(LabelSize::Small) - .color(Color::Muted), + Button::new("branch-from-default", "Confirm") + .key_binding( + KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(cx.listener(|this, _, window, cx| { + this.delegate.confirm(false, window, cx); + })) + .disabled(self.last_query.is_empty()), ) .into_any_element(), ), @@ -1239,6 +1232,7 @@ mod tests { use git::repository::{CommitSummary, Remote}; use gpui::{TestAppContext, VisualTestContext}; use project::{FakeFs, Project}; + use rand::{Rng, rngs::StdRng}; use serde_json::json; use settings::SettingsStore; use util::path; @@ -1286,10 +1280,10 @@ mod tests { } fn init_branch_list_test( - cx: &mut TestAppContext, repository: Option>, branches: Vec, - ) -> (VisualTestContext, Entity) { + cx: &mut TestAppContext, + ) -> (Entity, VisualTestContext) { let window = cx.add_window(|window, cx| { let mut delegate = BranchListDelegate::new(None, repository, BranchListStyle::Modal, cx); @@ -1315,7 +1309,7 @@ mod tests { let branch_list = window.root(cx).unwrap(); let cx = VisualTestContext::from_window(*window, cx); - (cx, branch_list) + (branch_list, cx) } async fn init_fake_repository(cx: &mut TestAppContext) -> Entity { @@ -1349,7 +1343,7 @@ mod tests { init_test(cx); let branches = create_test_branches(); - let (mut ctx, branch_list) = init_branch_list_test(cx, None, branches); + let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx); let cx = &mut ctx; branch_list @@ -1425,7 +1419,7 @@ mod tests { .await; cx.run_until_parked(); - let (mut ctx, branch_list) = init_branch_list_test(cx, repository.into(), branches); + let (branch_list, mut ctx) = init_branch_list_test(repository.into(), branches, cx); let cx = &mut ctx; update_branch_list_matches_with_empty_query(&branch_list, cx).await; @@ -1490,12 +1484,12 @@ mod tests { .await; cx.run_until_parked(); - let (mut ctx, branch_list) = init_branch_list_test(cx, repository.into(), branches); + let (branch_list, mut ctx) = init_branch_list_test(repository.into(), branches, cx); let cx = &mut ctx; // Enable remote filter branch_list.update(cx, |branch_list, cx| { branch_list.picker.update(cx, |picker, _cx| { - picker.delegate.display_remotes = true; + picker.delegate.branch_filter = BranchFilter::Remote; }); }); update_branch_list_matches_with_empty_query(&branch_list, cx).await; @@ -1548,7 +1542,7 @@ mod tests { create_test_branch("develop", false, None, Some(700)), ]; - let (mut ctx, branch_list) = init_branch_list_test(cx, None, branches); + let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx); let cx = &mut ctx; update_branch_list_matches_with_empty_query(&branch_list, cx).await; @@ -1575,7 +1569,7 @@ mod tests { let last_match = picker.delegate.matches.last().unwrap(); assert!(!last_match.is_new_branch()); assert!(!last_match.is_new_url()); - picker.delegate.display_remotes = true; + picker.delegate.branch_filter = BranchFilter::Remote; picker.delegate.update_matches(String::new(), window, cx) }) }) @@ -1602,7 +1596,7 @@ mod tests { // Verify the last entry is NOT the "create new branch" option let last_match = picker.delegate.matches.last().unwrap(); assert!(!last_match.is_new_url()); - picker.delegate.display_remotes = true; + picker.delegate.branch_filter = BranchFilter::Remote; picker .delegate .update_matches(String::from("fork"), window, cx) @@ -1631,22 +1625,27 @@ mod tests { #[gpui::test] async fn test_new_branch_creation_with_query(test_cx: &mut TestAppContext) { + const MAIN_BRANCH: &str = "main"; + const FEATURE_BRANCH: &str = "feature"; + const NEW_BRANCH: &str = "new-feature-branch"; + init_test(test_cx); let repository = init_fake_repository(test_cx).await; let branches = vec![ - create_test_branch("main", true, None, Some(1000)), - create_test_branch("feature", false, None, Some(900)), + create_test_branch(MAIN_BRANCH, true, None, Some(1000)), + create_test_branch(FEATURE_BRANCH, false, None, Some(900)), ]; - let (mut ctx, branch_list) = init_branch_list_test(test_cx, repository.into(), branches); + let (branch_list, mut ctx) = init_branch_list_test(repository.into(), branches, test_cx); let cx = &mut ctx; branch_list .update_in(cx, |branch_list, window, cx| { branch_list.picker.update(cx, |picker, cx| { - let query = "new-feature-branch".to_string(); - picker.delegate.update_matches(query, window, cx) + picker + .delegate + .update_matches(NEW_BRANCH.to_string(), window, cx) }) }) .await; @@ -1657,7 +1656,7 @@ mod tests { branch_list.picker.update(cx, |picker, cx| { let last_match = picker.delegate.matches.last().unwrap(); assert!(last_match.is_new_branch()); - assert_eq!(last_match.name(), "new-feature-branch"); + assert_eq!(last_match.name(), NEW_BRANCH); // State is NewBranch because no existing branches fuzzy-match the query assert!(matches!(picker.delegate.state, PickerState::NewBranch)); picker.delegate.confirm(false, window, cx); @@ -1682,11 +1681,11 @@ mod tests { let new_branch = branches .into_iter() - .find(|branch| branch.name() == "new-feature-branch") + .find(|branch| branch.name() == NEW_BRANCH) .expect("new-feature-branch should exist"); assert_eq!( new_branch.ref_name.as_ref(), - "refs/heads/new-feature-branch", + &format!("refs/heads/{NEW_BRANCH}"), "branch ref_name should not have duplicate refs/heads/ prefix" ); } @@ -1697,7 +1696,7 @@ mod tests { let repository = init_fake_repository(cx).await; let branches = vec![create_test_branch("main", true, None, Some(1000))]; - let (mut ctx, branch_list) = init_branch_list_test(cx, repository.into(), branches); + let (branch_list, mut ctx) = init_branch_list_test(repository.into(), branches, cx); let cx = &mut ctx; branch_list @@ -1736,8 +1735,13 @@ mod tests { branch_list.update_in(cx, |branch_list, window, cx| { branch_list.picker.update(cx, |picker, cx| { + assert_eq!(picker.delegate.matches.len(), 1); + assert!(matches!( + picker.delegate.matches.first(), + Some(Entry::NewRemoteName { name, url }) + if name == "my_new_remote" && url.as_ref() == "https://github.com/user/repo.git" + )); picker.delegate.confirm(false, window, cx); - assert_eq!(picker.delegate.matches.len(), 0); }) }); cx.run_until_parked(); @@ -1770,7 +1774,7 @@ mod tests { init_test(cx); let branches = vec![create_test_branch("main_branch", true, None, Some(1000))]; - let (mut ctx, branch_list) = init_branch_list_test(cx, None, branches); + let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx); let cx = &mut ctx; branch_list @@ -1825,4 +1829,79 @@ mod tests { }) }); } + + #[gpui::test] + async fn test_confirm_remote_url_does_not_dismiss(cx: &mut TestAppContext) { + const REMOTE_URL: &str = "https://github.com/user/repo.git"; + + init_test(cx); + let branches = vec![create_test_branch("main", true, None, Some(1000))]; + + let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx); + let cx = &mut ctx; + + let subscription = cx.update(|_, cx| { + cx.subscribe(&branch_list, |_, _: &DismissEvent, _| { + panic!("DismissEvent should not be emitted when confirming a remote URL"); + }) + }); + + branch_list + .update_in(cx, |branch_list, window, cx| { + window.focus(&branch_list.picker_focus_handle); + branch_list.picker.update(cx, |picker, cx| { + picker + .delegate + .update_matches(REMOTE_URL.to_string(), window, cx) + }) + }) + .await; + + cx.run_until_parked(); + + branch_list.update_in(cx, |branch_list, window, cx| { + branch_list.picker.update(cx, |picker, cx| { + let last_match = picker.delegate.matches.last().unwrap(); + assert!(last_match.is_new_url()); + assert!(matches!(picker.delegate.state, PickerState::NewRemote)); + + picker.delegate.confirm(false, window, cx); + + assert!( + matches!(picker.delegate.state, PickerState::CreateRemote(ref url) if url.as_ref() == REMOTE_URL), + "State should transition to CreateRemote with the URL" + ); + }); + + assert!( + branch_list.picker_focus_handle.is_focused(window), + "Branch list picker should still be focused after confirming remote URL" + ); + }); + + cx.run_until_parked(); + + drop(subscription); + } + + #[gpui::test(iterations = 10)] + async fn test_empty_query_displays_all_branches(mut rng: StdRng, cx: &mut TestAppContext) { + init_test(cx); + let branch_count = rng.random_range(13..540); + + let branches: Vec = (0..branch_count) + .map(|i| create_test_branch(&format!("branch-{:02}", i), i == 0, None, Some(i * 100))) + .collect(); + + let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx); + let cx = &mut ctx; + + update_branch_list_matches_with_empty_query(&branch_list, cx).await; + + branch_list.update(cx, |branch_list, cx| { + branch_list.picker.update(cx, |picker, _cx| { + assert_eq!(picker.delegate.matches.len(), branch_count as usize); + }) + }); + } } diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index b83ad6d8a6ddab467eb32c31cbc67810b9f74247..8cb9d82826086371950d2c51fd06381dd013251f 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -391,14 +391,16 @@ impl CommitView { time_format::TimestampFormat::MediumAbsolute, ); - let github_url = self.remote.as_ref().map(|remote| { - format!( + let remote_info = self.remote.as_ref().map(|remote| { + let provider = remote.host.name(); + let url = format!( "{}/{}/{}/commit/{}", remote.host.base_url(), remote.owner, remote.repo, commit.sha - ) + ); + (provider, url) }); let (additions, deletions) = self.calculate_changed_lines(cx); @@ -472,9 +474,14 @@ impl CommitView { .children(commit_diff_stat), ), ) - .children(github_url.map(|url| { - Button::new("view_on_github", "View on GitHub") - .icon(IconName::Github) + .children(remote_info.map(|(provider_name, url)| { + let icon = match provider_name.as_str() { + "GitHub" => IconName::Github, + _ => IconName::Link, + }; + + Button::new("view_on_provider", format!("View on {}", provider_name)) + .icon(icon) .icon_color(Color::Muted) .icon_size(IconSize::Small) .icon_position(IconPosition::Start) diff --git a/crates/git_ui/src/file_history_view.rs b/crates/git_ui/src/file_history_view.rs index 5b3588d29678ec406749ec45be3de154fd71c5f8..4e91fe7e06a5823caac5bf00be8f48cc98dc8da4 100644 --- a/crates/git_ui/src/file_history_view.rs +++ b/crates/git_ui/src/file_history_view.rs @@ -4,7 +4,8 @@ use git::repository::{FileHistory, FileHistoryEntry, RepoPath}; use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url}; use gpui::{ AnyElement, AnyEntity, App, Asset, Context, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, Render, Task, UniformListScrollHandle, WeakEntity, Window, actions, uniform_list, + IntoElement, Render, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity, Window, + actions, uniform_list, }; use project::{ Project, ProjectPath, @@ -191,6 +192,93 @@ impl FileHistoryView { task.detach(); } + fn select_next(&mut self, _: &menu::SelectNext, _: &mut Window, cx: &mut Context) { + let entry_count = self.history.entries.len(); + let ix = match self.selected_entry { + _ if entry_count == 0 => None, + None => Some(0), + Some(ix) => { + if ix == entry_count - 1 { + Some(0) + } else { + Some(ix + 1) + } + } + }; + self.select_ix(ix, cx); + } + + fn select_previous( + &mut self, + _: &menu::SelectPrevious, + _: &mut Window, + cx: &mut Context, + ) { + let entry_count = self.history.entries.len(); + let ix = match self.selected_entry { + _ if entry_count == 0 => None, + None => Some(entry_count - 1), + Some(ix) => { + if ix == 0 { + Some(entry_count - 1) + } else { + Some(ix - 1) + } + } + }; + self.select_ix(ix, cx); + } + + fn select_first(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context) { + let entry_count = self.history.entries.len(); + let ix = if entry_count != 0 { Some(0) } else { None }; + self.select_ix(ix, cx); + } + + fn select_last(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context) { + let entry_count = self.history.entries.len(); + let ix = if entry_count != 0 { + Some(entry_count - 1) + } else { + None + }; + self.select_ix(ix, cx); + } + + fn select_ix(&mut self, ix: Option, cx: &mut Context) { + self.selected_entry = ix; + if let Some(ix) = ix { + self.scroll_handle.scroll_to_item(ix, ScrollStrategy::Top); + } + cx.notify(); + } + + fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { + self.open_commit_view(window, cx); + } + + fn open_commit_view(&mut self, window: &mut Window, cx: &mut Context) { + let Some(entry) = self + .selected_entry + .and_then(|ix| self.history.entries.get(ix)) + else { + return; + }; + + if let Some(repo) = self.repository.upgrade() { + let sha_str = entry.sha.to_string(); + CommitView::open( + sha_str, + repo.downgrade(), + self.workspace.clone(), + None, + Some(self.history.path.clone()), + window, + cx, + ); + } + } + fn render_commit_avatar( &self, sha: &SharedString, @@ -245,12 +333,8 @@ impl FileHistoryView { time_format::TimestampFormat::Relative, ); - let sha = entry.sha.clone(); - let repo = self.repository.clone(); - let workspace = self.workspace.clone(); - let file_path = self.history.path.clone(); - ListItem::new(("commit", ix)) + .toggle_state(Some(ix) == self.selected_entry) .child( h_flex() .h_8() @@ -301,18 +385,7 @@ impl FileHistoryView { this.selected_entry = Some(ix); cx.notify(); - if let Some(repo) = repo.upgrade() { - let sha_str = sha.to_string(); - CommitView::open( - sha_str, - repo.downgrade(), - workspace.clone(), - None, - Some(file_path.clone()), - window, - cx, - ); - } + this.open_commit_view(window, cx); })) .into_any_element() } @@ -380,6 +453,14 @@ impl Render for FileHistoryView { let entry_count = self.history.entries.len(); v_flex() + .id("file_history_view") + .key_context("FileHistoryView") + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::select_next)) + .on_action(cx.listener(Self::select_previous)) + .on_action(cx.listener(Self::select_first)) + .on_action(cx.listener(Self::select_last)) + .on_action(cx.listener(Self::confirm)) .size_full() .bg(cx.theme().colors().editor_background) .child( diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 81d2a547bf11d91df98935efa0c167d28644e073..cf588d6b0448c2a7c8e7feb50d34c6e405845116 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -3598,6 +3598,7 @@ impl GitPanel { .icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) .action(text, move |_, cx| cx.open_url(&link)), } + .dismiss_button(true) }); workspace.toggle_status_toast(status_toast, cx) }); @@ -4810,10 +4811,10 @@ impl GitPanel { .id(id) .h(self.list_item_height()) .w_full() - .items_center() .border_1() + .border_r_2() .when(selected && self.focus_handle.is_focused(window), |el| { - el.border_color(cx.theme().colors().border_focused) + el.border_color(cx.theme().colors().panel_focused_border) }) .px(rems(0.75)) // ~12px .overflow_hidden() @@ -4976,8 +4977,9 @@ impl GitPanel { .w_full() .items_center() .border_1() + .border_r_2() .when(selected && self.focus_handle.is_focused(window), |el| { - el.border_color(cx.theme().colors().border_focused) + el.border_color(cx.theme().colors().panel_focused_border) }) .px(rems(0.75)) .overflow_hidden() diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index f40d70da6494cf8491c1d3d7909a288e5f99023c..4d7a27354b1b4b6e972579e73c48bcd4c2448a5c 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -74,6 +74,13 @@ pub struct ProjectDiff { _subscription: Subscription, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RefreshReason { + DiffChanged, + StatusesChanged, + EditorSaved, +} + const CONFLICT_SORT_PREFIX: u64 = 1; const TRACKED_SORT_PREFIX: u64 = 2; const NEW_SORT_PREFIX: u64 = 3; @@ -149,6 +156,10 @@ impl ProjectDiff { .items_of_type::(cx) .find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Head)); let project_diff = if let Some(existing) = existing { + existing.update(cx, |project_diff, cx| { + project_diff.move_to_beginning(window, cx); + }); + workspace.activate_item(&existing, true, true, window, cx); existing } else { @@ -278,7 +289,7 @@ impl ProjectDiff { BranchDiffEvent::FileListChanged => { this._task = window.spawn(cx, { let this = cx.weak_entity(); - async |cx| Self::refresh(this, cx).await + async |cx| Self::refresh(this, RefreshReason::StatusesChanged, cx).await }) } }, @@ -297,7 +308,7 @@ impl ProjectDiff { this._task = { window.spawn(cx, { let this = cx.weak_entity(); - async |cx| Self::refresh(this, cx).await + async |cx| Self::refresh(this, RefreshReason::StatusesChanged, cx).await }) } } @@ -308,7 +319,7 @@ impl ProjectDiff { let task = window.spawn(cx, { let this = cx.weak_entity(); - async |cx| Self::refresh(this, cx).await + async |cx| Self::refresh(this, RefreshReason::StatusesChanged, cx).await }); Self { @@ -358,6 +369,14 @@ impl ProjectDiff { }) } + fn move_to_beginning(&mut self, window: &mut Window, cx: &mut Context) { + self.editor.update(cx, |editor, cx| { + editor.primary_editor().update(cx, |editor, cx| { + editor.move_to_beginning(&Default::default(), window, cx); + }); + }); + } + fn move_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context) { if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) { self.editor.update(cx, |editor, cx| { @@ -448,19 +467,27 @@ impl ProjectDiff { window: &mut Window, cx: &mut Context, ) { - if let EditorEvent::SelectionsChanged { local: true } = event { - let Some(project_path) = self.active_path(cx) else { - return; - }; - self.workspace - .update(cx, |workspace, cx| { - if let Some(git_panel) = workspace.panel::(cx) { - git_panel.update(cx, |git_panel, cx| { - git_panel.select_entry_by_path(project_path, window, cx) - }) - } - }) - .ok(); + match event { + EditorEvent::SelectionsChanged { local: true } => { + let Some(project_path) = self.active_path(cx) else { + return; + }; + self.workspace + .update(cx, |workspace, cx| { + if let Some(git_panel) = workspace.panel::(cx) { + git_panel.update(cx, |git_panel, cx| { + git_panel.select_entry_by_path(project_path, window, cx) + }) + } + }) + .ok(); + } + EditorEvent::Saved => { + self._task = cx.spawn_in(window, async move |this, cx| { + Self::refresh(this, RefreshReason::EditorSaved, cx).await + }); + } + _ => {} } if editor.focus_handle(cx).contains_focused(window, cx) && self.multibuffer.read(cx).is_empty() @@ -482,7 +509,7 @@ impl ProjectDiff { let subscription = cx.subscribe_in(&diff, window, move |this, _, _, window, cx| { this._task = window.spawn(cx, { let this = cx.weak_entity(); - async |cx| Self::refresh(this, cx).await + async |cx| Self::refresh(this, RefreshReason::DiffChanged, cx).await }) }); self.buffer_diff_subscriptions @@ -581,14 +608,23 @@ impl ProjectDiff { } } - pub async fn refresh(this: WeakEntity, cx: &mut AsyncWindowContext) -> Result<()> { + pub async fn refresh( + this: WeakEntity, + reason: RefreshReason, + cx: &mut AsyncWindowContext, + ) -> Result<()> { let mut path_keys = Vec::new(); let buffers_to_load = this.update(cx, |this, cx| { let (repo, buffers_to_load) = this.branch_diff.update(cx, |branch_diff, cx| { let load_buffers = branch_diff.load_buffers(cx); (branch_diff.repo().cloned(), load_buffers) }); - let mut previous_paths = this.multibuffer.read(cx).paths().collect::>(); + let mut previous_paths = this + .multibuffer + .read(cx) + .paths() + .cloned() + .collect::>(); if let Some(repo) = repo { let repo = repo.read(cx); @@ -605,8 +641,20 @@ impl ProjectDiff { this.multibuffer.update(cx, |multibuffer, cx| { for path in previous_paths { + if let Some(buffer) = multibuffer.buffer_for_path(&path, cx) { + let skip = match reason { + RefreshReason::DiffChanged | RefreshReason::EditorSaved => { + buffer.read(cx).is_dirty() + } + RefreshReason::StatusesChanged => false, + }; + if skip { + continue; + } + } + this.buffer_diff_subscriptions.remove(&path.path); - multibuffer.remove_excerpts_for_path(path, cx); + multibuffer.remove_excerpts_for_path(path.clone(), cx); } }); buffers_to_load @@ -619,7 +667,27 @@ impl ProjectDiff { yield_now().await; cx.update(|window, cx| { this.update(cx, |this, cx| { - this.register_buffer(path_key, entry.file_status, buffer, diff, window, cx) + let multibuffer = this.multibuffer.read(cx); + let skip = multibuffer.buffer(buffer.read(cx).remote_id()).is_some() + && multibuffer + .diff_for(buffer.read(cx).remote_id()) + .is_some_and(|prev_diff| prev_diff.entity_id() == diff.entity_id()) + && match reason { + RefreshReason::DiffChanged | RefreshReason::EditorSaved => { + buffer.read(cx).is_dirty() + } + RefreshReason::StatusesChanged => false, + }; + if !skip { + this.register_buffer( + path_key, + entry.file_status, + buffer, + diff, + window, + cx, + ) + } }) .ok(); })?; @@ -637,7 +705,7 @@ impl ProjectDiff { pub fn excerpt_paths(&self, cx: &App) -> Vec> { self.multibuffer .read(cx) - .excerpt_paths() + .paths() .map(|key| key.path.clone()) .collect() } @@ -1650,9 +1718,13 @@ mod tests { .unindent(), ); - editor.update_in(cx, |editor, window, cx| { - editor.git_restore(&Default::default(), window, cx); - }); + editor + .update_in(cx, |editor, window, cx| { + editor.git_restore(&Default::default(), window, cx); + editor.save(SaveOptions::default(), project.clone(), window, cx) + }) + .await + .unwrap(); cx.run_until_parked(); assert_state_with_diff(&editor, cx, &"ˇ".unindent()); @@ -1841,8 +1913,8 @@ mod tests { cx, &" - original - + ˇdifferent - " + + different + ˇ" .unindent(), ); } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 8fc37978683357e53ed9f9c3cf587fcd704431e2..da7e660a0171f38b8dd61de1c9323773ded2589b 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -330,3 +330,7 @@ path = "examples/window_shadow.rs" [[example]] name = "grid_layout" path = "examples/grid_layout.rs" + +[[example]] +name = "mouse_pressure" +path = "examples/mouse_pressure.rs" diff --git a/crates/gpui/examples/mouse_pressure.rs b/crates/gpui/examples/mouse_pressure.rs new file mode 100644 index 0000000000000000000000000000000000000000..12790f988eedac3009ae619cadbc6f40c4af2e4b --- /dev/null +++ b/crates/gpui/examples/mouse_pressure.rs @@ -0,0 +1,66 @@ +use gpui::{ + App, Application, Bounds, Context, MousePressureEvent, PressureStage, Window, WindowBounds, + WindowOptions, div, prelude::*, px, rgb, size, +}; + +struct MousePressureExample { + pressure_stage: PressureStage, + pressure_amount: f32, +} + +impl Render for MousePressureExample { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_3() + .bg(rgb(0x505050)) + .size(px(500.0)) + .justify_center() + .items_center() + .shadow_lg() + .border_1() + .border_color(rgb(0x0000ff)) + .text_xl() + .text_color(rgb(0xffffff)) + .child(format!("Pressure stage: {:?}", &self.pressure_stage)) + .child(format!("Pressure amount: {:.2}", &self.pressure_amount)) + .on_mouse_pressure(cx.listener(Self::on_mouse_pressure)) + } +} + +impl MousePressureExample { + fn on_mouse_pressure( + &mut self, + pressure_event: &MousePressureEvent, + _window: &mut Window, + cx: &mut Context, + ) { + self.pressure_amount = pressure_event.pressure; + self.pressure_stage = pressure_event.stage; + + cx.notify(); + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| { + cx.new(|_| MousePressureExample { + pressure_stage: PressureStage::Zero, + pressure_amount: 0.0, + }) + }, + ) + .unwrap(); + + cx.activate(true); + }); +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f7c57ef015e73618b8cfd9d5da8dbb717905577b..aa1acae33b8fb55fc5e2f8fa8c0f5b8bb91758f3 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1777,7 +1777,10 @@ impl App { /// Register a global handler for actions invoked via the keyboard. These handlers are run at /// the end of the bubble phase for actions, and so will only be invoked if there are no other /// handlers or if they called `cx.propagate()`. - pub fn on_action(&mut self, listener: impl Fn(&A, &mut Self) + 'static) { + pub fn on_action( + &mut self, + listener: impl Fn(&A, &mut Self) + 'static, + ) -> &mut Self { self.global_action_listeners .entry(TypeId::of::()) .or_default() @@ -1787,6 +1790,7 @@ impl App { listener(action, cx) } })); + self } /// Event handlers propagate events by default. Call this method to stop dispatching to diff --git a/crates/gpui/src/bounds_tree.rs b/crates/gpui/src/bounds_tree.rs index d621609bf7334801059513e03dfd11b4036ea816..9cf86a2cc9b6def8fbf5ca7e94f7cd19236468cc 100644 --- a/crates/gpui/src/bounds_tree.rs +++ b/crates/gpui/src/bounds_tree.rs @@ -5,14 +5,91 @@ use std::{ ops::{Add, Sub}, }; +/// Maximum children per internal node (R-tree style branching factor). +/// Higher values = shorter tree = fewer cache misses, but more work per node. +const MAX_CHILDREN: usize = 12; + +/// A spatial tree optimized for finding maximum ordering among intersecting bounds. +/// +/// This is an R-tree variant specifically designed for the use case of assigning +/// z-order to overlapping UI elements. Key optimizations: +/// - Tracks the leaf with global max ordering for O(1) fast-path queries +/// - Uses higher branching factor (4) for lower tree height +/// - Aggressive pruning during search based on max_order metadata #[derive(Debug)] pub(crate) struct BoundsTree where U: Clone + Debug + Default + PartialEq, { - root: Option, + /// All nodes stored contiguously for cache efficiency. nodes: Vec>, - stack: Vec, + /// Index of the root node, if any. + root: Option, + /// Index of the leaf with the highest ordering (for fast-path lookups). + max_leaf: Option, + /// Reusable stack for tree traversal during insertion. + insert_path: Vec, + /// Reusable stack for search operations. + search_stack: Vec, +} + +/// A node in the bounds tree. +#[derive(Debug, Clone)] +struct Node +where + U: Clone + Debug + Default + PartialEq, +{ + /// Bounding box containing this node and all descendants. + bounds: Bounds, + /// Maximum ordering value in this subtree. + max_order: u32, + /// Node-specific data. + kind: NodeKind, +} + +#[derive(Debug, Clone)] +enum NodeKind { + /// Leaf node containing actual bounds data. + Leaf { + /// The ordering assigned to this bounds. + order: u32, + }, + /// Internal node with children. + Internal { + /// Indices of child nodes (2 to MAX_CHILDREN). + children: NodeChildren, + }, +} + +/// Fixed-size array for child indices, avoiding heap allocation. +#[derive(Debug, Clone)] +struct NodeChildren { + // Keeps an invariant where the max order child is always at the end + indices: [usize; MAX_CHILDREN], + len: u8, +} + +impl NodeChildren { + fn new() -> Self { + Self { + indices: [0; MAX_CHILDREN], + len: 0, + } + } + + fn push(&mut self, index: usize) { + debug_assert!((self.len as usize) < MAX_CHILDREN); + self.indices[self.len as usize] = index; + self.len += 1; + } + + fn len(&self) -> usize { + self.len as usize + } + + fn as_slice(&self) -> &[usize] { + &self.indices[..self.len as usize] + } } impl BoundsTree @@ -26,158 +103,250 @@ where + Half + Default, { + /// Clears all nodes from the tree. pub fn clear(&mut self) { - self.root = None; self.nodes.clear(); - self.stack.clear(); + self.root = None; + self.max_leaf = None; + self.insert_path.clear(); + self.search_stack.clear(); } + /// Inserts bounds into the tree and returns its assigned ordering. + /// + /// The ordering is one greater than the maximum ordering of any + /// existing bounds that intersect with the new bounds. pub fn insert(&mut self, new_bounds: Bounds) -> u32 { - // If the tree is empty, make the root the new leaf. - let Some(mut index) = self.root else { - let new_node = self.push_leaf(new_bounds, 1); - self.root = Some(new_node); - return 1; + // Find maximum ordering among intersecting bounds + let max_intersecting = self.find_max_ordering(&new_bounds); + let ordering = max_intersecting + 1; + + // Insert the new leaf + let new_leaf_idx = self.insert_leaf(new_bounds, ordering); + + // Update max_leaf tracking + self.max_leaf = match self.max_leaf { + None => Some(new_leaf_idx), + Some(old_idx) if self.nodes[old_idx].max_order < ordering => Some(new_leaf_idx), + some => some, }; - // Search for the best place to add the new leaf based on heuristics. - let mut max_intersecting_ordering = 0; - while let Node::Internal { - left, - right, - bounds: node_bounds, - .. - } = &mut self.nodes[index] - { - let left = *left; - let right = *right; - *node_bounds = node_bounds.union(&new_bounds); - self.stack.push(index); - - // Descend to the best-fit child, based on which one would increase - // the surface area the least. This attempts to keep the tree balanced - // in terms of surface area. If there is an intersection with the other child, - // add its keys to the intersections vector. - let left_cost = new_bounds.union(self.nodes[left].bounds()).half_perimeter(); - let right_cost = new_bounds - .union(self.nodes[right].bounds()) - .half_perimeter(); - if left_cost < right_cost { - max_intersecting_ordering = - self.find_max_ordering(right, &new_bounds, max_intersecting_ordering); - index = left; - } else { - max_intersecting_ordering = - self.find_max_ordering(left, &new_bounds, max_intersecting_ordering); - index = right; + ordering + } + + /// Finds the maximum ordering among all bounds that intersect with the query. + fn find_max_ordering(&mut self, query: &Bounds) -> u32 { + let Some(root_idx) = self.root else { + return 0; + }; + + // Fast path: check if the max-ordering leaf intersects + if let Some(max_idx) = self.max_leaf { + let max_node = &self.nodes[max_idx]; + if query.intersects(&max_node.bounds) { + return max_node.max_order; } } - // We've found a leaf ('index' now refers to a leaf node). - // We'll insert a new parent node above the leaf and attach our new leaf to it. - let sibling = index; - - // Check for collision with the located leaf node - let Node::Leaf { - bounds: sibling_bounds, - order: sibling_ordering, - .. - } = &self.nodes[index] - else { - unreachable!(); - }; - if sibling_bounds.intersects(&new_bounds) { - max_intersecting_ordering = cmp::max(max_intersecting_ordering, *sibling_ordering); + // Slow path: search the tree + self.search_stack.clear(); + self.search_stack.push(root_idx); + + let mut max_found = 0u32; + + while let Some(node_idx) = self.search_stack.pop() { + let node = &self.nodes[node_idx]; + + // Pruning: skip if this subtree can't improve our result + if node.max_order <= max_found { + continue; + } + + // Spatial pruning: skip if bounds don't intersect + if !query.intersects(&node.bounds) { + continue; + } + + match &node.kind { + NodeKind::Leaf { order } => { + max_found = cmp::max(max_found, *order); + } + NodeKind::Internal { children } => { + // Children are maintained with highest max_order at the end. + // Push in forward order to highest (last) is popped first. + for &child_idx in children.as_slice() { + if self.nodes[child_idx].max_order > max_found { + self.search_stack.push(child_idx); + } + } + } + } } - let ordering = max_intersecting_ordering + 1; - let new_node = self.push_leaf(new_bounds, ordering); - let new_parent = self.push_internal(sibling, new_node); + max_found + } - // If there was an old parent, we need to update its children indices. - if let Some(old_parent) = self.stack.last().copied() { - let Node::Internal { left, right, .. } = &mut self.nodes[old_parent] else { - unreachable!(); - }; + /// Inserts a leaf node with the given bounds and ordering. + /// Returns the index of the new leaf. + fn insert_leaf(&mut self, bounds: Bounds, order: u32) -> usize { + let new_leaf_idx = self.nodes.len(); + self.nodes.push(Node { + bounds: bounds.clone(), + max_order: order, + kind: NodeKind::Leaf { order }, + }); - if *left == sibling { - *left = new_parent; + let Some(root_idx) = self.root else { + // Tree is empty, new leaf becomes root + self.root = Some(new_leaf_idx); + return new_leaf_idx; + }; + + // If root is a leaf, create internal node with both + if matches!(self.nodes[root_idx].kind, NodeKind::Leaf { .. }) { + let root_bounds = self.nodes[root_idx].bounds.clone(); + let root_order = self.nodes[root_idx].max_order; + + let mut children = NodeChildren::new(); + // Max end invariant + if order > root_order { + children.push(root_idx); + children.push(new_leaf_idx); } else { - *right = new_parent; + children.push(new_leaf_idx); + children.push(root_idx); } - } else { - // If the old parent was the root, the new parent is the new root. - self.root = Some(new_parent); + + let new_root_idx = self.nodes.len(); + self.nodes.push(Node { + bounds: root_bounds.union(&bounds), + max_order: cmp::max(root_order, order), + kind: NodeKind::Internal { children }, + }); + self.root = Some(new_root_idx); + return new_leaf_idx; } - for node_index in self.stack.drain(..).rev() { - let Node::Internal { - max_order: max_ordering, - .. - } = &mut self.nodes[node_index] - else { - unreachable!() + // Descend to find the best internal node to insert into + self.insert_path.clear(); + let mut current_idx = root_idx; + + loop { + let current = &self.nodes[current_idx]; + let NodeKind::Internal { children } = ¤t.kind else { + unreachable!("Should only traverse internal nodes"); }; - if *max_ordering >= ordering { - break; - } - *max_ordering = ordering; - } - ordering - } + self.insert_path.push(current_idx); + + // Find the best child to descend into + let mut best_child_idx = children.as_slice()[0]; + let mut best_child_pos = 0; + let mut best_cost = bounds + .union(&self.nodes[best_child_idx].bounds) + .half_perimeter(); - fn find_max_ordering(&self, index: usize, bounds: &Bounds, mut max_ordering: u32) -> u32 { - match &self.nodes[index] { - Node::Leaf { - bounds: node_bounds, - order: ordering, - .. - } => { - if bounds.intersects(node_bounds) { - max_ordering = cmp::max(*ordering, max_ordering); + for (pos, &child_idx) in children.as_slice().iter().enumerate().skip(1) { + let cost = bounds.union(&self.nodes[child_idx].bounds).half_perimeter(); + if cost < best_cost { + best_cost = cost; + best_child_idx = child_idx; + best_child_pos = pos; } } - Node::Internal { - left, - right, - bounds: node_bounds, - max_order: node_max_ordering, - .. - } => { - if bounds.intersects(node_bounds) && max_ordering < *node_max_ordering { - let left_max_ordering = self.nodes[*left].max_ordering(); - let right_max_ordering = self.nodes[*right].max_ordering(); - if left_max_ordering > right_max_ordering { - max_ordering = self.find_max_ordering(*left, bounds, max_ordering); - max_ordering = self.find_max_ordering(*right, bounds, max_ordering); + + // Check if best child is a leaf or internal + if matches!(self.nodes[best_child_idx].kind, NodeKind::Leaf { .. }) { + // Best child is a leaf. Check if current node has room for another child. + if children.len() < MAX_CHILDREN { + // Add new leaf directly to this node + let node = &mut self.nodes[current_idx]; + + if let NodeKind::Internal { children } = &mut node.kind { + children.push(new_leaf_idx); + // Swap new leaf only if it has the highest max_order + if order <= node.max_order { + let last = children.len() - 1; + children.indices.swap(last - 1, last); + } + } + + node.bounds = node.bounds.union(&bounds); + node.max_order = cmp::max(node.max_order, order); + break; + } else { + // Node is full, create new internal with [best_leaf, new_leaf] + let sibling_bounds = self.nodes[best_child_idx].bounds.clone(); + let sibling_order = self.nodes[best_child_idx].max_order; + + let mut new_children = NodeChildren::new(); + // Max end invariant + if order > sibling_order { + new_children.push(best_child_idx); + new_children.push(new_leaf_idx); } else { - max_ordering = self.find_max_ordering(*right, bounds, max_ordering); - max_ordering = self.find_max_ordering(*left, bounds, max_ordering); + new_children.push(new_leaf_idx); + new_children.push(best_child_idx); + } + + let new_internal_idx = self.nodes.len(); + let new_internal_max = cmp::max(sibling_order, order); + self.nodes.push(Node { + bounds: sibling_bounds.union(&bounds), + max_order: new_internal_max, + kind: NodeKind::Internal { + children: new_children, + }, + }); + + // Replace the leaf with the new internal in parent + let parent = &mut self.nodes[current_idx]; + if let NodeKind::Internal { children } = &mut parent.kind { + let children_len = children.len(); + + children.indices[best_child_pos] = new_internal_idx; + + // If new internal has highest max_order, swap it to the end + // to maintain sorting invariant + if new_internal_max > parent.max_order { + children.indices.swap(best_child_pos, children_len - 1); + } } + break; } + } else { + // Best child is internal, continue descent + current_idx = best_child_idx; } } - max_ordering - } - fn push_leaf(&mut self, bounds: Bounds, order: u32) -> usize { - self.nodes.push(Node::Leaf { bounds, order }); - self.nodes.len() - 1 - } + // Propagate bounds and max_order updates up the tree + let mut updated_child_idx = None; + for &node_idx in self.insert_path.iter().rev() { + let node = &mut self.nodes[node_idx]; + node.bounds = node.bounds.union(&bounds); - fn push_internal(&mut self, left: usize, right: usize) -> usize { - let left_node = &self.nodes[left]; - let right_node = &self.nodes[right]; - let new_bounds = left_node.bounds().union(right_node.bounds()); - let max_ordering = cmp::max(left_node.max_ordering(), right_node.max_ordering()); - self.nodes.push(Node::Internal { - bounds: new_bounds, - left, - right, - max_order: max_ordering, - }); - self.nodes.len() - 1 + if node.max_order < order { + node.max_order = order; + + // Swap updated child to end (skip first iteration since the invariant is already handled by previous cases) + if let Some(child_idx) = updated_child_idx { + if let NodeKind::Internal { children } = &mut node.kind { + if let Some(pos) = children.as_slice().iter().position(|&c| c == child_idx) + { + let last = children.len() - 1; + if pos != last { + children.indices.swap(pos, last); + } + } + } + } + } + + updated_child_idx = Some(node_idx); + } + + new_leaf_idx } } @@ -187,50 +356,11 @@ where { fn default() -> Self { BoundsTree { - root: None, nodes: Vec::new(), - stack: Vec::new(), - } - } -} - -#[derive(Debug, Clone)] -enum Node -where - U: Clone + Debug + Default + PartialEq, -{ - Leaf { - bounds: Bounds, - order: u32, - }, - Internal { - left: usize, - right: usize, - bounds: Bounds, - max_order: u32, - }, -} - -impl Node -where - U: Clone + Debug + Default + PartialEq, -{ - fn bounds(&self) -> &Bounds { - match self { - Node::Leaf { bounds, .. } => bounds, - Node::Internal { bounds, .. } => bounds, - } - } - - fn max_ordering(&self) -> u32 { - match self { - Node::Leaf { - order: ordering, .. - } => *ordering, - Node::Internal { - max_order: max_ordering, - .. - } => *max_ordering, + root: None, + max_leaf: None, + insert_path: Vec::new(), + search_stack: Vec::new(), } } } diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 821f155f96d168e5319d9a8981ca4be75df7b854..374fd2c55a8e1cd5280d6ea9378a64c265a5c508 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -20,8 +20,8 @@ use crate::{ DispatchPhase, Display, 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, + MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, + Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px, size, }; @@ -166,6 +166,38 @@ impl Interactivity { })); } + /// Bind the given callback to the mouse pressure event, during the bubble phase + /// the imperative API equivalent to [`InteractiveElement::on_mouse_pressure`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_mouse_pressure( + &mut self, + listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_pressure_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse pressure event, during the capture phase + /// the imperative API equivalent to [`InteractiveElement::on_mouse_pressure`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_mouse_pressure( + &mut self, + listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_pressure_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + /// Bind the given callback to the mouse up event for the given button, during the bubble phase. /// The imperative API equivalent to [`InteractiveElement::on_mouse_up`]. /// @@ -769,6 +801,30 @@ pub trait InteractiveElement: Sized { self } + /// Bind the given callback to the mouse pressure event, during the bubble phase + /// the fluent API equivalent to [`Interactivity::on_mouse_pressure`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_mouse_pressure( + mut self, + listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_pressure(listener); + self + } + + /// Bind the given callback to the mouse pressure event, during the capture phase + /// the fluent API equivalent to [`Interactivity::on_mouse_pressure`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_mouse_pressure( + mut self, + listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_mouse_pressure(listener); + self + } + /// Bind the given callback to the mouse down event, on any button, during the capture phase, /// when the mouse is outside of the bounds of this element. /// The fluent API equivalent to [`Interactivity::on_mouse_down_out`]. @@ -1197,7 +1253,8 @@ pub(crate) type MouseDownListener = Box; pub(crate) type MouseUpListener = Box; - +pub(crate) type MousePressureListener = + Box; pub(crate) type MouseMoveListener = Box; @@ -1521,6 +1578,7 @@ pub struct Interactivity { pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>, pub(crate) mouse_down_listeners: Vec, pub(crate) mouse_up_listeners: Vec, + pub(crate) mouse_pressure_listeners: Vec, pub(crate) mouse_move_listeners: Vec, pub(crate) scroll_wheel_listeners: Vec, pub(crate) key_down_listeners: Vec, @@ -1714,6 +1772,7 @@ impl Interactivity { || self.group_hover_style.is_some() || self.hover_listener.is_some() || !self.mouse_up_listeners.is_empty() + || !self.mouse_pressure_listeners.is_empty() || !self.mouse_down_listeners.is_empty() || !self.mouse_move_listeners.is_empty() || !self.click_listeners.is_empty() @@ -2064,6 +2123,13 @@ impl Interactivity { }) } + for listener in self.mouse_pressure_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MousePressureEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + for listener in self.mouse_move_listeners.drain(..) { let hitbox = hitbox.clone(); window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| { @@ -3193,7 +3259,11 @@ impl ScrollHandle { match active_item.strategy { ScrollStrategy::FirstVisible => { if state.overflow.y == Overflow::Scroll { - if bounds.top() + scroll_offset.y < state.bounds.top() { + let child_height = bounds.size.height; + let viewport_height = state.bounds.size.height; + if child_height > viewport_height { + scroll_offset.y = state.bounds.top() - bounds.top(); + } else if bounds.top() + scroll_offset.y < state.bounds.top() { scroll_offset.y = state.bounds.top() - bounds.top(); } else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() { scroll_offset.y = state.bounds.bottom() - bounds.bottom(); @@ -3206,7 +3276,11 @@ impl ScrollHandle { } if state.overflow.x == Overflow::Scroll { - if bounds.left() + scroll_offset.x < state.bounds.left() { + let child_width = bounds.size.width; + let viewport_width = state.bounds.size.width; + if child_width > viewport_width { + scroll_offset.x = state.bounds.left() - bounds.left(); + } else if bounds.left() + scroll_offset.x < state.bounds.left() { scroll_offset.x = state.bounds.left() - bounds.left(); } else if bounds.right() + scroll_offset.x > state.bounds.right() { scroll_offset.x = state.bounds.right() - bounds.right(); @@ -3268,3 +3342,46 @@ impl ScrollHandle { self.0.borrow().child_bounds.len() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn scroll_handle_aligns_wide_children_to_left_edge() { + let handle = ScrollHandle::new(); + { + let mut state = handle.0.borrow_mut(); + state.bounds = Bounds::new(point(px(0.), px(0.)), size(px(80.), px(20.))); + state.child_bounds = vec![Bounds::new(point(px(25.), px(0.)), size(px(200.), px(20.)))]; + state.overflow.x = Overflow::Scroll; + state.active_item = Some(ScrollActiveItem { + index: 0, + strategy: ScrollStrategy::default(), + }); + } + + handle.scroll_to_active_item(); + + assert_eq!(handle.offset().x, px(-25.)); + } + + #[test] + fn scroll_handle_aligns_tall_children_to_top_edge() { + let handle = ScrollHandle::new(); + { + let mut state = handle.0.borrow_mut(); + state.bounds = Bounds::new(point(px(0.), px(0.)), size(px(20.), px(80.))); + state.child_bounds = vec![Bounds::new(point(px(0.), px(25.)), size(px(20.), px(200.)))]; + state.overflow.y = Overflow::Scroll; + state.active_item = Some(ScrollActiveItem { + index: 0, + strategy: ScrollStrategy::default(), + }); + } + + handle.scroll_to_active_item(); + + assert_eq!(handle.offset().y, px(-25.)); + } +} diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index f466624dfb91af9b4a33421ea15827ebe2559665..fc735ba5e0e7e719ed12b6b1b168ec3ee49e22bb 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -2648,6 +2648,18 @@ impl Debug for Pixels { } } +impl std::iter::Sum for Pixels { + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |a, b| a + b) + } +} + +impl<'a> std::iter::Sum<&'a Pixels> for Pixels { + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |a, b| a + *b) + } +} + impl TryFrom<&'_ str> for Pixels { type Error = anyhow::Error; diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 03acf81addaad1ae9800ef476a2dc7d13e690cf7..6852b9596a3f74e1d533fc2a7e9a7b7eeab71cda 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -174,6 +174,40 @@ pub struct MouseClickEvent { pub up: MouseUpEvent, } +/// The stage of a pressure click event. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum PressureStage { + /// No pressure. + #[default] + Zero, + /// Normal click pressure. + Normal, + /// High pressure, enough to trigger a force click. + Force, +} + +/// A mouse pressure event from the platform. Generated when a force-sensitive trackpad is pressed hard. +/// Currently only implemented for macOS trackpads. +#[derive(Debug, Clone, Default)] +pub struct MousePressureEvent { + /// Pressure of the current stage as a float between 0 and 1 + pub pressure: f32, + /// The pressure stage of the event. + pub stage: PressureStage, + /// The position of the mouse on the window. + pub position: Point, + /// The modifiers that were held down when the mouse pressure changed. + pub modifiers: Modifiers, +} + +impl Sealed for MousePressureEvent {} +impl InputEvent for MousePressureEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MousePressure(self) + } +} +impl MouseEvent for MousePressureEvent {} + /// A click event that was generated by a keyboard button being pressed and released. #[derive(Clone, Debug, Default)] pub struct KeyboardClickEvent { @@ -571,6 +605,8 @@ pub enum PlatformInput { MouseDown(MouseDownEvent), /// The mouse was released. MouseUp(MouseUpEvent), + /// Mouse pressure. + MousePressure(MousePressureEvent), /// The mouse was moved. MouseMove(MouseMoveEvent), /// The mouse exited the window. @@ -590,6 +626,7 @@ impl PlatformInput { PlatformInput::MouseDown(event) => Some(event), PlatformInput::MouseUp(event) => Some(event), PlatformInput::MouseMove(event) => Some(event), + PlatformInput::MousePressure(event) => Some(event), PlatformInput::MouseExited(event) => Some(event), PlatformInput::ScrollWheel(event) => Some(event), PlatformInput::FileDrop(event) => Some(event), @@ -604,6 +641,7 @@ impl PlatformInput { PlatformInput::MouseDown(_) => None, PlatformInput::MouseUp(_) => None, PlatformInput::MouseMove(_) => None, + PlatformInput::MousePressure(_) => None, PlatformInput::MouseExited(_) => None, PlatformInput::ScrollWheel(_) => None, PlatformInput::FileDrop(_) => None, diff --git a/crates/gpui/src/platform/linux/dispatcher.rs b/crates/gpui/src/platform/linux/dispatcher.rs index d88eefd2c8a7fc648b20f7a2e520fe40158acd51..c8ae7269edd495669baa6ab0e22e745917f143b2 100644 --- a/crates/gpui/src/platform/linux/dispatcher.rs +++ b/crates/gpui/src/platform/linux/dispatcher.rs @@ -1,18 +1,21 @@ -use crate::{ - GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueReceiver, - PriorityQueueSender, RealtimePriority, RunnableVariant, THREAD_TIMINGS, TaskLabel, TaskTiming, - ThreadTaskTimings, profiler, -}; use calloop::{ EventLoop, PostAction, channel::{self, Sender}, timer::TimeoutAction, }; +use util::ResultExt; + use std::{ + mem::MaybeUninit, thread, time::{Duration, Instant}, }; -use util::ResultExt; + +use crate::{ + GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueReceiver, + PriorityQueueSender, RealtimePriority, RunnableVariant, THREAD_TIMINGS, TaskLabel, TaskTiming, + ThreadTaskTimings, profiler, +}; struct TimerAfter { duration: Duration, @@ -228,7 +231,10 @@ impl PlatformDispatcher for LinuxDispatcher { RealtimePriority::Other => 45, }; - let sched_param = libc::sched_param { sched_priority }; + // SAFETY: all sched_param members are valid when initialized to zero. + let mut sched_param = + unsafe { MaybeUninit::::zeroed().assume_init() }; + sched_param.sched_priority = sched_priority; // SAFETY: sched_param is a valid initialized structure let result = unsafe { libc::pthread_setschedparam(thread_id, policy, &sched_param) }; if result != 0 { diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index acc392a5f3429f20931455ea06733376ea0f587a..7a12e8d3d7ccb2e8a2f7b32b81c24a29f650e6e2 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -1,7 +1,8 @@ use crate::{ Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, - MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, - PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase, + MouseDownEvent, MouseExitEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, + NavigationDirection, Pixels, PlatformInput, PressureStage, ScrollDelta, ScrollWheelEvent, + TouchPhase, platform::mac::{ LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData, @@ -187,6 +188,26 @@ impl PlatformInput { }) }) } + NSEventType::NSEventTypePressure => { + let stage = native_event.stage(); + let pressure = native_event.pressure(); + + window_height.map(|window_height| { + Self::MousePressure(MousePressureEvent { + stage: match stage { + 1 => PressureStage::Normal, + 2 => PressureStage::Force, + _ => PressureStage::Zero, + }, + pressure, + modifiers: read_modifiers(native_event), + position: point( + px(native_event.locationInWindow().x as f32), + window_height - px(native_event.locationInWindow().y as f32), + ), + }) + }) + } // Some mice (like Logitech MX Master) send navigation buttons as swipe events NSEventType::NSEventTypeSwipe => { let navigation_direction = match native_event.phase() { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 23752fc53edbc1062db19caf13c5c65fc282ca87..53207fb77d16f2e1956f6914889b29ae3ea7bb35 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -153,6 +153,10 @@ unsafe fn build_classes() { sel!(mouseMoved:), handle_view_event as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(pressureChangeWithEvent:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); decl.add_method( sel!(mouseExited:), handle_view_event as extern "C" fn(&Object, Sel, id), diff --git a/crates/gpui/src/queue.rs b/crates/gpui/src/queue.rs index 3a4ef912ffd5fb85b80384454f7afd84cecb1648..9e9da710977ee80df1853791918eebe5e7f01096 100644 --- a/crates/gpui/src/queue.rs +++ b/crates/gpui/src/queue.rs @@ -58,8 +58,7 @@ impl PriorityQueueState { return Err(crate::queue::RecvError); } - // parking_lot doesn't do spurious wakeups so an if is fine - if queues.is_empty() { + while queues.is_empty() { self.condvar.wait(&mut queues); } @@ -265,7 +264,7 @@ impl Iterator for Iter { type Item = T; fn next(&mut self) -> Option { - self.0.pop_inner(true).ok().flatten() + self.0.pop().ok() } } impl FusedIterator for Iter {} @@ -283,7 +282,7 @@ impl Iterator for TryIter { return None; } - let res = self.receiver.pop_inner(false); + let res = self.receiver.try_pop(); self.ended = res.is_err(); res.transpose() diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 42f8f25e47620fe673720055037b7f91f44165a2..446c3ad2a325681a39689577a261ed1ffdde6d5b 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -252,6 +252,7 @@ pub struct Style { pub box_shadow: Vec, /// The text style of this element + #[refineable] pub text: TextStyleRefinement, /// The mouse cursor style shown when the mouse pointer is over an element. @@ -1469,4 +1470,21 @@ mod tests { ] ); } + + #[perf] + fn test_text_style_refinement() { + let mut style = Style::default(); + style.refine(&StyleRefinement::default().text_size(px(20.0))); + style.refine(&StyleRefinement::default().font_weight(FontWeight::SEMIBOLD)); + + assert_eq!( + Some(AbsoluteLength::from(px(20.0))), + style.text_style().unwrap().font_size + ); + + assert_eq!( + Some(FontWeight::SEMIBOLD), + style.text_style().unwrap().font_weight + ); + } } diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index 752038c1ed63a1d0d5960bf0a74a1c2fdbc43392..e01649be481e27f89643db2ffb3a9ccd294b9b73 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -64,43 +64,33 @@ pub trait Styled: Sized { /// Sets the whitespace of the element to `normal`. /// [Docs](https://tailwindcss.com/docs/whitespace#normal) fn whitespace_normal(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .white_space = Some(WhiteSpace::Normal); + self.text_style().white_space = Some(WhiteSpace::Normal); self } /// Sets the whitespace of the element to `nowrap`. /// [Docs](https://tailwindcss.com/docs/whitespace#nowrap) fn whitespace_nowrap(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .white_space = Some(WhiteSpace::Nowrap); + self.text_style().white_space = Some(WhiteSpace::Nowrap); self } /// Sets the truncate overflowing text with an ellipsis (…) if needed. /// [Docs](https://tailwindcss.com/docs/text-overflow#ellipsis) fn text_ellipsis(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .text_overflow = Some(TextOverflow::Truncate(ELLIPSIS)); + self.text_style().text_overflow = Some(TextOverflow::Truncate(ELLIPSIS)); self } /// Sets the text overflow behavior of the element. fn text_overflow(mut self, overflow: TextOverflow) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .text_overflow = Some(overflow); + self.text_style().text_overflow = Some(overflow); self } /// Set the text alignment of the element. fn text_align(mut self, align: TextAlign) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .text_align = Some(align); + self.text_style().text_align = Some(align); self } @@ -128,7 +118,7 @@ pub trait Styled: Sized { /// Sets number of lines to show before truncating the text. /// [Docs](https://tailwindcss.com/docs/line-clamp) fn line_clamp(mut self, lines: usize) -> Self { - let mut text_style = self.text_style().get_or_insert_with(Default::default); + let mut text_style = self.text_style(); text_style.line_clamp = Some(lines); self.overflow_hidden() } @@ -396,7 +386,7 @@ pub trait Styled: Sized { } /// Returns a mutable reference to the text style that has been configured on this element. - fn text_style(&mut self) -> &mut Option { + fn text_style(&mut self) -> &mut TextStyleRefinement { let style: &mut StyleRefinement = self.style(); &mut style.text } @@ -405,7 +395,7 @@ pub trait Styled: Sized { /// /// This value cascades to its child elements. fn text_color(mut self, color: impl Into) -> Self { - self.text_style().get_or_insert_with(Default::default).color = Some(color.into()); + self.text_style().color = Some(color.into()); self } @@ -413,9 +403,7 @@ pub trait Styled: Sized { /// /// This value cascades to its child elements. fn font_weight(mut self, weight: FontWeight) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_weight = Some(weight); + self.text_style().font_weight = Some(weight); self } @@ -423,9 +411,7 @@ pub trait Styled: Sized { /// /// This value cascades to its child elements. fn text_bg(mut self, bg: impl Into) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .background_color = Some(bg.into()); + self.text_style().background_color = Some(bg.into()); self } @@ -433,97 +419,77 @@ pub trait Styled: Sized { /// /// This value cascades to its child elements. fn text_size(mut self, size: impl Into) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_size = Some(size.into()); + self.text_style().font_size = Some(size.into()); self } /// Sets the text size to 'extra small'. /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) fn text_xs(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_size = Some(rems(0.75).into()); + self.text_style().font_size = Some(rems(0.75).into()); self } /// Sets the text size to 'small'. /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) fn text_sm(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_size = Some(rems(0.875).into()); + self.text_style().font_size = Some(rems(0.875).into()); self } /// Sets the text size to 'base'. /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) fn text_base(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_size = Some(rems(1.0).into()); + self.text_style().font_size = Some(rems(1.0).into()); self } /// Sets the text size to 'large'. /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) fn text_lg(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_size = Some(rems(1.125).into()); + self.text_style().font_size = Some(rems(1.125).into()); self } /// Sets the text size to 'extra large'. /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) fn text_xl(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_size = Some(rems(1.25).into()); + self.text_style().font_size = Some(rems(1.25).into()); self } /// Sets the text size to 'extra extra large'. /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) fn text_2xl(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_size = Some(rems(1.5).into()); + self.text_style().font_size = Some(rems(1.5).into()); self } /// Sets the text size to 'extra extra extra large'. /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) fn text_3xl(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_size = Some(rems(1.875).into()); + self.text_style().font_size = Some(rems(1.875).into()); self } /// Sets the font style of the element to italic. /// [Docs](https://tailwindcss.com/docs/font-style#italicizing-text) fn italic(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_style = Some(FontStyle::Italic); + self.text_style().font_style = Some(FontStyle::Italic); self } /// Sets the font style of the element to normal (not italic). /// [Docs](https://tailwindcss.com/docs/font-style#displaying-text-normally) fn not_italic(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_style = Some(FontStyle::Normal); + self.text_style().font_style = Some(FontStyle::Normal); self } /// Sets the text decoration to underline. /// [Docs](https://tailwindcss.com/docs/text-decoration-line#underling-text) fn underline(mut self) -> Self { - let style = self.text_style().get_or_insert_with(Default::default); + let style = self.text_style(); style.underline = Some(UnderlineStyle { thickness: px(1.), ..Default::default() @@ -534,7 +500,7 @@ pub trait Styled: Sized { /// Sets the decoration of the text to have a line through it. /// [Docs](https://tailwindcss.com/docs/text-decoration-line#adding-a-line-through-text) fn line_through(mut self) -> Self { - let style = self.text_style().get_or_insert_with(Default::default); + let style = self.text_style(); style.strikethrough = Some(StrikethroughStyle { thickness: px(1.), ..Default::default() @@ -546,15 +512,13 @@ pub trait Styled: Sized { /// /// This value cascades to its child elements. fn text_decoration_none(mut self) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .underline = None; + self.text_style().underline = None; self } /// Sets the color for the underline on this element fn text_decoration_color(mut self, color: impl Into) -> Self { - let style = self.text_style().get_or_insert_with(Default::default); + let style = self.text_style(); let underline = style.underline.get_or_insert_with(Default::default); underline.color = Some(color.into()); self @@ -563,7 +527,7 @@ pub trait Styled: Sized { /// Sets the text decoration style to a solid line. /// [Docs](https://tailwindcss.com/docs/text-decoration-style) fn text_decoration_solid(mut self) -> Self { - let style = self.text_style().get_or_insert_with(Default::default); + let style = self.text_style(); let underline = style.underline.get_or_insert_with(Default::default); underline.wavy = false; self @@ -572,7 +536,7 @@ pub trait Styled: Sized { /// Sets the text decoration style to a wavy line. /// [Docs](https://tailwindcss.com/docs/text-decoration-style) fn text_decoration_wavy(mut self) -> Self { - let style = self.text_style().get_or_insert_with(Default::default); + let style = self.text_style(); let underline = style.underline.get_or_insert_with(Default::default); underline.wavy = true; self @@ -581,7 +545,7 @@ pub trait Styled: Sized { /// Sets the text decoration to be 0px thick. /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) fn text_decoration_0(mut self) -> Self { - let style = self.text_style().get_or_insert_with(Default::default); + let style = self.text_style(); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(0.); self @@ -590,7 +554,7 @@ pub trait Styled: Sized { /// Sets the text decoration to be 1px thick. /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) fn text_decoration_1(mut self) -> Self { - let style = self.text_style().get_or_insert_with(Default::default); + let style = self.text_style(); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(1.); self @@ -599,7 +563,7 @@ pub trait Styled: Sized { /// Sets the text decoration to be 2px thick. /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) fn text_decoration_2(mut self) -> Self { - let style = self.text_style().get_or_insert_with(Default::default); + let style = self.text_style(); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(2.); self @@ -608,7 +572,7 @@ pub trait Styled: Sized { /// Sets the text decoration to be 4px thick. /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) fn text_decoration_4(mut self) -> Self { - let style = self.text_style().get_or_insert_with(Default::default); + let style = self.text_style(); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(4.); self @@ -617,7 +581,7 @@ pub trait Styled: Sized { /// Sets the text decoration to be 8px thick. /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) fn text_decoration_8(mut self) -> Self { - let style = self.text_style().get_or_insert_with(Default::default); + let style = self.text_style(); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(8.); self @@ -625,17 +589,13 @@ pub trait Styled: Sized { /// Sets the font family of this element and its children. fn font_family(mut self, family_name: impl Into) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_family = Some(family_name.into()); + self.text_style().font_family = Some(family_name.into()); self } /// Sets the font features of this element and its children. fn font_features(mut self, features: FontFeatures) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .font_features = Some(features); + self.text_style().font_features = Some(features); self } @@ -649,7 +609,7 @@ pub trait Styled: Sized { style, } = font; - let text_style = self.text_style().get_or_insert_with(Default::default); + let text_style = self.text_style(); text_style.font_family = Some(family); text_style.font_features = Some(features); text_style.font_weight = Some(weight); @@ -661,9 +621,7 @@ pub trait Styled: Sized { /// Sets the line height of this element and its children. fn line_height(mut self, line_height: impl Into) -> Self { - self.text_style() - .get_or_insert_with(Default::default) - .line_height = Some(line_height.into()); + self.text_style().line_height = Some(line_height.into()); self } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 54fe99c2634f5afa2e1f1e224e969c21d4c38e34..36e46f6961ae8a1e8581b3c01987f4641377d677 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3705,6 +3705,9 @@ impl Window { self.modifiers = mouse_up.modifiers; PlatformInput::MouseUp(mouse_up) } + PlatformInput::MousePressure(mouse_pressure) => { + PlatformInput::MousePressure(mouse_pressure) + } PlatformInput::MouseExited(mouse_exited) => { self.modifiers = mouse_exited.modifiers; PlatformInput::MouseExited(mouse_exited) diff --git a/crates/gpui_tokio/src/gpui_tokio.rs b/crates/gpui_tokio/src/gpui_tokio.rs index 61dcfc48efb1dfecc04c4a131ddc32691e01e255..9cfa1493af49ee95210edb9669a6ca89095f42cd 100644 --- a/crates/gpui_tokio/src/gpui_tokio.rs +++ b/crates/gpui_tokio/src/gpui_tokio.rs @@ -5,25 +5,48 @@ use util::defer; pub use tokio::task::JoinError; +/// Initializes the Tokio wrapper using a new Tokio runtime with 2 worker threads. +/// +/// If you need more threads (or access to the runtime outside of GPUI), you can create the runtime +/// yourself and pass a Handle to `init_from_handle`. pub fn init(cx: &mut App) { - cx.set_global(GlobalTokio::new()); + let runtime = tokio::runtime::Builder::new_multi_thread() + // Since we now have two executors, let's try to keep our footprint small + .worker_threads(2) + .enable_all() + .build() + .expect("Failed to initialize Tokio"); + + cx.set_global(GlobalTokio::new(RuntimeHolder::Owned(runtime))); +} + +/// Initializes the Tokio wrapper using a Tokio runtime handle. +pub fn init_from_handle(cx: &mut App, handle: tokio::runtime::Handle) { + cx.set_global(GlobalTokio::new(RuntimeHolder::Shared(handle))); +} + +enum RuntimeHolder { + Owned(tokio::runtime::Runtime), + Shared(tokio::runtime::Handle), +} + +impl RuntimeHolder { + pub fn handle(&self) -> &tokio::runtime::Handle { + match self { + RuntimeHolder::Owned(runtime) => runtime.handle(), + RuntimeHolder::Shared(handle) => handle, + } + } } struct GlobalTokio { - runtime: tokio::runtime::Runtime, + runtime: RuntimeHolder, } impl Global for GlobalTokio {} impl GlobalTokio { - fn new() -> Self { - let runtime = tokio::runtime::Builder::new_multi_thread() - // Since we now have two executors, let's try to keep our footprint small - .worker_threads(2) - .enable_all() - .build() - .expect("Failed to initialize Tokio"); - + fn new(runtime: RuntimeHolder) -> Self { Self { runtime } } } @@ -40,7 +63,7 @@ impl Tokio { R: Send + 'static, { cx.read_global(|tokio: &GlobalTokio, cx| { - let join_handle = tokio.runtime.spawn(f); + let join_handle = tokio.runtime.handle().spawn(f); let abort_handle = join_handle.abort_handle(); let cancel = defer(move || { abort_handle.abort(); @@ -62,7 +85,7 @@ impl Tokio { R: Send + 'static, { cx.read_global(|tokio: &GlobalTokio, cx| { - let join_handle = tokio.runtime.spawn(f); + let join_handle = tokio.runtime.handle().spawn(f); let abort_handle = join_handle.abort_handle(); let cancel = defer(move || { abort_handle.abort(); diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index bf4c74f984ff4aa8f06d6408957eddabcf5f94ed..23ae7a6d928d98aafe48d28cfe5626bbf76d29b8 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -260,6 +260,7 @@ pub enum IconName { XCircle, XCircleFilled, ZedAgent, + ZedAgentTwo, ZedAssistant, ZedBurnMode, ZedBurnModeOn, diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index 113d5026eb89587714172ff4c76698bcadb5fd6a..e81b1077c70d4eb3828715a6bcd28dfe564ab188 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -123,8 +123,8 @@ pub fn init(cx: &mut App) { }) } - cx.on_action(|_: &OpenKeymap, cx| common(None, cx)); - cx.on_action(|action: &ChangeKeybinding, cx| common(Some(action.action.clone()), cx)); + cx.on_action(|_: &OpenKeymap, cx| common(None, cx)) + .on_action(|action: &ChangeKeybinding, cx| common(Some(action.action.clone()), cx)); register_serializable_item::(cx); } diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml index 7c6470f4fa0c1eac847c1194e967b451093a76ad..e472521074109216bd243f5875dcc325cc9b3fed 100644 --- a/crates/language_model/Cargo.toml +++ b/crates/language_model/Cargo.toml @@ -18,6 +18,7 @@ test-support = [] [dependencies] anthropic = { workspace = true, features = ["schemars"] } anyhow.workspace = true +credentials_provider.workspace = true base64.workspace = true client.workspace = true cloud_api_types.workspace = true @@ -38,9 +39,9 @@ serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true -telemetry_events.workspace = true thiserror.workspace = true util.workspace = true +zed_env_vars.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/language_models/src/api_key.rs b/crates/language_model/src/api_key.rs similarity index 95% rename from crates/language_models/src/api_key.rs rename to crates/language_model/src/api_key.rs index 1f4271d04a24dc9bd1a22a1bb7682527086d7639..d2e156992130186373ad3307b9f51bb079946ff3 100644 --- a/crates/language_models/src/api_key.rs +++ b/crates/language_model/src/api_key.rs @@ -2,7 +2,6 @@ use anyhow::{Result, anyhow}; use credentials_provider::CredentialsProvider; use futures::{FutureExt, future}; use gpui::{AsyncApp, Context, SharedString, Task}; -use language_model::AuthenticateError; use std::{ fmt::{Display, Formatter}, sync::Arc, @@ -10,13 +9,16 @@ use std::{ use util::ResultExt as _; use zed_env_vars::EnvVar; +use crate::AuthenticateError; + /// Manages a single API key for a language model provider. API keys either come from environment /// variables or the system keychain. /// /// Keys from the system keychain are associated with a provider URL, and this ensures that they are /// only used with that URL. pub struct ApiKeyState { - url: SharedString, + pub url: SharedString, + env_var: EnvVar, load_status: LoadStatus, load_task: Option>>, } @@ -35,9 +37,10 @@ pub struct ApiKey { } impl ApiKeyState { - pub fn new(url: SharedString) -> Self { + pub fn new(url: SharedString, env_var: EnvVar) -> Self { Self { url, + env_var, load_status: LoadStatus::NotPresent, load_task: None, } @@ -47,6 +50,10 @@ impl ApiKeyState { matches!(self.load_status, LoadStatus::Loaded { .. }) } + pub fn env_var_name(&self) -> &SharedString { + &self.env_var.name + } + pub fn is_from_env_var(&self) -> bool { match &self.load_status { LoadStatus::Loaded(ApiKey { @@ -136,14 +143,13 @@ impl ApiKeyState { pub fn handle_url_change( &mut self, url: SharedString, - env_var: &EnvVar, get_this: impl Fn(&mut Ent) -> &mut Self + Clone + 'static, cx: &mut Context, ) { if url != self.url { if !self.is_from_env_var() { // loading will continue even though this result task is dropped - let _task = self.load_if_needed(url, env_var, get_this, cx); + let _task = self.load_if_needed(url, get_this, cx); } } } @@ -156,7 +162,6 @@ impl ApiKeyState { pub fn load_if_needed( &mut self, url: SharedString, - env_var: &EnvVar, get_this: impl Fn(&mut Ent) -> &mut Self + Clone + 'static, cx: &mut Context, ) -> Task> { @@ -166,10 +171,10 @@ impl ApiKeyState { return Task::ready(Ok(())); } - if let Some(key) = &env_var.value + if let Some(key) = &self.env_var.value && !key.is_empty() { - let api_key = ApiKey::from_env(env_var.name.clone(), key); + let api_key = ApiKey::from_env(self.env_var.name.clone(), key); self.url = url; self.load_status = LoadStatus::Loaded(api_key); self.load_task = None; diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index b2807fb666e58ec7c876b54f41e9bbbd6cfceb44..21756517a9c48cc72c7cc17bf62dff810ef0dd92 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -1,3 +1,4 @@ +mod api_key; mod model; mod rate_limiter; mod registry; @@ -30,6 +31,7 @@ use std::{fmt, io}; use thiserror::Error; use util::serde::is_default; +pub use crate::api_key::{ApiKey, ApiKeyState}; pub use crate::model::*; pub use crate::rate_limiter::*; pub use crate::registry::*; @@ -37,6 +39,7 @@ pub use crate::request::*; pub use crate::role::*; pub use crate::telemetry::*; pub use crate::tool_schema::LanguageModelToolSchemaFormat; +pub use zed_env_vars::{EnvVar, env_var}; pub const ANTHROPIC_PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("anthropic"); @@ -609,6 +612,11 @@ pub trait LanguageModel: Send + Sync { false } + /// Returns whether this model or provider supports streaming tool calls; + fn supports_streaming_tools(&self) -> bool { + false + } + fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { LanguageModelToolSchemaFormat::JsonSchema } @@ -763,6 +771,21 @@ pub trait LanguageModelExt: LanguageModel { } impl LanguageModelExt for dyn LanguageModel {} +impl std::fmt::Debug for dyn LanguageModel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("") + .field("id", &self.id()) + .field("name", &self.name()) + .field("provider_id", &self.provider_id()) + .field("provider_name", &self.provider_name()) + .field("upstream_provider_name", &self.upstream_provider_name()) + .field("upstream_provider_id", &self.upstream_provider_id()) + .field("upstream_provider_id", &self.upstream_provider_id()) + .field("supports_streaming_tools", &self.supports_streaming_tools()) + .finish() + } +} + /// An error that occurred when trying to authenticate the language model provider. #[derive(Debug, Error)] pub enum AuthenticateError { diff --git a/crates/language_model/src/telemetry.rs b/crates/language_model/src/telemetry.rs index ccdcb0ad0cdf0d830d0163f39afad478377fe01d..6d7f4df7f644115cae7b2148f4d78fde19674344 100644 --- a/crates/language_model/src/telemetry.rs +++ b/crates/language_model/src/telemetry.rs @@ -1,41 +1,101 @@ use crate::ANTHROPIC_PROVIDER_ID; use anthropic::ANTHROPIC_API_URL; use anyhow::{Context as _, anyhow}; -use client::telemetry::Telemetry; use gpui::BackgroundExecutor; use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; use std::env; use std::sync::Arc; -use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; use util::ResultExt; -pub fn report_assistant_event( - event: AssistantEventData, - telemetry: Option>, - client: Arc, - model_api_key: Option, - executor: &BackgroundExecutor, +#[derive(Clone, Debug)] +pub struct AnthropicEventData { + pub completion_type: AnthropicCompletionType, + pub event: AnthropicEventType, + pub language_name: Option, + pub message_id: Option, +} + +#[derive(Clone, Debug)] +pub enum AnthropicCompletionType { + Editor, + Terminal, + Panel, +} + +#[derive(Clone, Debug)] +pub enum AnthropicEventType { + Invoked, + Response, + Accept, + Reject, +} + +impl AnthropicCompletionType { + fn as_str(&self) -> &'static str { + match self { + Self::Editor => "natural_language_completion_in_editor", + Self::Terminal => "natural_language_completion_in_terminal", + Self::Panel => "conversation_message", + } + } +} + +impl AnthropicEventType { + fn as_str(&self) -> &'static str { + match self { + Self::Invoked => "invoke", + Self::Response => "response", + Self::Accept => "accept", + Self::Reject => "reject", + } + } +} + +pub fn report_anthropic_event( + model: &Arc, + event: AnthropicEventData, + cx: &gpui::App, ) { - if let Some(telemetry) = telemetry.as_ref() { - telemetry.report_assistant_event(event.clone()); - if telemetry.metrics_enabled() && event.model_provider == ANTHROPIC_PROVIDER_ID.0 { - if let Some(api_key) = model_api_key { - executor - .spawn(async move { - report_anthropic_event(event, client, api_key) - .await - .log_err(); - }) - .detach(); - } else { - log::error!("Cannot send Anthropic telemetry because API key is missing"); - } + let reporter = AnthropicEventReporter::new(model, cx); + reporter.report(event); +} + +#[derive(Clone)] +pub struct AnthropicEventReporter { + http_client: Arc, + executor: BackgroundExecutor, + api_key: Option, + is_anthropic: bool, +} + +impl AnthropicEventReporter { + pub fn new(model: &Arc, cx: &gpui::App) -> Self { + Self { + http_client: cx.http_client(), + executor: cx.background_executor().clone(), + api_key: model.api_key(cx), + is_anthropic: model.provider_id() == ANTHROPIC_PROVIDER_ID, } } + + pub fn report(&self, event: AnthropicEventData) { + if !self.is_anthropic { + return; + } + let Some(api_key) = self.api_key.clone() else { + return; + }; + let client = self.http_client.clone(); + self.executor + .spawn(async move { + send_anthropic_event(event, client, api_key).await.log_err(); + }) + .detach(); + } } -async fn report_anthropic_event( - event: AssistantEventData, +async fn send_anthropic_event( + event: AnthropicEventData, client: Arc, api_key: String, ) -> anyhow::Result<()> { @@ -45,18 +105,10 @@ async fn report_anthropic_event( .uri(uri) .header("X-Api-Key", api_key) .header("Content-Type", "application/json"); - let serialized_event: serde_json::Value = serde_json::json!({ - "completion_type": match event.kind { - AssistantKind::Inline => "natural_language_completion_in_editor", - AssistantKind::InlineTerminal => "natural_language_completion_in_terminal", - AssistantKind::Panel => "conversation_message", - }, - "event": match event.phase { - AssistantPhase::Response => "response", - AssistantPhase::Invoked => "invoke", - AssistantPhase::Accepted => "accept", - AssistantPhase::Rejected => "reject", - }, + + let serialized_event = serde_json::json!({ + "completion_type": event.completion_type.as_str(), + "event": event.event.as_str(), "metadata": { "language_name": event.language_name, "message_id": event.message_id, diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 4aaf9dcec5d33a8625297ffba98e1dbbc1c57fa8..1bec5d94d2bb35f91305c6c77a9e85ed8579e1af 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -62,7 +62,6 @@ ui_input.workspace = true util.workspace = true vercel = { workspace = true, features = ["schemars"] } x_ai = { workspace = true, features = ["schemars"] } -zed_env_vars.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index db10da3d07c1ea93ddf58f92baab3b0bc55b413f..ab93f46a1df0debc0fa9927ed3bbd8d651dc6ecc 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -7,12 +7,10 @@ use gpui::{App, Context, Entity}; use language_model::{LanguageModelProviderId, LanguageModelRegistry}; use provider::deepseek::DeepSeekLanguageModelProvider; -mod api_key; pub mod extension; mod google_ai_api_key; pub mod provider; mod settings; -pub mod ui; pub use crate::extension::init_proxy as init_extension_proxy; pub use crate::google_ai_api_key::api_key_for_gemini_cli; diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 1affe38a08d22e2aaed8c1207513ce41a13b8e59..25ba7615dc23e2561648e173588be6d93c28e295 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -8,25 +8,21 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::B use gpui::{AnyView, App, AsyncApp, Context, Entity, Task}; use http_client::HttpClient; use language_model::{ - AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, - LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelId, - LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, - LanguageModelToolResultContent, MessageContent, RateLimiter, Role, + ApiKeyState, AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModel, + LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent, + LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, + LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, + LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, + RateLimiter, Role, StopReason, env_var, }; -use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason}; use settings::{Settings, SettingsStore}; use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{List, prelude::*}; +use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*}; use ui_input::InputField; use util::ResultExt; -use zed_env_vars::{EnvVar, env_var}; - -use crate::api_key::ApiKeyState; -use crate::ui::{ConfiguredApiCard, InstructionListItem}; pub use settings::AnthropicAvailableModel as AvailableModel; @@ -65,12 +61,8 @@ impl State { fn authenticate(&mut self, cx: &mut Context) -> Task> { let api_url = AnthropicLanguageModelProvider::api_url(cx); - self.api_key_state.load_if_needed( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ) + self.api_key_state + .load_if_needed(api_url, |this| &mut this.api_key_state, cx) } } @@ -79,17 +71,13 @@ impl AnthropicLanguageModelProvider { let state = cx.new(|cx| { cx.observe_global::(|this: &mut State, cx| { let api_url = Self::api_url(cx); - this.api_key_state.handle_url_change( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ); + this.api_key_state + .handle_url_change(api_url, |this| &mut this.api_key_state, cx); cx.notify(); }) .detach(); State { - api_key_state: ApiKeyState::new(Self::api_url(cx)), + api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), } }); @@ -362,6 +350,10 @@ impl LanguageModel for AnthropicModel { true } + fn supports_streaming_tools(&self) -> bool { + true + } + fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { match choice { LanguageModelToolChoice::Auto @@ -937,14 +929,12 @@ impl Render for ConfigurationView { .child( List::new() .child( - InstructionListItem::new( - "Create one by visiting", - Some("Anthropic's settings"), - Some("https://console.anthropic.com/settings/keys") - ) + ListBulletItem::new("") + .child(Label::new("Create one by visiting")) + .child(ButtonLink::new("Anthropic's settings", "https://console.anthropic.com/settings/keys")) ) .child( - InstructionListItem::text_only("Paste your API key below and hit enter to start using the agent") + ListBulletItem::new("Paste your API key below and hit enter to start using the agent") ) ) .child(self.api_key_editor.clone()) @@ -953,7 +943,8 @@ impl Render for ConfigurationView { format!("You can also assign the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."), ) .size(LabelSize::Small) - .color(Color::Muted), + .color(Color::Muted) + .mt_0p5(), ) .into_any_element() } else { diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index e478c193a27a9e30301ae9233ea666c8160b25f5..b85a038bb235d97bd9de8614f19764ecabf7bbfe 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -2,7 +2,6 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::Arc; -use crate::ui::{ConfiguredApiCard, InstructionListItem}; use anyhow::{Context as _, Result, anyhow}; use aws_config::stalled_stream_protection::StalledStreamProtectionConfig; use aws_config::{BehaviorVersion, Region}; @@ -44,7 +43,7 @@ use serde_json::Value; use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore}; use smol::lock::OnceCell; use strum::{EnumIter, IntoEnumIterator, IntoStaticStr}; -use ui::{List, prelude::*}; +use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*}; use ui_input::InputField; use util::ResultExt; @@ -1250,18 +1249,14 @@ impl Render for ConfigurationView { .child( List::new() .child( - InstructionListItem::new( - "Grant permissions to the strategy you'll use according to the:", - Some("Prerequisites"), - Some("https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"), - ) + ListBulletItem::new("") + .child(Label::new("Grant permissions to the strategy you'll use according to the:")) + .child(ButtonLink::new("Prerequisites", "https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html")) ) .child( - InstructionListItem::new( - "Select the models you would like access to:", - Some("Bedrock Model Catalog"), - Some("https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess"), - ) + ListBulletItem::new("") + .child(Label::new("Select the models you would like access to:")) + .child(ButtonLink::new("Bedrock Model Catalog", "https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess")) ) ) .child(self.render_static_credentials_ui()) @@ -1302,22 +1297,22 @@ impl ConfigurationView { ) .child( List::new() - .child(InstructionListItem::new( - "Create an IAM user in the AWS console with programmatic access", - Some("IAM Console"), - Some("https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/users"), - )) - .child(InstructionListItem::new( - "Attach the necessary Bedrock permissions to this ", - Some("user"), - Some("https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"), - )) - .child(InstructionListItem::text_only( - "Copy the access key ID and secret access key when provided", - )) - .child(InstructionListItem::text_only( - "Enter these credentials below", - )), + .child( + ListBulletItem::new("") + .child(Label::new("Create an IAM user in the AWS console with programmatic access")) + .child(ButtonLink::new("IAM Console", "https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/users")) + ) + .child( + ListBulletItem::new("") + .child(Label::new("Attach the necessary Bedrock permissions to this")) + .child(ButtonLink::new("user", "https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html")) + ) + .child( + ListBulletItem::new("Copy the access key ID and secret access key when provided") + ) + .child( + ListBulletItem::new("Enter these credentials below") + ) ) .child(self.access_key_id_editor.clone()) .child(self.secret_access_key_editor.clone()) diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index a19a427dbacb32883b1877888ec04899a2b8d427..508a77d38abcf2143170382e945ab6ce31f3a623 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -602,6 +602,10 @@ impl LanguageModel for CloudLanguageModel { self.model.supports_images } + fn supports_streaming_tools(&self) -> bool { + self.model.supports_streaming_tools + } + fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { match choice { LanguageModelToolChoice::Auto diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index 92ac342a39ff04ae42f5b01b5777a5d16563c37f..70198b337e467e1618192e781d3e3be305fea9c5 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -14,7 +14,7 @@ use copilot::{Copilot, Status}; use futures::future::BoxFuture; use futures::stream::BoxStream; use futures::{FutureExt, Stream, StreamExt}; -use gpui::{Action, AnyView, App, AsyncApp, Entity, Render, Subscription, Task, svg}; +use gpui::{AnyView, App, AsyncApp, Entity, Subscription, Task}; use http_client::StatusCode; use language::language_settings::all_language_settings; use language_model::{ @@ -26,11 +26,9 @@ use language_model::{ StopReason, TokenUsage, }; use settings::SettingsStore; -use ui::{CommonAnimationExt, prelude::*}; +use ui::prelude::*; use util::debug_panic; -use crate::ui::ConfiguredApiCard; - const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("copilot_chat"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("GitHub Copilot Chat"); @@ -179,8 +177,18 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider { _: &mut Window, cx: &mut App, ) -> AnyView { - let state = self.state.clone(); - cx.new(|cx| ConfigurationView::new(state, cx)).into() + cx.new(|cx| { + copilot::ConfigurationView::new( + |cx| { + CopilotChat::global(cx) + .map(|m| m.read(cx).is_authenticated()) + .unwrap_or(false) + }, + copilot::ConfigurationMode::Chat, + cx, + ) + }) + .into() } fn reset_credentials(&self, _cx: &mut App) -> Task> { @@ -1474,92 +1482,3 @@ mod tests { ); } } -struct ConfigurationView { - copilot_status: Option, - state: Entity, - _subscription: Option, -} - -impl ConfigurationView { - pub fn new(state: Entity, cx: &mut Context) -> Self { - let copilot = Copilot::global(cx); - - Self { - copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()), - state, - _subscription: copilot.as_ref().map(|copilot| { - cx.observe(copilot, |this, model, cx| { - this.copilot_status = Some(model.read(cx).status()); - cx.notify(); - }) - }), - } - } -} - -impl Render for ConfigurationView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.state.read(cx).is_authenticated(cx) { - ConfiguredApiCard::new("Authorized") - .button_label("Sign Out") - .on_click(|_, window, cx| { - window.dispatch_action(copilot::SignOut.boxed_clone(), cx); - }) - .into_any_element() - } else { - let loading_icon = Icon::new(IconName::ArrowCircle).with_rotate_animation(4); - - const ERROR_LABEL: &str = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different Assistant provider."; - - match &self.copilot_status { - Some(status) => match status { - Status::Starting { task: _ } => h_flex() - .gap_2() - .child(loading_icon) - .child(Label::new("Starting Copilot…")) - .into_any_element(), - Status::SigningIn { prompt: _ } - | Status::SignedOut { - awaiting_signing_in: true, - } => h_flex() - .gap_2() - .child(loading_icon) - .child(Label::new("Signing into Copilot…")) - .into_any_element(), - Status::Error(_) => { - const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot."; - v_flex() - .gap_6() - .child(Label::new(LABEL)) - .child(svg().size_8().path(IconName::CopilotError.path())) - .into_any_element() - } - _ => { - const LABEL: &str = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription."; - - v_flex() - .gap_2() - .child(Label::new(LABEL)) - .child( - Button::new("sign_in", "Sign in to use GitHub Copilot") - .full_width() - .style(ButtonStyle::Outlined) - .icon_color(Color::Muted) - .icon(IconName::Github) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .on_click(|_, window, cx| { - copilot::initiate_sign_in(window, cx) - }), - ) - .into_any_element() - } - }, - None => v_flex() - .gap_6() - .child(Label::new(ERROR_LABEL)) - .into_any_element(), - } - } - } -} diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index 91b83bb9f1d0f08fe70f5e750ff8ce993a7afd7f..b00a5d82f5665a5c87c662d1af84fbeb9ac07ebb 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -7,11 +7,11 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture, stream::BoxStream use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window}; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, - RateLimiter, Role, StopReason, TokenUsage, + ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent, + LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var, }; pub use settings::DeepseekAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; @@ -19,13 +19,9 @@ use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, LazyLock}; -use ui::{List, prelude::*}; +use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*}; use ui_input::InputField; use util::ResultExt; -use zed_env_vars::{EnvVar, env_var}; - -use crate::ui::ConfiguredApiCard; -use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("deepseek"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("DeepSeek"); @@ -67,12 +63,8 @@ impl State { fn authenticate(&mut self, cx: &mut Context) -> Task> { let api_url = DeepSeekLanguageModelProvider::api_url(cx); - self.api_key_state.load_if_needed( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ) + self.api_key_state + .load_if_needed(api_url, |this| &mut this.api_key_state, cx) } } @@ -81,17 +73,13 @@ impl DeepSeekLanguageModelProvider { let state = cx.new(|cx| { cx.observe_global::(|this: &mut State, cx| { let api_url = Self::api_url(cx); - this.api_key_state.handle_url_change( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ); + this.api_key_state + .handle_url_change(api_url, |this| &mut this.api_key_state, cx); cx.notify(); }) .detach(); State { - api_key_state: ApiKeyState::new(Self::api_url(cx)), + api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), } }); @@ -632,12 +620,15 @@ impl Render for ConfigurationView { .child(Label::new("To use DeepSeek in Zed, you need an API key:")) .child( List::new() - .child(InstructionListItem::new( - "Get your API key from the", - Some("DeepSeek console"), - Some("https://platform.deepseek.com/api_keys"), - )) - .child(InstructionListItem::text_only( + .child( + ListBulletItem::new("") + .child(Label::new("Get your API key from the")) + .child(ButtonLink::new( + "DeepSeek console", + "https://platform.deepseek.com/api_keys", + )), + ) + .child(ListBulletItem::new( "Paste your API key below and hit enter to start using the assistant", )), ) diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index a80d36c414e126cc08c641c44ce5ea2ee732a5c6..0a7df23fa04222cb087d56b5034af2efdbdcbd3a 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -8,7 +8,7 @@ use google_ai::{ use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window}; use http_client::HttpClient; use language_model::{ - AuthenticateError, ConfigurationViewTargetAgent, LanguageModelCompletionError, + AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason, }; @@ -27,13 +27,11 @@ use std::sync::{ atomic::{self, AtomicU64}, }; use strum::IntoEnumIterator; -use ui::{List, prelude::*}; +use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*}; use ui_input::InputField; use util::ResultExt; -use zed_env_vars::EnvVar; -use crate::api_key::ApiKeyState; -use crate::ui::{ConfiguredApiCard, InstructionListItem}; +use language_model::ApiKeyState; const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID; const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME; @@ -85,12 +83,8 @@ impl State { fn authenticate(&mut self, cx: &mut Context) -> Task> { let api_url = GoogleLanguageModelProvider::api_url(cx); - self.api_key_state.load_if_needed( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ) + self.api_key_state + .load_if_needed(api_url, |this| &mut this.api_key_state, cx) } } @@ -99,17 +93,13 @@ impl GoogleLanguageModelProvider { let state = cx.new(|cx| { cx.observe_global::(|this: &mut State, cx| { let api_url = Self::api_url(cx); - this.api_key_state.handle_url_change( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ); + this.api_key_state + .handle_url_change(api_url, |this| &mut this.api_key_state, cx); cx.notify(); }) .detach(); State { - api_key_state: ApiKeyState::new(Self::api_url(cx)), + api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), } }); @@ -855,14 +845,14 @@ impl Render for ConfigurationView { }))) .child( List::new() - .child(InstructionListItem::new( - "Create one by visiting", - Some("Google AI's console"), - Some("https://aistudio.google.com/app/apikey"), - )) - .child(InstructionListItem::text_only( - "Paste your API key below and hit enter to start using the assistant", - )), + .child( + ListBulletItem::new("") + .child(Label::new("Create one by visiting")) + .child(ButtonLink::new("Google AI's console", "https://aistudio.google.com/app/apikey")) + ) + .child( + ListBulletItem::new("Paste your API key below and hit enter to start using the agent") + ) ) .child(self.api_key_editor.clone()) .child( diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index a16bd351a9d779bcba5b2a4111fc62e0dc9dc639..8e42d12db4c24ef6a66ddef470a34c620ed7ee00 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -20,11 +20,10 @@ use settings::{Settings, SettingsStore}; use std::pin::Pin; use std::str::FromStr; use std::{collections::BTreeMap, sync::Arc}; -use ui::{ButtonLike, Indicator, List, prelude::*}; +use ui::{ButtonLike, Indicator, InlineCode, List, ListBulletItem, prelude::*}; use util::ResultExt; use crate::AllLanguageModelSettings; -use crate::ui::InstructionListItem; const LMSTUDIO_DOWNLOAD_URL: &str = "https://lmstudio.ai/download"; const LMSTUDIO_CATALOG_URL: &str = "https://lmstudio.ai/models"; @@ -686,12 +685,14 @@ impl Render for ConfigurationView { .child( v_flex().gap_1().child(Label::new(lmstudio_intro)).child( List::new() - .child(InstructionListItem::text_only( + .child(ListBulletItem::new( "LM Studio needs to be running with at least one model downloaded.", )) - .child(InstructionListItem::text_only( - "To get your first model, try running `lms get qwen2.5-coder-7b`", - )), + .child( + ListBulletItem::new("") + .child(Label::new("To get your first model, try running")) + .child(InlineCode::new("lms get qwen2.5-coder-7b")), + ), ), ) .child( diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index 8372a8c95e579f1d860fd9bb25656731ee2c7e50..3e99f32be8224bb2b9973feccb0ce973b58eaaed 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -1,31 +1,27 @@ use anyhow::{Result, anyhow}; use collections::BTreeMap; -use fs::Fs; + use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, Context, Entity, Global, SharedString, Task, Window}; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, - RateLimiter, Role, StopReason, TokenUsage, + ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent, + LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var, }; -use mistral::{CODESTRAL_API_URL, MISTRAL_API_URL, StreamResponse}; +pub use mistral::{CODESTRAL_API_URL, MISTRAL_API_URL, StreamResponse}; pub use settings::MistralAvailableModel as AvailableModel; -use settings::{EditPredictionProvider, Settings, SettingsStore, update_settings_file}; +use settings::{Settings, SettingsStore}; use std::collections::HashMap; use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{List, prelude::*}; +use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*}; use ui_input::InputField; use util::ResultExt; -use zed_env_vars::{EnvVar, env_var}; - -use crate::ui::ConfiguredApiCard; -use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("mistral"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Mistral"); @@ -44,12 +40,26 @@ pub struct MistralSettings { pub struct MistralLanguageModelProvider { http_client: Arc, - state: Entity, + pub state: Entity, } pub struct State { api_key_state: ApiKeyState, - codestral_api_key_state: ApiKeyState, + codestral_api_key_state: Entity, +} + +struct CodestralApiKey(Entity); +impl Global for CodestralApiKey {} + +pub fn codestral_api_key(cx: &mut App) -> Entity { + if cx.has_global::() { + cx.global::().0.clone() + } else { + let api_key_state = cx + .new(|_| ApiKeyState::new(CODESTRAL_API_URL.into(), CODESTRAL_API_KEY_ENV_VAR.clone())); + cx.set_global(CodestralApiKey(api_key_state.clone())); + api_key_state + } } impl State { @@ -63,39 +73,19 @@ impl State { .store(api_url, api_key, |this| &mut this.api_key_state, cx) } - fn set_codestral_api_key( - &mut self, - api_key: Option, - cx: &mut Context, - ) -> Task> { - self.codestral_api_key_state.store( - CODESTRAL_API_URL.into(), - api_key, - |this| &mut this.codestral_api_key_state, - cx, - ) - } - fn authenticate(&mut self, cx: &mut Context) -> Task> { let api_url = MistralLanguageModelProvider::api_url(cx); - self.api_key_state.load_if_needed( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ) + self.api_key_state + .load_if_needed(api_url, |this| &mut this.api_key_state, cx) } fn authenticate_codestral( &mut self, cx: &mut Context, ) -> Task> { - self.codestral_api_key_state.load_if_needed( - CODESTRAL_API_URL.into(), - &CODESTRAL_API_KEY_ENV_VAR, - |this| &mut this.codestral_api_key_state, - cx, - ) + self.codestral_api_key_state.update(cx, |state, cx| { + state.load_if_needed(CODESTRAL_API_URL.into(), |state| state, cx) + }) } } @@ -116,18 +106,14 @@ impl MistralLanguageModelProvider { let state = cx.new(|cx| { cx.observe_global::(|this: &mut State, cx| { let api_url = Self::api_url(cx); - this.api_key_state.handle_url_change( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ); + this.api_key_state + .handle_url_change(api_url, |this| &mut this.api_key_state, cx); cx.notify(); }) .detach(); State { - api_key_state: ApiKeyState::new(Self::api_url(cx)), - codestral_api_key_state: ApiKeyState::new(CODESTRAL_API_URL.into()), + api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), + codestral_api_key_state: codestral_api_key(cx), } }); @@ -142,7 +128,11 @@ impl MistralLanguageModelProvider { } pub fn codestral_api_key(&self, url: &str, cx: &App) -> Option> { - self.state.read(cx).codestral_api_key_state.key(url) + self.state + .read(cx) + .codestral_api_key_state + .read(cx) + .key(url) } fn create_language_model(&self, model: mistral::Model) -> Arc { @@ -159,7 +149,7 @@ impl MistralLanguageModelProvider { &crate::AllLanguageModelSettings::get_global(cx).mistral } - fn api_url(cx: &App) -> SharedString { + pub fn api_url(cx: &App) -> SharedString { let api_url = &Self::settings(cx).api_url; if api_url.is_empty() { mistral::MISTRAL_API_URL.into() @@ -747,7 +737,6 @@ struct RawToolCall { struct ConfigurationView { api_key_editor: Entity, - codestral_api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -756,8 +745,6 @@ impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = cx.new(|cx| InputField::new(window, cx, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")); - let codestral_api_key_editor = - cx.new(|cx| InputField::new(window, cx, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")); cx.observe(&state, |_, _, cx| { cx.notify(); @@ -774,12 +761,6 @@ impl ConfigurationView { // We don't log an error, because "not signed in" is also an error. let _ = task.await; } - if let Some(task) = state - .update(cx, |state, cx| state.authenticate_codestral(cx)) - .log_err() - { - let _ = task.await; - } this.update(cx, |this, cx| { this.load_credentials_task = None; @@ -791,7 +772,6 @@ impl ConfigurationView { Self { api_key_editor, - codestral_api_key_editor, state, load_credentials_task, } @@ -829,110 +809,9 @@ impl ConfigurationView { .detach_and_log_err(cx); } - fn save_codestral_api_key( - &mut self, - _: &menu::Confirm, - window: &mut Window, - cx: &mut Context, - ) { - let api_key = self - .codestral_api_key_editor - .read(cx) - .text(cx) - .trim() - .to_string(); - if api_key.is_empty() { - return; - } - - // url changes can cause the editor to be displayed again - self.codestral_api_key_editor - .update(cx, |editor, cx| editor.set_text("", window, cx)); - - let state = self.state.clone(); - cx.spawn_in(window, async move |_, cx| { - state - .update(cx, |state, cx| { - state.set_codestral_api_key(Some(api_key), cx) - })? - .await?; - cx.update(|_window, cx| { - set_edit_prediction_provider(EditPredictionProvider::Codestral, cx) - }) - }) - .detach_and_log_err(cx); - } - - fn reset_codestral_api_key(&mut self, window: &mut Window, cx: &mut Context) { - self.codestral_api_key_editor - .update(cx, |editor, cx| editor.set_text("", window, cx)); - - let state = self.state.clone(); - cx.spawn_in(window, async move |_, cx| { - state - .update(cx, |state, cx| state.set_codestral_api_key(None, cx))? - .await?; - cx.update(|_window, cx| set_edit_prediction_provider(EditPredictionProvider::Zed, cx)) - }) - .detach_and_log_err(cx); - } - fn should_render_api_key_editor(&self, cx: &mut Context) -> bool { !self.state.read(cx).is_authenticated() } - - fn render_codestral_api_key_editor(&mut self, cx: &mut Context) -> AnyElement { - let key_state = &self.state.read(cx).codestral_api_key_state; - let should_show_editor = !key_state.has_key(); - let env_var_set = key_state.is_from_env_var(); - let configured_card_label = if env_var_set { - format!("API key set in {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable") - } else { - "Codestral API key configured".to_string() - }; - - if should_show_editor { - v_flex() - .id("codestral") - .size_full() - .mt_2() - .on_action(cx.listener(Self::save_codestral_api_key)) - .child(Label::new( - "To use Codestral as an edit prediction provider, \ - you need to add a Codestral-specific API key. Follow these steps:", - )) - .child( - List::new() - .child(InstructionListItem::new( - "Create one by visiting", - Some("the Codestral section of Mistral's console"), - Some("https://console.mistral.ai/codestral"), - )) - .child(InstructionListItem::text_only("Paste your API key below and hit enter")), - ) - .child(self.codestral_api_key_editor.clone()) - .child( - Label::new( - format!("You can also assign the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable and restart Zed."), - ) - .size(LabelSize::Small).color(Color::Muted), - ).into_any() - } else { - ConfiguredApiCard::new(configured_card_label) - .disabled(env_var_set) - .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))) - .when(env_var_set, |this| { - this.tooltip_label(format!( - "To reset your API key, \ - unset the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable." - )) - }) - .on_click( - cx.listener(|this, _, window, cx| this.reset_codestral_api_key(window, cx)), - ) - .into_any_element() - } - } } impl Render for ConfigurationView { @@ -958,17 +837,17 @@ impl Render for ConfigurationView { .child(Label::new("To use Zed's agent with Mistral, you need to add an API key. Follow these steps:")) .child( List::new() - .child(InstructionListItem::new( - "Create one by visiting", - Some("Mistral's console"), - Some("https://console.mistral.ai/api-keys"), - )) - .child(InstructionListItem::text_only( - "Ensure your Mistral account has credits", - )) - .child(InstructionListItem::text_only( - "Paste your API key below and hit enter to start using the assistant", - )), + .child( + ListBulletItem::new("") + .child(Label::new("Create one by visiting")) + .child(ButtonLink::new("Mistral's console", "https://console.mistral.ai/api-keys")) + ) + .child( + ListBulletItem::new("Ensure your Mistral account has credits") + ) + .child( + ListBulletItem::new("Paste your API key below and hit enter to start using the assistant") + ), ) .child(self.api_key_editor.clone()) .child( @@ -977,7 +856,6 @@ impl Render for ConfigurationView { ) .size(LabelSize::Small).color(Color::Muted), ) - .child(self.render_codestral_api_key_editor(cx)) .into_any() } else { v_flex() @@ -994,24 +872,11 @@ impl Render for ConfigurationView { )) }), ) - .child(self.render_codestral_api_key_editor(cx)) .into_any() } } } -fn set_edit_prediction_provider(provider: EditPredictionProvider, cx: &mut App) { - let fs = ::global(cx); - update_settings_file(fs, cx, move |settings, _| { - settings - .project - .all_languages - .features - .get_or_insert_default() - .edit_prediction_provider = Some(provider); - }); -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 8345db3cce9fc51c487ec039c4257bfb39b162c3..c961001e65be662e0023b3199f68dfbf4989e604 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -5,11 +5,11 @@ use futures::{Stream, TryFutureExt, stream}; use gpui::{AnyView, App, AsyncApp, Context, CursorStyle, Entity, Task}; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse, - LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage, + ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse, + LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var, }; use menu; use ollama::{ @@ -22,13 +22,13 @@ use std::pin::Pin; use std::sync::LazyLock; use std::sync::atomic::{AtomicU64, Ordering}; use std::{collections::HashMap, sync::Arc}; -use ui::{ButtonLike, ElevationIndex, List, Tooltip, prelude::*}; +use ui::{ + ButtonLike, ButtonLink, ConfiguredApiCard, ElevationIndex, InlineCode, List, ListBulletItem, + Tooltip, prelude::*, +}; use ui_input::InputField; -use zed_env_vars::{EnvVar, env_var}; use crate::AllLanguageModelSettings; -use crate::api_key::ApiKeyState; -use crate::ui::{ConfiguredApiCard, InstructionListItem}; const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download"; const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library"; @@ -80,12 +80,9 @@ impl State { fn authenticate(&mut self, cx: &mut Context) -> Task> { let api_url = OllamaLanguageModelProvider::api_url(cx); - let task = self.api_key_state.load_if_needed( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ); + let task = self + .api_key_state + .load_if_needed(api_url, |this| &mut this.api_key_state, cx); // Always try to fetch models - if no API key is needed (local Ollama), it will work // If API key is needed and provided, it will work @@ -185,7 +182,7 @@ impl OllamaLanguageModelProvider { http_client, fetched_models: Default::default(), fetch_model_task: None, - api_key_state: ApiKeyState::new(Self::api_url(cx)), + api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), } }), }; @@ -733,15 +730,17 @@ impl ConfigurationView { .child(Label::new("To use local Ollama:")) .child( List::new() - .child(InstructionListItem::new( - "Download and install Ollama from", - Some("ollama.com"), - Some("https://ollama.com/download"), - )) - .child(InstructionListItem::text_only( - "Start Ollama and download a model: `ollama run gpt-oss:20b`", - )) - .child(InstructionListItem::text_only( + .child( + ListBulletItem::new("") + .child(Label::new("Download and install Ollama from")) + .child(ButtonLink::new("ollama.com", "https://ollama.com/download")), + ) + .child( + ListBulletItem::new("") + .child(Label::new("Start Ollama and download a model:")) + .child(InlineCode::new("ollama run gpt-oss:20b")), + ) + .child(ListBulletItem::new( "Click 'Connect' below to start using Ollama in Zed", )), ) diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 403b025f518681f335f28e35d11450bef046fca2..afaffba3e53eb2496f9fae795d69b9e9c9f57249 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -5,11 +5,11 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture}; use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window}; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, - RateLimiter, Role, StopReason, TokenUsage, + ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent, + LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var, }; use menu; use open_ai::{ @@ -20,13 +20,9 @@ use std::pin::Pin; use std::str::FromStr as _; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{List, prelude::*}; +use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*}; use ui_input::InputField; use util::ResultExt; -use zed_env_vars::{EnvVar, env_var}; - -use crate::ui::ConfiguredApiCard; -use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = language_model::OPEN_AI_PROVIDER_ID; const PROVIDER_NAME: LanguageModelProviderName = language_model::OPEN_AI_PROVIDER_NAME; @@ -62,12 +58,8 @@ impl State { fn authenticate(&mut self, cx: &mut Context) -> Task> { let api_url = OpenAiLanguageModelProvider::api_url(cx); - self.api_key_state.load_if_needed( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ) + self.api_key_state + .load_if_needed(api_url, |this| &mut this.api_key_state, cx) } } @@ -76,17 +68,13 @@ impl OpenAiLanguageModelProvider { let state = cx.new(|cx| { cx.observe_global::(|this: &mut State, cx| { let api_url = Self::api_url(cx); - this.api_key_state.handle_url_change( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ); + this.api_key_state + .handle_url_change(api_url, |this| &mut this.api_key_state, cx); cx.notify(); }) .detach(); State { - api_key_state: ApiKeyState::new(Self::api_url(cx)), + api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), } }); @@ -790,17 +778,17 @@ impl Render for ConfigurationView { .child(Label::new("To use Zed's agent with OpenAI, you need to add an API key. Follow these steps:")) .child( List::new() - .child(InstructionListItem::new( - "Create one by visiting", - Some("OpenAI's console"), - Some("https://platform.openai.com/api-keys"), - )) - .child(InstructionListItem::text_only( - "Ensure your OpenAI account has credits", - )) - .child(InstructionListItem::text_only( - "Paste your API key below and hit enter to start using the assistant", - )), + .child( + ListBulletItem::new("") + .child(Label::new("Create one by visiting")) + .child(ButtonLink::new("OpenAI's console", "https://platform.openai.com/api-keys")) + ) + .child( + ListBulletItem::new("Ensure your OpenAI account has credits") + ) + .child( + ListBulletItem::new("Paste your API key below and hit enter to start using the agent") + ), ) .child(self.api_key_editor.clone()) .child( diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index a30c8bfa5d3a728d6dd388f8e768cd470ee9736d..e6e7a9984da3d48b9e3c0f9571b8e916359fba03 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -4,10 +4,10 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture}; use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window}; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, + ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, }; use menu; use open_ai::{ResponseStreamEvent, stream_completion}; @@ -16,9 +16,7 @@ use std::sync::Arc; use ui::{ElevationIndex, Tooltip, prelude::*}; use ui_input::InputField; use util::ResultExt; -use zed_env_vars::EnvVar; -use crate::api_key::ApiKeyState; use crate::provider::open_ai::{OpenAiEventMapper, into_open_ai}; pub use settings::OpenAiCompatibleAvailableModel as AvailableModel; pub use settings::OpenAiCompatibleModelCapabilities as ModelCapabilities; @@ -38,7 +36,6 @@ pub struct OpenAiCompatibleLanguageModelProvider { pub struct State { id: Arc, - api_key_env_var: EnvVar, api_key_state: ApiKeyState, settings: OpenAiCompatibleSettings, } @@ -56,12 +53,8 @@ impl State { fn authenticate(&mut self, cx: &mut Context) -> Task> { let api_url = SharedString::new(self.settings.api_url.clone()); - self.api_key_state.load_if_needed( - api_url, - &self.api_key_env_var, - |this| &mut this.api_key_state, - cx, - ) + self.api_key_state + .load_if_needed(api_url, |this| &mut this.api_key_state, cx) } } @@ -83,7 +76,6 @@ impl OpenAiCompatibleLanguageModelProvider { let api_url = SharedString::new(settings.api_url.as_str()); this.api_key_state.handle_url_change( api_url, - &this.api_key_env_var, |this| &mut this.api_key_state, cx, ); @@ -95,8 +87,10 @@ impl OpenAiCompatibleLanguageModelProvider { let settings = resolve_settings(&id, cx).cloned().unwrap_or_default(); State { id: id.clone(), - api_key_env_var: EnvVar::new(api_key_env_var_name), - api_key_state: ApiKeyState::new(SharedString::new(settings.api_url.as_str())), + api_key_state: ApiKeyState::new( + SharedString::new(settings.api_url.as_str()), + EnvVar::new(api_key_env_var_name), + ), settings, } }); @@ -437,7 +431,7 @@ impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let state = self.state.read(cx); let env_var_set = state.api_key_state.is_from_env_var(); - let env_var_name = &state.api_key_env_var.name; + let env_var_name = state.api_key_state.env_var_name(); let api_key_section = if self.should_render_editor(cx) { v_flex() diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 7b10ebf963033603ede691fa72d2fa523bcdbab9..ad2e90d9dd5f4ece7e2582a867da50f6962c981c 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -4,11 +4,12 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture}; use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task}; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, - LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, + ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent, + LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role, + StopReason, TokenUsage, env_var, }; use open_router::{ Model, ModelMode as OpenRouterModelMode, OPEN_ROUTER_API_URL, ResponseStreamEvent, list_models, @@ -17,13 +18,9 @@ use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsSto use std::pin::Pin; use std::str::FromStr as _; use std::sync::{Arc, LazyLock}; -use ui::{List, prelude::*}; +use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*}; use ui_input::InputField; use util::ResultExt; -use zed_env_vars::{EnvVar, env_var}; - -use crate::ui::ConfiguredApiCard; -use crate::{api_key::ApiKeyState, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("OpenRouter"); @@ -62,12 +59,9 @@ impl State { fn authenticate(&mut self, cx: &mut Context) -> Task> { let api_url = OpenRouterLanguageModelProvider::api_url(cx); - let task = self.api_key_state.load_if_needed( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ); + let task = self + .api_key_state + .load_if_needed(api_url, |this| &mut this.api_key_state, cx); cx.spawn(async move |this, cx| { let result = task.await; @@ -135,7 +129,7 @@ impl OpenRouterLanguageModelProvider { }) .detach(); State { - api_key_state: ApiKeyState::new(Self::api_url(cx)), + api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), http_client: http_client.clone(), available_models: Vec::new(), fetch_models_task: None, @@ -830,17 +824,15 @@ impl Render for ConfigurationView { .child(Label::new("To use Zed's agent with OpenRouter, you need to add an API key. Follow these steps:")) .child( List::new() - .child(InstructionListItem::new( - "Create an API key by visiting", - Some("OpenRouter's console"), - Some("https://openrouter.ai/keys"), - )) - .child(InstructionListItem::text_only( - "Ensure your OpenRouter account has credits", - )) - .child(InstructionListItem::text_only( - "Paste your API key below and hit enter to start using the assistant", - )), + .child( + ListBulletItem::new("") + .child(Label::new("Create an API key by visiting")) + .child(ButtonLink::new("OpenRouter's console", "https://openrouter.ai/keys")) + ) + .child(ListBulletItem::new("Ensure your OpenRouter account has credits") + ) + .child(ListBulletItem::new("Paste your API key below and hit enter to start using the assistant") + ), ) .child(self.api_key_editor.clone()) .child( diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 061dc1799922c03952b1a96e2785425f61bcf00b..4dfe848df80123dc4c37d27b81f76db359e076f9 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -4,26 +4,20 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture}; use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window}; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelToolChoice, RateLimiter, Role, + ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, LanguageModelToolChoice, RateLimiter, Role, env_var, }; use open_ai::ResponseStreamEvent; pub use settings::VercelAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{List, prelude::*}; +use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*}; use ui_input::InputField; use util::ResultExt; use vercel::{Model, VERCEL_API_URL}; -use zed_env_vars::{EnvVar, env_var}; - -use crate::{ - api_key::ApiKeyState, - ui::{ConfiguredApiCard, InstructionListItem}, -}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("vercel"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Vercel"); @@ -59,12 +53,8 @@ impl State { fn authenticate(&mut self, cx: &mut Context) -> Task> { let api_url = VercelLanguageModelProvider::api_url(cx); - self.api_key_state.load_if_needed( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ) + self.api_key_state + .load_if_needed(api_url, |this| &mut this.api_key_state, cx) } } @@ -73,17 +63,13 @@ impl VercelLanguageModelProvider { let state = cx.new(|cx| { cx.observe_global::(|this: &mut State, cx| { let api_url = Self::api_url(cx); - this.api_key_state.handle_url_change( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ); + this.api_key_state + .handle_url_change(api_url, |this| &mut this.api_key_state, cx); cx.notify(); }) .detach(); State { - api_key_state: ApiKeyState::new(Self::api_url(cx)), + api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), } }); @@ -472,14 +458,14 @@ impl Render for ConfigurationView { .child(Label::new("To use Zed's agent with Vercel v0, you need to add an API key. Follow these steps:")) .child( List::new() - .child(InstructionListItem::new( - "Create one by visiting", - Some("Vercel v0's console"), - Some("https://v0.dev/chat/settings/keys"), - )) - .child(InstructionListItem::text_only( - "Paste your API key below and hit enter to start using the agent", - )), + .child( + ListBulletItem::new("") + .child(Label::new("Create one by visiting")) + .child(ButtonLink::new("Vercel v0's console", "https://v0.dev/chat/settings/keys")) + ) + .child( + ListBulletItem::new("Paste your API key below and hit enter to start using the agent") + ), ) .child(self.api_key_editor.clone()) .child( diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index cc54dfa0dd8a3f2ca6ab2b769a779afa8e73988b..19c50d71cf4e483b68d48c8b982a975f3091ff46 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -4,26 +4,21 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture}; use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, Window}; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, Role, + ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, + Role, env_var, }; use open_ai::ResponseStreamEvent; pub use settings::XaiAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; -use ui::{List, prelude::*}; +use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*}; use ui_input::InputField; use util::ResultExt; use x_ai::{Model, XAI_API_URL}; -use zed_env_vars::{EnvVar, env_var}; - -use crate::{ - api_key::ApiKeyState, - ui::{ConfiguredApiCard, InstructionListItem}, -}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("x_ai"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("xAI"); @@ -59,12 +54,8 @@ impl State { fn authenticate(&mut self, cx: &mut Context) -> Task> { let api_url = XAiLanguageModelProvider::api_url(cx); - self.api_key_state.load_if_needed( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ) + self.api_key_state + .load_if_needed(api_url, |this| &mut this.api_key_state, cx) } } @@ -73,17 +64,13 @@ impl XAiLanguageModelProvider { let state = cx.new(|cx| { cx.observe_global::(|this: &mut State, cx| { let api_url = Self::api_url(cx); - this.api_key_state.handle_url_change( - api_url, - &API_KEY_ENV_VAR, - |this| &mut this.api_key_state, - cx, - ); + this.api_key_state + .handle_url_change(api_url, |this| &mut this.api_key_state, cx); cx.notify(); }) .detach(); State { - api_key_state: ApiKeyState::new(Self::api_url(cx)), + api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), } }); @@ -474,14 +461,14 @@ impl Render for ConfigurationView { .child(Label::new("To use Zed's agent with xAI, you need to add an API key. Follow these steps:")) .child( List::new() - .child(InstructionListItem::new( - "Create one by visiting", - Some("xAI console"), - Some("https://console.x.ai/team/default/api-keys"), - )) - .child(InstructionListItem::text_only( - "Paste your API key below and hit enter to start using the agent", - )), + .child( + ListBulletItem::new("") + .child(Label::new("Create one by visiting")) + .child(ButtonLink::new("xAI console", "https://console.x.ai/team/default/api-keys")) + ) + .child( + ListBulletItem::new("Paste your API key below and hit enter to start using the agent") + ), ) .child(self.api_key_editor.clone()) .child( diff --git a/crates/language_models/src/ui.rs b/crates/language_models/src/ui.rs deleted file mode 100644 index 1d7796ecc2b6c2a78b3ebc02dc9cd29bd8cfa2c6..0000000000000000000000000000000000000000 --- a/crates/language_models/src/ui.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod configured_api_card; -pub mod instruction_list_item; -pub use configured_api_card::ConfiguredApiCard; -pub use instruction_list_item::InstructionListItem; diff --git a/crates/language_models/src/ui/instruction_list_item.rs b/crates/language_models/src/ui/instruction_list_item.rs deleted file mode 100644 index bdb5fbe242ee902dc98a37addfaa0f103ef9ad20..0000000000000000000000000000000000000000 --- a/crates/language_models/src/ui/instruction_list_item.rs +++ /dev/null @@ -1,69 +0,0 @@ -use gpui::{AnyElement, IntoElement, ParentElement, SharedString}; -use ui::{ListItem, prelude::*}; - -/// A reusable list item component for adding LLM provider configuration instructions -pub struct InstructionListItem { - label: SharedString, - button_label: Option, - button_link: Option, -} - -impl InstructionListItem { - pub fn new( - label: impl Into, - button_label: Option>, - button_link: Option>, - ) -> Self { - Self { - label: label.into(), - button_label: button_label.map(|l| l.into()), - button_link: button_link.map(|l| l.into()), - } - } - - pub fn text_only(label: impl Into) -> Self { - Self { - label: label.into(), - button_label: None, - button_link: None, - } - } -} - -impl IntoElement for InstructionListItem { - type Element = AnyElement; - - fn into_element(self) -> Self::Element { - let item_content = if let (Some(button_label), Some(button_link)) = - (self.button_label, self.button_link) - { - let link = button_link; - let unique_id = SharedString::from(format!("{}-button", self.label)); - - h_flex() - .flex_wrap() - .child(Label::new(self.label)) - .child( - Button::new(unique_id, button_label) - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(move |_, _window, cx| cx.open_url(&link)), - ) - .into_any_element() - } else { - Label::new(self.label).into_any_element() - }; - - ListItem::new("list-item") - .selectable(false) - .start_slot( - Icon::new(IconName::Dash) - .size(IconSize::XSmall) - .color(Color::Hidden), - ) - .child(div().w_full().child(item_content)) - .into_any_element() - } -} diff --git a/crates/languages/src/c/injections.scm b/crates/languages/src/c/injections.scm index d7df76b118672e77e3e2a6eacb320aade84c05fa..447897340cc735ed77099b20fd6fc8c52ac19ec8 100644 --- a/crates/languages/src/c/injections.scm +++ b/crates/languages/src/c/injections.scm @@ -1,7 +1,6 @@ ((comment) @injection.content - (#match? @injection.content "^(///|//!|/\\*\\*|/\\*!)(.*)") - (#set! injection.language "doxygen") - (#set! injection.include-children)) + (#set! injection.language "comment") +) (preproc_def value: (preproc_arg) @injection.content diff --git a/crates/languages/src/cpp/brackets.scm b/crates/languages/src/cpp/brackets.scm index 2149bddc6c9a7ec04667d03da75580b676e12a28..9eaebba332861ef716902b3827d4940b71f37221 100644 --- a/crates/languages/src/cpp/brackets.scm +++ b/crates/languages/src/cpp/brackets.scm @@ -1,5 +1,6 @@ ("(" @open ")" @close) ("[" @open "]" @close) ("{" @open "}" @close) +("<" @open ">" @close) (("\"" @open "\"" @close) (#set! rainbow.exclude)) (("'" @open "'" @close) (#set! rainbow.exclude)) diff --git a/crates/languages/src/cpp/injections.scm b/crates/languages/src/cpp/injections.scm index a115a3bffdbe4c522b611f3786ffc95dcecc5cff..160770f3cc1d69f5cb3d1679c8a48726d8d437ed 100644 --- a/crates/languages/src/cpp/injections.scm +++ b/crates/languages/src/cpp/injections.scm @@ -1,7 +1,6 @@ ((comment) @injection.content - (#match? @injection.content "^(///|//!|/\\*\\*|/\\*!)(.*)") - (#set! injection.language "doxygen") - (#set! injection.include-children)) + (#set! injection.language "comment") +) (preproc_def value: (preproc_arg) @injection.content diff --git a/crates/languages/src/python/injections.scm b/crates/languages/src/python/injections.scm index 9117c713b98fdd2896b13e4949a77c6489b9ee36..d8470140e999f3dc649c0a498987cfae7df6bf59 100644 --- a/crates/languages/src/python/injections.scm +++ b/crates/languages/src/python/injections.scm @@ -1,3 +1,34 @@ ((comment) @injection.content (#set! injection.language "comment") ) + +; SQL ----------------------------------------------------------------------------- +( + [ + ; function calls + (call + [ + (attribute attribute: (identifier) @function_name) + (identifier) @function_name + ] + arguments: (argument_list + (comment) @comment + (string + (string_content) @injection.content + ) + )) + + ; string variables + ((comment) @comment + . + (expression_statement + (assignment + right: (string + (string_content) @injection.content + ) + ) + )) + ] + (#match? @comment "^(#|#\\s+)(?i:sql)\\s*$") + (#set! injection.language "sql") +) diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index a7aa1bc49c0132b01d0fe45d94a29af4efac6602..7daf178d37229a5b051461e199c3dbf8d830cf22 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -111,8 +111,7 @@ impl PackageJsonData { "--".to_owned(), "vitest".to_owned(), "run".to_owned(), - "--poolOptions.forks.minForks=0".to_owned(), - "--poolOptions.forks.maxForks=1".to_owned(), + "--no-file-parallelism".to_owned(), VariableName::File.template_value(), ], cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()), @@ -130,8 +129,7 @@ impl PackageJsonData { "--".to_owned(), "vitest".to_owned(), "run".to_owned(), - "--poolOptions.forks.minForks=0".to_owned(), - "--poolOptions.forks.maxForks=1".to_owned(), + "--no-file-parallelism".to_owned(), "--testNamePattern".to_owned(), format!( "\"{}\"", diff --git a/crates/livekit_client/src/livekit_client/playback/source.rs b/crates/livekit_client/src/livekit_client/playback/source.rs index cde4b19fda2e053346ad535e7c75b2abda60431a..a258c585285d8adafb1b0039400e6b6e787a509e 100644 --- a/crates/livekit_client/src/livekit_client/playback/source.rs +++ b/crates/livekit_client/src/livekit_client/playback/source.rs @@ -47,14 +47,17 @@ impl LiveKitStream { ); let (queue_input, queue_output) = rodio::queue::queue(true); // spawn rtc stream - let receiver_task = executor.spawn({ - async move { - while let Some(frame) = stream.next().await { - let samples = frame_to_samplesbuffer(frame); - queue_input.append(samples); + let receiver_task = executor.spawn_with_priority( + gpui::Priority::Realtime(gpui::RealtimePriority::Audio), + { + async move { + while let Some(frame) = stream.next().await { + let samples = frame_to_samplesbuffer(frame); + queue_input.append(samples); + } } - } - }); + }, + ); LiveKitStream { _receiver_task: receiver_task, diff --git a/crates/markdown/examples/markdown_as_child.rs b/crates/markdown/examples/markdown_as_child.rs index 6affa243ae5cc5f4cac1dc7fea0af9b9cc183aa6..775e2a141a849636512264dda2628e28254c8e2b 100644 --- a/crates/markdown/examples/markdown_as_child.rs +++ b/crates/markdown/examples/markdown_as_child.rs @@ -54,11 +54,11 @@ impl Render for HelloWorld { ..Default::default() }, code_block: StyleRefinement { - text: Some(gpui::TextStyleRefinement { + text: gpui::TextStyleRefinement { font_family: Some("Zed Mono".into()), background_color: Some(cx.theme().colors().editor_background), ..Default::default() - }), + }, margin: gpui::EdgesRefinement { top: Some(Length::Definite(rems(4.).into())), left: Some(Length::Definite(rems(4.).into())), diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 317657ea5f520cee15fc49d462b3f8ac5f0072dc..d6ba3babecf3b6b43155780e569bdc4515762d40 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -251,7 +251,7 @@ impl Markdown { self.autoscroll_request = None; self.pending_parse = None; self.should_reparse = false; - self.parsed_markdown = ParsedMarkdown::default(); + // Don't clear parsed_markdown here - keep existing content visible until new parse completes self.parse(cx); } @@ -838,8 +838,7 @@ impl Element for MarkdownElement { heading.style().refine(&self.style.heading); - let text_style = - self.style.heading.text_style().clone().unwrap_or_default(); + let text_style = self.style.heading.text_style().clone(); builder.push_text_style(text_style); builder.push_div(heading, range, markdown_end); @@ -933,10 +932,7 @@ impl Element for MarkdownElement { } }); - if let Some(code_block_text_style) = &self.style.code_block.text - { - builder.push_text_style(code_block_text_style.to_owned()); - } + builder.push_text_style(self.style.code_block.text.to_owned()); builder.push_code_block(language); builder.push_div(code_block, range, markdown_end); } @@ -1091,9 +1087,7 @@ impl Element for MarkdownElement { builder.pop_div(); builder.pop_code_block(); - if self.style.code_block.text.is_some() { - builder.pop_text_style(); - } + builder.pop_text_style(); if let CodeBlockRenderer::Default { copy_button: true, .. @@ -1346,7 +1340,7 @@ fn apply_heading_style( }; if let Some(style) = style_opt { - heading.style().text = Some(style.clone()); + heading.style().text = style.clone(); } } diff --git a/crates/multi_buffer/src/path_key.rs b/crates/multi_buffer/src/path_key.rs index 119194d088c946941b13ffab3f6f2b3ea126cd09..10d4088fd4bc28449c8a4ee74095ad31a45fbcf3 100644 --- a/crates/multi_buffer/src/path_key.rs +++ b/crates/multi_buffer/src/path_key.rs @@ -43,8 +43,8 @@ impl PathKey { } impl MultiBuffer { - pub fn paths(&self) -> impl Iterator + '_ { - self.excerpts_by_path.keys().cloned() + pub fn paths(&self) -> impl Iterator + '_ { + self.excerpts_by_path.keys() } pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context) { @@ -58,15 +58,18 @@ impl MultiBuffer { } } - pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option { + pub fn buffer_for_path(&self, path: &PathKey, cx: &App) -> Option> { let excerpt_id = self.excerpts_by_path.get(path)?.first()?; let snapshot = self.read(cx); let excerpt = snapshot.excerpt(*excerpt_id)?; - Some(Anchor::in_buffer(excerpt.id, excerpt.range.context.start)) + self.buffer(excerpt.buffer_id) } - pub fn excerpt_paths(&self) -> impl Iterator { - self.excerpts_by_path.keys() + pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option { + let excerpt_id = self.excerpts_by_path.get(path)?.first()?; + let snapshot = self.read(cx); + let excerpt = snapshot.excerpt(*excerpt_id)?; + Some(Anchor::in_buffer(excerpt.id, excerpt.range.context.start)) } /// Sets excerpts, returns `true` if at least one new excerpt was added. diff --git a/crates/notifications/src/status_toast.rs b/crates/notifications/src/status_toast.rs index 7affa93f5a496bd0e436c74e5ff32f8aa871d026..40c5bdc8f85d0b9a46474760954247e8bba76ca9 100644 --- a/crates/notifications/src/status_toast.rs +++ b/crates/notifications/src/status_toast.rs @@ -137,7 +137,8 @@ impl Render for StatusToast { let handle = self.this_handle.clone(); this.child( IconButton::new("dismiss", IconName::Close) - .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .tooltip(Tooltip::text("Dismiss")) .on_click(move |_click_event, _window, cx| { diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index a787ad5b032ffcabc38790668fd4e0901ac1bebc..8dbf7b681d9be45bda0fd9803cbb8e2cd434e921 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -75,6 +75,16 @@ actions!( OpenSelectedEntry, /// Reveals the selected item in the system file manager. RevealInFileManager, + /// Scroll half a page upwards + ScrollUp, + /// Scroll half a page downwards + ScrollDown, + /// Scroll until the cursor displays at the center + ScrollCursorCenter, + /// Scroll until the cursor displays at the top + ScrollCursorTop, + /// Scroll until the cursor displays at the bottom + ScrollCursorBottom, /// Selects the parent of the current entry. SelectParent, /// Toggles the pin status of the active editor. @@ -100,6 +110,7 @@ pub struct OutlinePanel { active: bool, pinned: bool, scroll_handle: UniformListScrollHandle, + rendered_entries_len: usize, context_menu: Option<(Entity, Point, Subscription)>, focus_handle: FocusHandle, pending_serialization: Task>, @@ -839,6 +850,7 @@ impl OutlinePanel { fs: workspace.app_state().fs.clone(), max_width_item_index: None, scroll_handle, + rendered_entries_len: 0, focus_handle, filter_editor, fs_entries: Vec::new(), @@ -1149,6 +1161,70 @@ impl OutlinePanel { } } + fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context) { + for _ in 0..self.rendered_entries_len / 2 { + window.dispatch_action(SelectPrevious.boxed_clone(), cx); + } + } + + fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context) { + for _ in 0..self.rendered_entries_len / 2 { + window.dispatch_action(SelectNext.boxed_clone(), cx); + } + } + + fn scroll_cursor_center( + &mut self, + _: &ScrollCursorCenter, + _: &mut Window, + cx: &mut Context, + ) { + if let Some(selected_entry) = self.selected_entry() { + let index = self + .cached_entries + .iter() + .position(|cached_entry| &cached_entry.entry == selected_entry); + if let Some(index) = index { + self.scroll_handle + .scroll_to_item_strict(index, ScrollStrategy::Center); + cx.notify(); + } + } + } + + fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, _: &mut Window, cx: &mut Context) { + if let Some(selected_entry) = self.selected_entry() { + let index = self + .cached_entries + .iter() + .position(|cached_entry| &cached_entry.entry == selected_entry); + if let Some(index) = index { + self.scroll_handle + .scroll_to_item_strict(index, ScrollStrategy::Top); + cx.notify(); + } + } + } + + fn scroll_cursor_bottom( + &mut self, + _: &ScrollCursorBottom, + _: &mut Window, + cx: &mut Context, + ) { + if let Some(selected_entry) = self.selected_entry() { + let index = self + .cached_entries + .iter() + .position(|cached_entry| &cached_entry.entry == selected_entry); + if let Some(index) = index { + self.scroll_handle + .scroll_to_item_strict(index, ScrollStrategy::Bottom); + cx.notify(); + } + } + } + fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| { self.cached_entries @@ -2610,7 +2686,7 @@ impl OutlinePanel { }) .when( is_active && self.focus_handle.contains_focused(window, cx), - |div| div.border_color(Color::Selected.color(cx)), + |div| div.border_color(cx.theme().colors().panel_focused_border), ) } @@ -4578,6 +4654,7 @@ impl OutlinePanel { "entries", items_len, cx.processor(move |outline_panel, range: Range, window, cx| { + outline_panel.rendered_entries_len = range.end - range.start; let entries = outline_panel.cached_entries.get(range); entries .map(|entries| entries.to_vec()) @@ -4970,7 +5047,12 @@ impl Render for OutlinePanel { .key_context(self.dispatch_context(window, cx)) .on_action(cx.listener(Self::open_selected_entry)) .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::scroll_up)) + .on_action(cx.listener(Self::scroll_down)) .on_action(cx.listener(Self::select_next)) + .on_action(cx.listener(Self::scroll_cursor_center)) + .on_action(cx.listener(Self::scroll_cursor_top)) + .on_action(cx.listener(Self::scroll_cursor_bottom)) .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_first)) .on_action(cx.listener(Self::select_last)) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ec44a60d71e4b0c1f10ad698f727357e60aa3b85..7e7c1ecb67d2f463cb5b728cbb2a7f1ea2b072e0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -966,6 +966,14 @@ impl DirectoryLister { } } } + + pub fn path_style(&self, cx: &App) -> PathStyle { + match self { + Self::Local(project, ..) | Self::Project(project, ..) => { + project.read(cx).path_style(cx) + } + } + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index d6a172218a8eb3d4538363e6202a7e721d2b7bc1..847e45742db17fe194d002c26a67380390b68f06 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -286,7 +286,7 @@ impl PromptBuilder { Ok(()) } - pub fn generate_inline_transformation_prompt_v2( + pub fn generate_inline_transformation_prompt_tools( &self, language_name: Option<&LanguageName>, buffer: BufferSnapshot, diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 32a4ef1a81a06a8b5968f7941edb4ab8ea0a5111..1df3abbeaee41532abcf12f5939db050429c73da 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -217,14 +217,13 @@ impl ProjectPicker { connection: RemoteConnectionOptions, project: Entity, home_dir: RemotePathBuf, - path_style: PathStyle, workspace: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Entity { let (tx, rx) = oneshot::channel(); let lister = project::DirectoryLister::Project(project.clone()); - let delegate = file_finder::OpenPathDelegate::new(tx, lister, false, path_style); + let delegate = file_finder::OpenPathDelegate::new(tx, lister, false, cx); let picker = cx.new(|cx| { let picker = Picker::uniform_list(delegate, window, cx) @@ -719,7 +718,6 @@ impl RemoteServerProjects { connection_options: remote::RemoteConnectionOptions, project: Entity, home_dir: RemotePathBuf, - path_style: PathStyle, window: &mut Window, cx: &mut Context, workspace: WeakEntity, @@ -732,7 +730,6 @@ impl RemoteServerProjects { connection_options, project, home_dir, - path_style, workspace, window, cx, @@ -1030,7 +1027,6 @@ impl RemoteServerProjects { connection_options, project, home_dir, - path_style, window, cx, weak, diff --git a/crates/refineable/derive_refineable/src/derive_refineable.rs b/crates/refineable/derive_refineable/src/derive_refineable.rs index ddf3855a4dc5ae6917309ced57391bd244f1b465..c7c8a91ad9b05d054a94c8ca7f55a54c75150d81 100644 --- a/crates/refineable/derive_refineable/src/derive_refineable.rs +++ b/crates/refineable/derive_refineable/src/derive_refineable.rs @@ -528,7 +528,12 @@ fn get_wrapper_type(field: &Field, ty: &Type) -> syn::Type { } else { panic!("Expected struct type for a refineable field"); }; - let refinement_struct_name = format_ident!("{}Refinement", struct_name); + + let refinement_struct_name = if struct_name.to_string().ends_with("Refinement") { + format_ident!("{}", struct_name) + } else { + format_ident!("{}Refinement", struct_name) + }; let generics = if let Type::Path(tp) = ty { &tp.path.segments.last().unwrap().arguments } else { diff --git a/crates/refineable/src/refineable.rs b/crates/refineable/src/refineable.rs index d2a7c3d3f2148ae174be10121dc23b4dbfc5a650..b2305d4b5a7c1e5e45287394a49a258d98767c66 100644 --- a/crates/refineable/src/refineable.rs +++ b/crates/refineable/src/refineable.rs @@ -13,7 +13,7 @@ pub use derive_refineable::Refineable; /// wrapped appropriately: /// /// - **Refineable fields** (marked with `#[refineable]`): Become the corresponding refinement type -/// (e.g., `Bar` becomes `BarRefinement`) +/// (e.g., `Bar` becomes `BarRefinement`, or `BarRefinement` remains `BarRefinement`) /// - **Optional fields** (`Option`): Remain as `Option` /// - **Regular fields**: Become `Option` /// diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index a2a8e8d58df2d5ddc3336e8e56dd8446f4dcf118..c1916768c1f8a0980fb4d5aa1b718483b08c6087 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -47,22 +47,59 @@ impl Chunk { #[inline(always)] pub fn new(text: &str) -> Self { - let mut this = Chunk::default(); - this.push_str(text); - this + let text = ArrayString::from(text).unwrap(); + + const CHUNK_SIZE: usize = 8; + + let mut chars_bytes = [0; MAX_BASE / CHUNK_SIZE]; + let mut newlines_bytes = [0; MAX_BASE / CHUNK_SIZE]; + let mut tabs_bytes = [0; MAX_BASE / CHUNK_SIZE]; + let mut chars_utf16_bytes = [0; MAX_BASE / CHUNK_SIZE]; + + let mut chunk_ix = 0; + + let mut bytes = text.as_bytes(); + while !bytes.is_empty() { + let (chunk, rest) = bytes.split_at(bytes.len().min(CHUNK_SIZE)); + bytes = rest; + + let mut chars = 0; + let mut newlines = 0; + let mut tabs = 0; + let mut chars_utf16 = 0; + + for (ix, &b) in chunk.iter().enumerate() { + chars |= (util::is_utf8_char_boundary(b) as u8) << ix; + newlines |= ((b == b'\n') as u8) << ix; + tabs |= ((b == b'\t') as u8) << ix; + // b >= 240 when we are at the first byte of the 4 byte encoded + // utf-8 code point (U+010000 or greater) it means that it would + // be encoded as two 16-bit code units in utf-16 + chars_utf16 |= ((b >= 240) as u8) << ix; + } + + chars_bytes[chunk_ix] = chars; + newlines_bytes[chunk_ix] = newlines; + tabs_bytes[chunk_ix] = tabs; + chars_utf16_bytes[chunk_ix] = chars_utf16; + + chunk_ix += 1; + } + + let chars = Bitmap::from_le_bytes(chars_bytes); + + Chunk { + text, + chars, + chars_utf16: (Bitmap::from_le_bytes(chars_utf16_bytes) << 1) | chars, + newlines: Bitmap::from_le_bytes(newlines_bytes), + tabs: Bitmap::from_le_bytes(tabs_bytes), + } } #[inline(always)] pub fn push_str(&mut self, text: &str) { - for (char_ix, c) in text.char_indices() { - let ix = self.text.len() + char_ix; - self.chars |= 1 << ix; - self.chars_utf16 |= 1 << ix; - self.chars_utf16 |= (c.len_utf16() as Bitmap) << ix; - self.newlines |= ((c == '\n') as Bitmap) << ix; - self.tabs |= ((c == '\t') as Bitmap) << ix; - } - self.text.push_str(text); + self.append(Chunk::new(text).as_slice()); } #[inline(always)] diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 50f9ba044d90072aa9c6fc2fc4abfd6d0e6b98cb..fba7b96aca83fa05c0d6f3e7992ad7443ec7958a 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -227,7 +227,7 @@ impl Rope { #[cfg(all(test, not(rust_analyzer)))] const PARALLEL_THRESHOLD: usize = 4; #[cfg(not(all(test, not(rust_analyzer))))] - const PARALLEL_THRESHOLD: usize = 4 * (2 * sum_tree::TREE_BASE); + const PARALLEL_THRESHOLD: usize = 84 * (2 * sum_tree::TREE_BASE); if new_chunks.len() >= PARALLEL_THRESHOLD { self.chunks diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index a9c26ac9bad0f524acdb47d6f09c2bd67cb8dfc6..686d385aa07accac168062fa598790b36e80199f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -729,12 +729,14 @@ impl BufferSearchBar { self.search_suggested(window, cx); self.smartcase(window, cx); self.sync_select_next_case_sensitivity(cx); - self.replace_enabled = deploy.replace_enabled; - self.selection_search_enabled = if deploy.selection_search_enabled { - Some(FilteredSearchRange::Default) - } else { - None - }; + self.replace_enabled |= deploy.replace_enabled; + self.selection_search_enabled = + self.selection_search_enabled + .or(if deploy.selection_search_enabled { + Some(FilteredSearchRange::Default) + } else { + None + }); if deploy.focus { let mut handle = self.query_editor.focus_handle(cx); let mut select_query = true; diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index a9ca77a5b8bd30b8492cf8f8dfa2b17fdcdb6a5b..278f2e86b7b13fd5a82777054c12ff2e1b6239bb 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1147,7 +1147,7 @@ impl ProjectSearchView { }; search.update(cx, |search, cx| { - search.replace_enabled = action.replace_enabled; + search.replace_enabled |= action.replace_enabled; if let Some(query) = query { search.set_query(&query, window, cx); } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 743e22b04d9cf87a0d09a73aef879c781a50cca2..ba349b865bf2ac4dfd9d19b22c5693307ebae20a 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -286,6 +286,10 @@ pub struct TitleBarSettingsContent { /// /// Default: true pub show_sign_in: Option, + /// Whether to show the user menu button in the title bar. + /// + /// Default: true + pub show_user_menu: Option, /// Whether to show the menus in the title bar. /// /// Default: false diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs index e875c95f1c89b68cbb226f6c32734d82846d3b5d..5bcf0f49a445ed82d98871c4b8fd3cc66c37d9b1 100644 --- a/crates/settings/src/settings_content/agent.rs +++ b/crates/settings/src/settings_content/agent.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use settings_macros::{MergeFrom, with_fallible_options}; use std::{borrow::Cow, path::PathBuf, sync::Arc}; -use crate::DockPosition; +use crate::{DockPosition, DockSide}; #[with_fallible_options] #[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)] @@ -22,6 +22,10 @@ pub struct AgentSettingsContent { /// /// Default: right pub dock: Option, + /// Where to dock the utility pane (the thread view pane). + /// + /// Default: left + pub agents_panel_dock: Option, /// Default width in pixels when the agent panel is docked to the left or right. /// /// Default: 640 @@ -36,7 +40,13 @@ pub struct AgentSettingsContent { pub default_model: Option, /// Model to use for the inline assistant. Defaults to default_model when not specified. pub inline_assistant_model: Option, - /// Model to use for generating git commit messages. Defaults to default_model when not specified. + /// Model to use for the inline assistant when streaming tools are enabled. + /// + /// Default: true + pub inline_assistant_use_streaming_tools: Option, + /// Model to use for generating git commit messages. + /// + /// Default: true pub commit_message_model: Option, /// Model to use for generating thread summaries. Defaults to default_model when not specified. pub thread_summary_model: Option, @@ -129,6 +139,9 @@ impl AgentSettingsContent { model, }); } + pub fn set_inline_assistant_use_streaming_tools(&mut self, use_tools: bool) { + self.inline_assistant_use_streaming_tools = Some(use_tools); + } pub fn set_commit_message_model(&mut self, provider: String, model: String) { self.commit_message_model = Some(LanguageModelSelection { diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index 25ff60e9f46cf797b815227222a3d27a6353c396..f9c85f18f380a7ad82b0d8bc202fe3763ba3a832 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -186,22 +186,20 @@ pub struct CopilotSettingsContent { pub enterprise_uri: Option, } +#[with_fallible_options] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] pub struct CodestralSettingsContent { /// Model to use for completions. /// /// Default: "codestral-latest" - #[serde(default)] pub model: Option, /// Maximum tokens to generate. /// /// Default: 150 - #[serde(default)] pub max_tokens: Option, /// Api URL to use for completions. /// /// Default: "https://codestral.mistral.ai" - #[serde(default)] pub api_url: Option, } diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index b5a259a3b9f901f4885b1cde8ad1e933efb263c0..256ec2de557e903405d1c3431ef44e98d757d3c6 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -18,6 +18,9 @@ test-support = [] [dependencies] anyhow.workspace = true bm25 = "2.3.2" +copilot.workspace = true +edit_prediction.workspace = true +language_models.workspace = true editor.workspace = true feature_flags.workspace = true fs.workspace = true @@ -38,8 +41,8 @@ strum.workspace = true telemetry.workspace = true theme.workspace = true title_bar.workspace = true -ui.workspace = true ui_input.workspace = true +ui.workspace = true util.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/settings_ui/src/components.rs b/crates/settings_ui/src/components.rs index b073372ac9b625036252e0a1722a960c8f6b3c45..f9754b0c749a77423930ef881e5b60ad3535b83d 100644 --- a/crates/settings_ui/src/components.rs +++ b/crates/settings_ui/src/components.rs @@ -2,10 +2,12 @@ mod dropdown; mod font_picker; mod icon_theme_picker; mod input_field; +mod section_items; mod theme_picker; pub use dropdown::*; pub use font_picker::font_picker; pub use icon_theme_picker::icon_theme_picker; pub use input_field::*; +pub use section_items::*; pub use theme_picker::theme_picker; diff --git a/crates/settings_ui/src/components/input_field.rs b/crates/settings_ui/src/components/input_field.rs index 57917c321127baf2e96e3862106461331afaf86f..575da7f7ae13f8a304b23d57dd41607e7b7c512a 100644 --- a/crates/settings_ui/src/components/input_field.rs +++ b/crates/settings_ui/src/components/input_field.rs @@ -13,6 +13,7 @@ pub struct SettingsInputField { tab_index: Option, } +// TODO: Update the `ui_input::InputField` to use `window.use_state` and `RenceOnce` and remove this component impl SettingsInputField { pub fn new() -> Self { Self { diff --git a/crates/settings_ui/src/components/section_items.rs b/crates/settings_ui/src/components/section_items.rs new file mode 100644 index 0000000000000000000000000000000000000000..69559d24f447f3d218b296600ed1ecdd9bf1dc30 --- /dev/null +++ b/crates/settings_ui/src/components/section_items.rs @@ -0,0 +1,56 @@ +use gpui::{IntoElement, ParentElement, Styled}; +use ui::{Divider, DividerColor, prelude::*}; + +#[derive(IntoElement)] +pub struct SettingsSectionHeader { + icon: Option, + label: SharedString, + no_padding: bool, +} + +impl SettingsSectionHeader { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + icon: None, + no_padding: false, + } + } + + pub fn icon(mut self, icon: IconName) -> Self { + self.icon = Some(icon); + self + } + + pub fn no_padding(mut self, no_padding: bool) -> Self { + self.no_padding = no_padding; + self + } +} + +impl RenderOnce for SettingsSectionHeader { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let label = Label::new(self.label) + .size(LabelSize::Small) + .color(Color::Muted) + .buffer_font(cx); + + v_flex() + .w_full() + .when(!self.no_padding, |this| this.px_8()) + .gap_1p5() + .map(|this| { + if self.icon.is_some() { + this.child( + h_flex() + .gap_1p5() + .child(Icon::new(self.icon.unwrap()).color(Color::Muted)) + .child(label), + ) + } else { + this.child(label) + } + }) + .child(Divider::horizontal().color(DividerColor::BorderFaded)) + } +} diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 8652ccf68b48e8e858b96e4fe69edecd8ae29d25..79fc1cc11158399265a184a289fd8d7a71ce8d69 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -2330,8 +2330,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec { // Note that `crates/json_schema_store` solves the same problem, there is probably a way to unify the two items.push(SettingsPageItem::SectionHeader(LANGUAGES_SECTION_HEADER)); items.extend(all_language_names(cx).into_iter().map(|language_name| { + let link = format!("languages.{language_name}"); SettingsPageItem::SubPageLink(SubPageLink { title: language_name, + description: None, + json_path: Some(link.leak()), + in_json: true, files: USER | PROJECT, render: Arc::new(|this, window, cx| { this.render_sub_page_items( @@ -2909,40 +2913,58 @@ pub(crate) fn settings_data(cx: &App) -> Vec { files: USER, }), SettingsPageItem::SettingItem(SettingItem { - title: "Show User Picture", - description: "Show user picture in the titlebar.", + title: "Show Sign In", + description: "Show the sign in button in the titlebar.", field: Box::new(SettingField { - json_path: Some("title_bar.show_user_picture"), + json_path: Some("title_bar.show_sign_in"), pick: |settings_content| { + settings_content.title_bar.as_ref()?.show_sign_in.as_ref() + }, + write: |settings_content, value| { settings_content .title_bar - .as_ref()? - .show_user_picture - .as_ref() + .get_or_insert_default() + .show_sign_in = value; + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Show User Menu", + description: "Show the user menu button in the titlebar.", + field: Box::new(SettingField { + json_path: Some("title_bar.show_user_menu"), + pick: |settings_content| { + settings_content.title_bar.as_ref()?.show_user_menu.as_ref() }, write: |settings_content, value| { settings_content .title_bar .get_or_insert_default() - .show_user_picture = value; + .show_user_menu = value; }, }), metadata: None, files: USER, }), SettingsPageItem::SettingItem(SettingItem { - title: "Show Sign In", - description: "Show the sign in button in the titlebar.", + title: "Show User Picture", + description: "Show user picture in the titlebar.", field: Box::new(SettingField { - json_path: Some("title_bar.show_sign_in"), + json_path: Some("title_bar.show_user_picture"), pick: |settings_content| { - settings_content.title_bar.as_ref()?.show_sign_in.as_ref() + settings_content + .title_bar + .as_ref()? + .show_user_picture + .as_ref() }, write: |settings_content, value| { settings_content .title_bar .get_or_insert_default() - .show_sign_in = value; + .show_user_picture = value; }, }), metadata: None, @@ -6013,7 +6035,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { files: USER, }), SettingsPageItem::SettingItem(SettingItem { - title: "In Text Threads", + title: "Display In Text Threads", description: "Whether edit predictions are enabled when editing text threads in the agent panel.", field: Box::new(SettingField { json_path: Some("edit_prediction.in_text_threads"), @@ -6027,42 +6049,6 @@ pub(crate) fn settings_data(cx: &App) -> Vec { metadata: None, files: USER, }), - SettingsPageItem::SettingItem(SettingItem { - title: "Copilot Provider", - description: "Use GitHub Copilot as your edit prediction provider.", - field: Box::new( - SettingField { - json_path: Some("edit_prediction.copilot_provider"), - pick: |settings_content| { - settings_content.project.all_languages.edit_predictions.as_ref()?.copilot.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.edit_predictions.get_or_insert_default().copilot = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Codestral Provider", - description: "Use Mistral's Codestral as your edit prediction provider.", - field: Box::new( - SettingField { - json_path: Some("edit_prediction.codestral_provider"), - pick: |settings_content| { - settings_content.project.all_languages.edit_predictions.as_ref()?.codestral.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.edit_predictions.get_or_insert_default().codestral = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), ] ); items @@ -7485,9 +7471,23 @@ fn non_editor_language_settings_data() -> Vec { fn edit_prediction_language_settings_section() -> Vec { vec![ SettingsPageItem::SectionHeader("Edit Predictions"), + SettingsPageItem::SubPageLink(SubPageLink { + title: "Configure Providers".into(), + json_path: Some("edit_predictions.providers"), + description: Some("Set up different edit prediction providers in complement to Zed's built-in Zeta model.".into()), + in_json: false, + files: USER, + render: Arc::new(|_, window, cx| { + let settings_window = cx.entity(); + let page = window.use_state(cx, |_, _| { + crate::pages::EditPredictionSetupPage::new(settings_window) + }); + page.into_any_element() + }), + }), SettingsPageItem::SettingItem(SettingItem { title: "Show Edit Predictions", - description: "Controls whether edit predictions are shown immediately or manually by triggering `editor::showeditprediction` (false).", + description: "Controls whether edit predictions are shown immediately or manually.", field: Box::new(SettingField { json_path: Some("languages.$(language).show_edit_predictions"), pick: |settings_content| { @@ -7505,7 +7505,7 @@ fn edit_prediction_language_settings_section() -> Vec { files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { - title: "Edit Predictions Disabled In", + title: "Disable in Language Scopes", description: "Controls whether edit predictions are shown in the given language scopes.", field: Box::new( SettingField { diff --git a/crates/settings_ui/src/pages.rs b/crates/settings_ui/src/pages.rs new file mode 100644 index 0000000000000000000000000000000000000000..2b2c4818c1322216707f38bf93cefffeb14add03 --- /dev/null +++ b/crates/settings_ui/src/pages.rs @@ -0,0 +1,2 @@ +mod edit_prediction_provider_setup; +pub use edit_prediction_provider_setup::EditPredictionSetupPage; diff --git a/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs b/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs new file mode 100644 index 0000000000000000000000000000000000000000..fb8f967613fa195080f62c5ab2ce76a43f3d1e22 --- /dev/null +++ b/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs @@ -0,0 +1,365 @@ +use edit_prediction::{ + ApiKeyState, Zeta2FeatureFlag, + mercury::{MERCURY_CREDENTIALS_URL, mercury_api_token}, + sweep_ai::{SWEEP_CREDENTIALS_URL, sweep_api_token}, +}; +use feature_flags::FeatureFlagAppExt as _; +use gpui::{Entity, ScrollHandle, prelude::*}; +use language_models::provider::mistral::{CODESTRAL_API_URL, codestral_api_key}; +use ui::{ButtonLink, ConfiguredApiCard, WithScrollbar, prelude::*}; + +use crate::{ + SettingField, SettingItem, SettingsFieldMetadata, SettingsPageItem, SettingsWindow, USER, + components::{SettingsInputField, SettingsSectionHeader}, +}; + +pub struct EditPredictionSetupPage { + settings_window: Entity, + scroll_handle: ScrollHandle, +} + +impl EditPredictionSetupPage { + pub fn new(settings_window: Entity) -> Self { + Self { + settings_window, + scroll_handle: ScrollHandle::new(), + } + } +} + +impl Render for EditPredictionSetupPage { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let settings_window = self.settings_window.clone(); + + let providers = [ + Some(render_github_copilot_provider(window, cx).into_any_element()), + cx.has_flag::().then(|| { + render_api_key_provider( + IconName::Inception, + "Mercury", + "https://platform.inceptionlabs.ai/dashboard/api-keys".into(), + mercury_api_token(cx), + |_cx| MERCURY_CREDENTIALS_URL, + None, + window, + cx, + ) + .into_any_element() + }), + cx.has_flag::().then(|| { + render_api_key_provider( + IconName::SweepAi, + "Sweep", + "https://app.sweep.dev/".into(), + sweep_api_token(cx), + |_cx| SWEEP_CREDENTIALS_URL, + None, + window, + cx, + ) + .into_any_element() + }), + Some( + render_api_key_provider( + IconName::AiMistral, + "Codestral", + "https://console.mistral.ai/codestral".into(), + codestral_api_key(cx), + |cx| language_models::MistralLanguageModelProvider::api_url(cx), + Some(settings_window.update(cx, |settings_window, cx| { + let codestral_settings = codestral_settings(); + settings_window + .render_sub_page_items_section( + codestral_settings.iter().enumerate(), + None, + window, + cx, + ) + .into_any_element() + })), + window, + cx, + ) + .into_any_element(), + ), + ]; + + div() + .size_full() + .vertical_scrollbar_for(&self.scroll_handle, window, cx) + .child( + v_flex() + .id("ep-setup-page") + .min_w_0() + .size_full() + .px_8() + .pb_16() + .overflow_y_scroll() + .track_scroll(&self.scroll_handle) + .children(providers.into_iter().flatten()), + ) + } +} + +fn render_api_key_provider( + icon: IconName, + title: &'static str, + link: SharedString, + api_key_state: Entity, + current_url: fn(&mut App) -> SharedString, + additional_fields: Option, + window: &mut Window, + cx: &mut Context, +) -> impl IntoElement { + let weak_page = cx.weak_entity(); + _ = window.use_keyed_state(title, cx, |_, cx| { + let task = api_key_state.update(cx, |key_state, cx| { + key_state.load_if_needed(current_url(cx), |state| state, cx) + }); + cx.spawn(async move |_, cx| { + task.await.ok(); + weak_page + .update(cx, |_, cx| { + cx.notify(); + }) + .ok(); + }) + }); + + let (has_key, env_var_name, is_from_env_var) = api_key_state.read_with(cx, |state, _| { + ( + state.has_key(), + Some(state.env_var_name().clone()), + state.is_from_env_var(), + ) + }); + + let write_key = move |api_key: Option, cx: &mut App| { + api_key_state + .update(cx, |key_state, cx| { + let url = current_url(cx); + key_state.store(url, api_key, |key_state| key_state, cx) + }) + .detach_and_log_err(cx); + }; + + let base_container = v_flex().id(title).min_w_0().pt_8().gap_1p5(); + let header = SettingsSectionHeader::new(title) + .icon(icon) + .no_padding(true); + let button_link_label = format!("{} dashboard", title); + let description = h_flex() + .min_w_0() + .gap_0p5() + .child( + Label::new("Visit the") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + ButtonLink::new(button_link_label, link) + .no_icon(true) + .label_size(LabelSize::Small) + .label_color(Color::Muted), + ) + .child( + Label::new("to generate an API key.") + .size(LabelSize::Small) + .color(Color::Muted), + ); + let configured_card_label = if is_from_env_var { + "API Key Set in Environment Variable" + } else { + "API Key Configured" + }; + + let container = if has_key { + base_container.child(header).child( + ConfiguredApiCard::new(configured_card_label) + .button_label("Reset Key") + .button_tab_index(0) + .disabled(is_from_env_var) + .when_some(env_var_name, |this, env_var_name| { + this.when(is_from_env_var, |this| { + this.tooltip_label(format!( + "To reset your API key, unset the {} environment variable.", + env_var_name + )) + }) + }) + .on_click(move |_, _, cx| { + write_key(None, cx); + }), + ) + } else { + base_container.child(header).child( + h_flex() + .pt_2p5() + .w_full() + .justify_between() + .child( + v_flex() + .w_full() + .max_w_1_2() + .child(Label::new("API Key")) + .child(description) + .when_some(env_var_name, |this, env_var_name| { + this.child({ + let label = format!( + "Or set the {} env var and restart Zed.", + env_var_name.as_ref() + ); + Label::new(label).size(LabelSize::Small).color(Color::Muted) + }) + }), + ) + .child( + SettingsInputField::new() + .tab_index(0) + .with_placeholder("xxxxxxxxxxxxxxxxxxxx") + .on_confirm(move |api_key, cx| { + write_key(api_key.filter(|key| !key.is_empty()), cx); + }), + ), + ) + }; + + container.when_some(additional_fields, |this, additional_fields| { + this.child( + div() + .map(|this| if has_key { this.mt_1() } else { this.mt_4() }) + .px_neg_8() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child(additional_fields), + ) + }) +} + +fn codestral_settings() -> Box<[SettingsPageItem]> { + Box::new([ + SettingsPageItem::SettingItem(SettingItem { + title: "API URL", + description: "The API URL to use for Codestral.", + field: Box::new(SettingField { + pick: |settings| { + settings + .project + .all_languages + .edit_predictions + .as_ref()? + .codestral + .as_ref()? + .api_url + .as_ref() + }, + write: |settings, value| { + settings + .project + .all_languages + .edit_predictions + .get_or_insert_default() + .codestral + .get_or_insert_default() + .api_url = value; + }, + json_path: Some("edit_predictions.codestral.api_url"), + }), + metadata: Some(Box::new(SettingsFieldMetadata { + placeholder: Some(CODESTRAL_API_URL), + ..Default::default() + })), + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Max Tokens", + description: "The maximum number of tokens to generate.", + field: Box::new(SettingField { + pick: |settings| { + settings + .project + .all_languages + .edit_predictions + .as_ref()? + .codestral + .as_ref()? + .max_tokens + .as_ref() + }, + write: |settings, value| { + settings + .project + .all_languages + .edit_predictions + .get_or_insert_default() + .codestral + .get_or_insert_default() + .max_tokens = value; + }, + json_path: Some("edit_predictions.codestral.max_tokens"), + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Model", + description: "The Codestral model id to use.", + field: Box::new(SettingField { + pick: |settings| { + settings + .project + .all_languages + .edit_predictions + .as_ref()? + .codestral + .as_ref()? + .model + .as_ref() + }, + write: |settings, value| { + settings + .project + .all_languages + .edit_predictions + .get_or_insert_default() + .codestral + .get_or_insert_default() + .model = value; + }, + json_path: Some("edit_predictions.codestral.model"), + }), + metadata: Some(Box::new(SettingsFieldMetadata { + placeholder: Some("codestral-latest"), + ..Default::default() + })), + files: USER, + }), + ]) +} + +pub(crate) fn render_github_copilot_provider( + window: &mut Window, + cx: &mut App, +) -> impl IntoElement { + let configuration_view = window.use_state(cx, |_, cx| { + copilot::ConfigurationView::new( + |cx| { + copilot::Copilot::global(cx) + .is_some_and(|copilot| copilot.read(cx).is_authenticated()) + }, + copilot::ConfigurationMode::EditPrediction, + cx, + ) + }); + + v_flex() + .id("github-copilot") + .min_w_0() + .gap_1p5() + .child( + SettingsSectionHeader::new("GitHub Copilot") + .icon(IconName::Copilot) + .no_padding(true), + ) + .child(configuration_view) +} diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 4464d3bdd951d4b7bf2511cfd718b0f297b8fc78..bfc60cc1ea21525effa5347431d90ee219064d24 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1,9 +1,9 @@ mod components; mod page_data; +mod pages; use anyhow::Result; use editor::{Editor, EditorEvent}; -use feature_flags::FeatureFlag; use fuzzy::StringMatchCandidate; use gpui::{ Action, App, ClipboardItem, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle, @@ -28,9 +28,8 @@ use std::{ }; use title_bar::platform_title_bar::PlatformTitleBar; use ui::{ - Banner, ContextMenu, Divider, DividerColor, DropdownMenu, DropdownStyle, IconButtonShape, - KeyBinding, KeybindingHint, PopoverMenu, Switch, Tooltip, TreeViewItem, WithScrollbar, - prelude::*, + Banner, ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, KeyBinding, + KeybindingHint, PopoverMenu, Switch, Tooltip, TreeViewItem, WithScrollbar, prelude::*, }; use ui_input::{NumberField, NumberFieldType}; use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath}; @@ -38,7 +37,8 @@ use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decor use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt}; use crate::components::{ - EnumVariantDropdown, SettingsInputField, font_picker, icon_theme_picker, theme_picker, + EnumVariantDropdown, SettingsInputField, SettingsSectionHeader, font_picker, icon_theme_picker, + theme_picker, }; const NAVBAR_CONTAINER_TAB_INDEX: isize = 0; @@ -369,12 +369,6 @@ struct SettingsFieldMetadata { should_do_titlecase: Option, } -pub struct SettingsUiFeatureFlag; - -impl FeatureFlag for SettingsUiFeatureFlag { - const NAME: &'static str = "settings-ui"; -} - pub fn init(cx: &mut App) { init_renderers(cx); @@ -613,7 +607,10 @@ pub fn open_settings_editor( app_id: Some(app_id.to_owned()), window_decorations: Some(window_decorations), window_min_size: Some(gpui::Size { - width: px(360.0), + // Don't make the settings window thinner than this, + // otherwise, it gets unusable. Users with smaller res monitors + // can customize the height, but not the width. + width: px(900.0), height: px(240.0), }), window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)), @@ -834,18 +831,9 @@ impl SettingsPageItem { }; match self { - SettingsPageItem::SectionHeader(header) => v_flex() - .w_full() - .px_8() - .gap_1p5() - .child( - Label::new(SharedString::new_static(header)) - .size(LabelSize::Small) - .color(Color::Muted) - .buffer_font(cx), - ) - .child(Divider::horizontal().color(DividerColor::BorderFaded)) - .into_any_element(), + SettingsPageItem::SectionHeader(header) => { + SettingsSectionHeader::new(SharedString::new_static(header)).into_any_element() + } SettingsPageItem::SettingItem(setting_item) => { let (field_with_padding, _) = render_setting_item_inner(setting_item, true, false, cx); @@ -869,9 +857,20 @@ impl SettingsPageItem { .map(apply_padding) .child( v_flex() + .relative() .w_full() .max_w_1_2() - .child(Label::new(sub_page_link.title.clone())), + .child(Label::new(sub_page_link.title.clone())) + .when_some( + sub_page_link.description.as_ref(), + |this, description| { + this.child( + Label::new(description.clone()) + .size(LabelSize::Small) + .color(Color::Muted), + ) + }, + ), ) .child( Button::new( @@ -909,7 +908,13 @@ impl SettingsPageItem { this.push_sub_page(sub_page_link.clone(), header, cx) }) }), - ), + ) + .child(render_settings_item_link( + sub_page_link.title.clone(), + sub_page_link.json_path, + false, + cx, + )), ) .when(!is_last, |this| this.child(Divider::horizontal())) .into_any_element(), @@ -983,20 +988,6 @@ fn render_settings_item( let (found_in_file, _) = setting_item.field.file_set_in(file.clone(), cx); let file_set_in = SettingsUiFile::from_settings(found_in_file.clone()); - let clipboard_has_link = cx - .read_from_clipboard() - .and_then(|entry| entry.text()) - .map_or(false, |maybe_url| { - setting_item.field.json_path().is_some() - && maybe_url.strip_prefix("zed://settings/") == setting_item.field.json_path() - }); - - let (link_icon, link_icon_color) = if clipboard_has_link { - (IconName::Check, Color::Success) - } else { - (IconName::Link, Color::Muted) - }; - h_flex() .id(setting_item.title) .min_w_0() @@ -1056,40 +1047,60 @@ fn render_settings_item( ) .child(control) .when(sub_page_stack().is_empty(), |this| { - // Intentionally using the description to make the icon button - // unique because some items share the same title (e.g., "Font Size") - let icon_button_id = - SharedString::new(format!("copy-link-btn-{}", setting_item.description)); + this.child(render_settings_item_link( + setting_item.description, + setting_item.field.json_path(), + sub_field, + cx, + )) + }) +} - this.child( - div() - .absolute() - .top(rems_from_px(18.)) - .map(|this| { - if sub_field { - this.visible_on_hover("setting-sub-item") - .left(rems_from_px(-8.5)) - } else { - this.visible_on_hover("setting-item") - .left(rems_from_px(-22.)) - } - }) - .child({ - IconButton::new(icon_button_id, link_icon) - .icon_color(link_icon_color) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) - .tooltip(Tooltip::text("Copy Link")) - .when_some(setting_item.field.json_path(), |this, path| { - this.on_click(cx.listener(move |_, _, _, cx| { - let link = format!("zed://settings/{}", path); - cx.write_to_clipboard(ClipboardItem::new_string(link)); - cx.notify(); - })) - }) - }), - ) +fn render_settings_item_link( + id: impl Into, + json_path: Option<&'static str>, + sub_field: bool, + cx: &mut Context<'_, SettingsWindow>, +) -> impl IntoElement { + let clipboard_has_link = cx + .read_from_clipboard() + .and_then(|entry| entry.text()) + .map_or(false, |maybe_url| { + json_path.is_some() && maybe_url.strip_prefix("zed://settings/") == json_path + }); + + let (link_icon, link_icon_color) = if clipboard_has_link { + (IconName::Check, Color::Success) + } else { + (IconName::Link, Color::Muted) + }; + + div() + .absolute() + .top(rems_from_px(18.)) + .map(|this| { + if sub_field { + this.visible_on_hover("setting-sub-item") + .left(rems_from_px(-8.5)) + } else { + this.visible_on_hover("setting-item") + .left(rems_from_px(-22.)) + } }) + .child( + IconButton::new((id.into(), "copy-link-btn"), link_icon) + .icon_color(link_icon_color) + .icon_size(IconSize::Small) + .shape(IconButtonShape::Square) + .tooltip(Tooltip::text("Copy Link")) + .when_some(json_path, |this, path| { + this.on_click(cx.listener(move |_, _, _, cx| { + let link = format!("zed://settings/{}", path); + cx.write_to_clipboard(ClipboardItem::new_string(link)); + cx.notify(); + })) + }), + ) } struct SettingItem { @@ -1175,6 +1186,12 @@ impl PartialEq for SettingItem { #[derive(Clone)] struct SubPageLink { title: SharedString, + description: Option, + /// See [`SettingField.json_path`] + json_path: Option<&'static str>, + /// Whether or not the settings in this sub page are configurable in settings.json + /// Removes the "Edit in settings.json" button from the page. + in_json: bool, files: FileMask, render: Arc< dyn Fn(&mut SettingsWindow, &mut Window, &mut Context) -> AnyElement @@ -1835,6 +1852,7 @@ impl SettingsWindow { header_str = *header; } SettingsPageItem::SubPageLink(sub_page_link) => { + json_path = sub_page_link.json_path; documents.push(bm25::Document { id: key_index, contents: [page.title, header_str, sub_page_link.title.as_ref()] @@ -2758,19 +2776,49 @@ impl SettingsWindow { page_content } - fn render_sub_page_items<'a, Items: Iterator>( + fn render_sub_page_items<'a, Items>( &self, items: Items, page_index: Option, window: &mut Window, cx: &mut Context, - ) -> impl IntoElement { - let mut page_content = v_flex() + ) -> impl IntoElement + where + Items: Iterator, + { + let page_content = v_flex() .id("settings-ui-page") .size_full() .overflow_y_scroll() .track_scroll(&self.sub_page_scroll_handle); + self.render_sub_page_items_in(page_content, items, page_index, window, cx) + } + + fn render_sub_page_items_section<'a, Items>( + &self, + items: Items, + page_index: Option, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement + where + Items: Iterator, + { + let page_content = v_flex().id("settings-ui-sub-page-section").size_full(); + self.render_sub_page_items_in(page_content, items, page_index, window, cx) + } + fn render_sub_page_items_in<'a, Items>( + &self, + mut page_content: Stateful
, + items: Items, + page_index: Option, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement + where + Items: Iterator, + { let items: Vec<_> = items.collect(); let items_len = items.len(); let mut section_header = None; @@ -2871,18 +2919,25 @@ impl SettingsWindow { ) .child(self.render_sub_page_breadcrumbs()), ) - .child( - Button::new("open-in-settings-file", "Edit in settings.json") - .tab_index(0_isize) - .style(ButtonStyle::OutlinedGhost) - .tooltip(Tooltip::for_action_title_in( - "Edit in settings.json", - &OpenCurrentFile, - &self.focus_handle, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.open_current_settings_file(window, cx); - })), + .when( + sub_page_stack() + .last() + .is_none_or(|sub_page| sub_page.link.in_json), + |this| { + this.child( + Button::new("open-in-settings-file", "Edit in settings.json") + .tab_index(0_isize) + .style(ButtonStyle::OutlinedGhost) + .tooltip(Tooltip::for_action_title_in( + "Edit in settings.json", + &OpenCurrentFile, + &self.focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| { + this.open_current_settings_file(window, cx); + })), + ) + }, ) .into_any_element(); diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index bfc4587969ec67bbda2fb90d34550c7d464317c9..6a76b73c3bbfb922e1b46fc1e228209ddf05b4a5 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -250,11 +250,11 @@ impl SumTree { ::add_summary(&mut summary, item_summary, cx); } - nodes.push(Node::Leaf { + nodes.push(SumTree(Arc::new(Node::Leaf { summary, items, item_summaries, - }); + }))); } let mut parent_nodes = Vec::new(); @@ -263,25 +263,27 @@ impl SumTree { height += 1; let mut current_parent_node = None; for child_node in nodes.drain(..) { - let parent_node = current_parent_node.get_or_insert_with(|| Node::Internal { - summary: ::zero(cx), - height, - child_summaries: ArrayVec::new(), - child_trees: ArrayVec::new(), + let parent_node = current_parent_node.get_or_insert_with(|| { + SumTree(Arc::new(Node::Internal { + summary: ::zero(cx), + height, + child_summaries: ArrayVec::new(), + child_trees: ArrayVec::new(), + })) }); let Node::Internal { summary, child_summaries, child_trees, .. - } = parent_node + } = Arc::get_mut(&mut parent_node.0).unwrap() else { unreachable!() }; let child_summary = child_node.summary(); ::add_summary(summary, child_summary, cx); child_summaries.push(child_summary.clone()); - child_trees.push(Self(Arc::new(child_node))); + child_trees.push(child_node); if child_trees.len() == 2 * TREE_BASE { parent_nodes.extend(current_parent_node.take()); @@ -295,7 +297,7 @@ impl SumTree { Self::new(cx) } else { debug_assert_eq!(nodes.len(), 1); - Self(Arc::new(nodes.pop().unwrap())) + nodes.pop().unwrap() } } diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 1266c5a6e5c2141be4255530d062f22cd87046fd..9b4302d02fc0a101cc609274b0abc42105402174 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -38,6 +38,7 @@ smol.workspace = true task.workspace = true theme.workspace = true thiserror.workspace = true +url.workspace = true util.workspace = true urlencoding.workspace = true @@ -49,5 +50,4 @@ gpui = { workspace = true, features = ["test-support"] } rand.workspace = true serde_json.workspace = true settings = { workspace = true, features = ["test-support"] } -url.workspace = true util_macros.workspace = true diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index caca93eac5b862450cdaa2aede0fd5491eaaf58f..e6bb454fa296b65de60c25f326bba28f484450f0 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -420,6 +420,10 @@ impl TerminalBuilder { ) -> Task> { let version = release_channel::AppVersion::global(cx); let fut = async move { + // Remove SHLVL so the spawned shell initializes it to 1, matching + // the behavior of standalone terminal emulators like iTerm2/Kitty/Alacritty. + env.remove("SHLVL"); + // If the parent environment doesn't have a locale set // (As is the case when launched from a .app on MacOS), // and the Project doesn't have a locale set, then diff --git a/crates/terminal/src/terminal_hyperlinks.rs b/crates/terminal/src/terminal_hyperlinks.rs index 71a1634076b7081cce4f5cbaa155e7eec5d7f57e..cff27c4567cca84b2310723bf73bfda8d58c166d 100644 --- a/crates/terminal/src/terminal_hyperlinks.rs +++ b/crates/terminal/src/terminal_hyperlinks.rs @@ -14,6 +14,7 @@ use std::{ ops::{Index, Range}, time::{Duration, Instant}, }; +use url::Url; const URL_REGEX: &str = r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`']+"#; const WIDE_CHAR_SPACERS: Flags = @@ -128,8 +129,19 @@ pub(super) fn find_from_grid_point( if is_url { // Treat "file://" IRIs like file paths to ensure // that line numbers at the end of the path are - // handled correctly - if let Some(path) = maybe_url_or_path.strip_prefix("file://") { + // handled correctly. + // Use Url::to_file_path() to properly handle Windows drive letters + // (e.g., file:///C:/path -> C:\path) + if maybe_url_or_path.starts_with("file://") { + if let Ok(url) = Url::parse(&maybe_url_or_path) { + if let Ok(path) = url.to_file_path() { + return (path.to_string_lossy().into_owned(), false, word_match); + } + } + // Fallback: strip file:// prefix if URL parsing fails + let path = maybe_url_or_path + .strip_prefix("file://") + .unwrap_or(&maybe_url_or_path); (path.to_string(), false, word_match) } else { (maybe_url_or_path, true, word_match) @@ -1042,8 +1054,9 @@ mod tests { } mod file_iri { - // File IRIs have a ton of use cases, most of which we currently do not support. A few of - // those cases are documented here as tests which are expected to fail. + // File IRIs have a ton of use cases. Absolute file URIs are supported on all platforms, + // including Windows drive letters (e.g., file:///C:/path) and percent-encoded characters. + // Some cases like relative file IRIs are not supported. // See https://en.wikipedia.org/wiki/File_URI_scheme /// [**`c₀, c₁, …, cₙ;`**]ₒₚₜ := use specified terminal widths of `c₀, c₁, …, cₙ` **columns** @@ -1063,7 +1076,6 @@ mod tests { mod issues { #[cfg(not(target_os = "windows"))] #[test] - #[should_panic(expected = "Path = «/test/Ῥόδος/», at grid cells (0, 0)..=(15, 1)")] fn issue_file_iri_with_percent_encoded_characters() { // Non-space characters // file:///test/Ῥόδος/ @@ -1092,18 +1104,12 @@ mod tests { // See https://en.wikipedia.org/wiki/File_URI_scheme // https://github.com/zed-industries/zed/issues/39189 #[test] - #[should_panic( - expected = r#"Path = «C:\\test\\cool\\index.rs», at grid cells (0, 0)..=(9, 1)"# - )] fn issue_39189() { test_file_iri!("file:///C:/test/cool/index.rs"); test_file_iri!("file:///C:/test/cool/"); } #[test] - #[should_panic( - expected = r#"Path = «C:\\test\\Ῥόδος\\», at grid cells (0, 0)..=(16, 1)"# - )] fn issue_file_iri_with_percent_encoded_characters() { // Non-space characters // file:///test/Ῥόδος/ diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index ab89787fc88510f4c92e929d96b51c682ff0af61..fb660e759c75aee9752cbaa3bdc8c8e0a47615e3 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -342,7 +342,7 @@ impl TerminalPanel { pane::Event::RemovedItem { .. } => self.serialize(cx), pane::Event::Remove { focus_on_pane } => { let pane_count_before_removal = self.center.panes().len(); - let _removal_result = self.center.remove(pane); + let _removal_result = self.center.remove(pane, cx); if pane_count_before_removal == 1 { self.center.first_pane().update(cx, |pane, cx| { pane.set_zoomed(false, cx); @@ -393,7 +393,10 @@ impl TerminalPanel { }; panel .update_in(cx, |panel, window, cx| { - panel.center.split(&pane, &new_pane, direction).log_err(); + panel + .center + .split(&pane, &new_pane, direction, cx) + .log_err(); window.focus(&new_pane.focus_handle(cx)); }) .ok(); @@ -415,7 +418,7 @@ impl TerminalPanel { new_pane.update(cx, |pane, cx| { pane.add_item(item, true, true, None, window, cx); }); - self.center.split(&pane, &new_pane, direction).log_err(); + self.center.split(&pane, &new_pane, direction, cx).log_err(); window.focus(&new_pane.focus_handle(cx)); } } @@ -1066,7 +1069,7 @@ impl TerminalPanel { .find_pane_in_direction(&self.active_pane, direction, cx) .cloned() { - self.center.swap(&self.active_pane, &to); + self.center.swap(&self.active_pane, &to, cx); cx.notify(); } } @@ -1074,7 +1077,7 @@ impl TerminalPanel { fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context) { if self .center - .move_to_border(&self.active_pane, direction) + .move_to_border(&self.active_pane, direction, cx) .unwrap() { cx.notify(); @@ -1189,6 +1192,7 @@ pub fn new_terminal_pane( &this_pane, &new_pane, split_direction, + cx, )?; anyhow::Ok(new_pane) }) @@ -1482,6 +1486,7 @@ impl Render for TerminalPanel { &terminal_panel.active_pane, &new_pane, SplitDirection::Right, + cx, ) .log_err(); let new_pane = new_pane.read(cx); diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 680c455e73ab135f418f199f06415fff79100ea5..5bd47d02691c9a5c7fec968b5ea6e97265b956b2 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -167,7 +167,7 @@ impl Render for TitleBar { .child(self.render_project_name(cx)) }) .when(title_bar_settings.show_branch_name, |title_bar| { - title_bar.children(self.render_project_branch(cx)) + title_bar.children(self.render_project_repo(cx)) }) }) }) @@ -202,9 +202,11 @@ impl Render for TitleBar { .children(self.render_connection_status(status, cx)) .when( user.is_none() && TitleBarSettings::get_global(cx).show_sign_in, - |el| el.child(self.render_sign_in_button(cx)), + |this| this.child(self.render_sign_in_button(cx)), ) - .child(self.render_app_menu_button(cx)) + .when(TitleBarSettings::get_global(cx).show_user_menu, |this| { + this.child(self.render_user_menu_button(cx)) + }) .into_any_element(), ); @@ -319,6 +321,27 @@ impl TitleBar { } } + fn project_name(&self, cx: &Context) -> Option { + self.project + .read(cx) + .visible_worktrees(cx) + .map(|worktree| { + let worktree = worktree.read(cx); + let settings_location = SettingsLocation { + worktree_id: worktree.id(), + path: RelPath::empty(), + }; + + let settings = WorktreeSettings::get(Some(settings_location), cx); + let name = match &settings.project_name { + Some(name) => name.as_str(), + None => worktree.root_name_str(), + }; + SharedString::new(name) + }) + .next() + } + fn render_remote_project_connection(&self, cx: &mut Context) -> Option { let options = self.project.read(cx).remote_connection_options(cx)?; let host: SharedString = options.display_name().into(); @@ -451,27 +474,10 @@ impl TitleBar { } pub fn render_project_name(&self, cx: &mut Context) -> impl IntoElement { - let name = self - .project - .read(cx) - .visible_worktrees(cx) - .map(|worktree| { - let worktree = worktree.read(cx); - let settings_location = SettingsLocation { - worktree_id: worktree.id(), - path: RelPath::empty(), - }; - - let settings = WorktreeSettings::get(Some(settings_location), cx); - match &settings.project_name { - Some(name) => name.as_str(), - None => worktree.root_name_str(), - } - }) - .next(); + let name = self.project_name(cx); let is_project_selected = name.is_some(); let name = if let Some(name) = name { - util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH) + util::truncate_and_trailoff(&name, MAX_PROJECT_NAME_LENGTH) } else { "Open recent project".to_string() }; @@ -500,9 +506,10 @@ impl TitleBar { })) } - pub fn render_project_branch(&self, cx: &mut Context) -> Option { + pub fn render_project_repo(&self, cx: &mut Context) -> Option { let settings = TitleBarSettings::get_global(cx); let repository = self.project.read(cx).active_repository(cx)?; + let repository_count = self.project.read(cx).repositories(cx).len(); let workspace = self.workspace.upgrade()?; let repo = repository.read(cx); let branch_name = repo @@ -519,6 +526,19 @@ impl TitleBar { .collect::() }) })?; + let project_name = self.project_name(cx); + let repo_name = repo + .work_directory_abs_path + .file_name() + .and_then(|name| name.to_str()) + .map(SharedString::new); + let show_repo_name = + repository_count > 1 && repo.branch.is_some() && repo_name != project_name; + let branch_name = if let Some(repo_name) = repo_name.filter(|_| show_repo_name) { + format!("{repo_name}/{branch_name}") + } else { + branch_name + }; Some( Button::new("project_branch_trigger", branch_name) @@ -667,7 +687,7 @@ impl TitleBar { }) } - pub fn render_app_menu_button(&mut self, cx: &mut Context) -> impl Element { + pub fn render_user_menu_button(&mut self, cx: &mut Context) -> impl Element { let user_store = self.user_store.read(cx); let user = user_store.current_user(); diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index 29fae4d31eb33ac70a22c21010f09350847439c2..155b7b7bc797567927a70b12c677372cb92c9453 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -8,6 +8,7 @@ pub struct TitleBarSettings { pub show_branch_name: bool, pub show_project_items: bool, pub show_sign_in: bool, + pub show_user_menu: bool, pub show_menus: bool, } @@ -21,6 +22,7 @@ impl Settings for TitleBarSettings { show_branch_name: content.show_branch_name.unwrap(), show_project_items: content.show_project_items.unwrap(), show_sign_in: content.show_sign_in.unwrap(), + show_user_menu: content.show_user_menu.unwrap(), show_menus: content.show_menus.unwrap(), } } diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index 138f99066f0a80188837de49f6afc67d91d9eeb5..b58b2f8d699f59c15525c452543cf5bdf071ad2c 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -128,67 +128,61 @@ impl AddToolchainState { ) -> (OpenPathDelegate, oneshot::Receiver>>) { let (tx, rx) = oneshot::channel(); let weak = cx.weak_entity(); - let path_style = project.read(cx).path_style(cx); - let lister = - OpenPathDelegate::new(tx, DirectoryLister::Project(project), false, path_style) - .show_hidden() - .with_footer(Arc::new(move |_, cx| { - let error = weak - .read_with(cx, |this, _| { - if let AddState::Path { error, .. } = &this.state { - error.clone() - } else { - None + let lister = OpenPathDelegate::new(tx, DirectoryLister::Project(project), false, cx) + .show_hidden() + .with_footer(Arc::new(move |_, cx| { + let error = weak + .read_with(cx, |this, _| { + if let AddState::Path { error, .. } = &this.state { + error.clone() + } else { + None + } + }) + .ok() + .flatten(); + let is_loading = weak + .read_with(cx, |this, _| { + matches!( + this.state, + AddState::Path { + input_state: PathInputState::Resolving(_), + .. } - }) - .ok() - .flatten(); - let is_loading = weak - .read_with(cx, |this, _| { - matches!( - this.state, - AddState::Path { - input_state: PathInputState::Resolving(_), - .. - } - ) - }) - .unwrap_or_default(); - Some( - v_flex() - .child(Divider::horizontal()) - .child( - h_flex() - .p_1() - .justify_between() - .gap_2() - .child( - Label::new("Select Toolchain Path") - .color(Color::Muted) - .map(|this| { - if is_loading { - this.with_animation( - "select-toolchain-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between( - 0.4, 0.8, - )), - |label, delta| label.alpha(delta), - ) - .into_any() - } else { - this.into_any_element() - } - }), - ) - .when_some(error, |this, error| { - this.child(Label::new(error).color(Color::Error)) - }), - ) - .into_any(), - ) - })); + ) + }) + .unwrap_or_default(); + Some( + v_flex() + .child(Divider::horizontal()) + .child( + h_flex() + .p_1() + .justify_between() + .gap_2() + .child(Label::new("Select Toolchain Path").color(Color::Muted).map( + |this| { + if is_loading { + this.with_animation( + "select-toolchain-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.alpha(delta), + ) + .into_any() + } else { + this.into_any_element() + } + }, + )) + .when_some(error, |this, error| { + this.child(Label::new(error).color(Color::Error)) + }), + ) + .into_any(), + ) + })); (lister, rx) } diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index b6318f18c973ca5ca7eefa1ba39517ef65cad6df..c9cb943277c6c6a5e6bc1b472040c31d9caac45c 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,3 +1,4 @@ +mod ai; mod avatar; mod banner; mod button; @@ -16,6 +17,7 @@ mod icon; mod image; mod indent_guides; mod indicator; +mod inline_code; mod keybinding; mod keybinding_hint; mod label; @@ -43,6 +45,7 @@ mod tree_view_item; #[cfg(feature = "stories")] mod stories; +pub use ai::*; pub use avatar::*; pub use banner::*; pub use button::*; @@ -61,6 +64,7 @@ pub use icon::*; pub use image::*; pub use indent_guides::*; pub use indicator::*; +pub use inline_code::*; pub use keybinding::*; pub use keybinding_hint::*; pub use label::*; diff --git a/crates/ui/src/components/ai.rs b/crates/ui/src/components/ai.rs new file mode 100644 index 0000000000000000000000000000000000000000..e36361b7b06559c1442b86acf26b6694bb950d82 --- /dev/null +++ b/crates/ui/src/components/ai.rs @@ -0,0 +1,3 @@ +mod configured_api_card; + +pub use configured_api_card::*; diff --git a/crates/language_models/src/ui/configured_api_card.rs b/crates/ui/src/components/ai/configured_api_card.rs similarity index 84% rename from crates/language_models/src/ui/configured_api_card.rs rename to crates/ui/src/components/ai/configured_api_card.rs index 063ac1717f3aa5de1a448e26c94df7530fec588f..37f9ac7602d676906565a911f1bbca6d2b40f755 100644 --- a/crates/language_models/src/ui/configured_api_card.rs +++ b/crates/ui/src/components/ai/configured_api_card.rs @@ -1,10 +1,11 @@ +use crate::{Tooltip, prelude::*}; use gpui::{ClickEvent, IntoElement, ParentElement, SharedString}; -use ui::{Tooltip, prelude::*}; #[derive(IntoElement)] pub struct ConfiguredApiCard { label: SharedString, button_label: Option, + button_tab_index: Option, tooltip_label: Option, disabled: bool, on_click: Option>, @@ -15,6 +16,7 @@ impl ConfiguredApiCard { Self { label: label.into(), button_label: None, + button_tab_index: None, tooltip_label: None, disabled: false, on_click: None, @@ -43,6 +45,11 @@ impl ConfiguredApiCard { self.disabled = disabled; self } + + pub fn button_tab_index(mut self, tab_index: isize) -> Self { + self.button_tab_index = Some(tab_index); + self + } } impl RenderOnce for ConfiguredApiCard { @@ -51,23 +58,27 @@ impl RenderOnce for ConfiguredApiCard { let button_id = SharedString::new(format!("id-{}", button_label)); h_flex() + .min_w_0() .mt_0p5() .p_1() .justify_between() .rounded_md() + .flex_wrap() .border_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().background) .child( h_flex() - .flex_1() .min_w_0() .gap_1() .child(Icon::new(IconName::Check).color(Color::Success)) - .child(Label::new(self.label).truncate()), + .child(Label::new(self.label)), ) .child( Button::new(button_id, button_label) + .when_some(self.button_tab_index, |elem, tab_index| { + elem.tab_index(tab_index) + }) .label_size(LabelSize::Small) .icon(IconName::Undo) .icon_size(IconSize::Small) diff --git a/crates/ui/src/components/ai/copilot_configuration_callout.rs b/crates/ui/src/components/ai/copilot_configuration_callout.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/crates/ui/src/components/button.rs b/crates/ui/src/components/button.rs index 23e7702f6241b6ca0d4074936ee20da26531fbed..d56a9c09d3b57ba607b6837b16af31d240e58663 100644 --- a/crates/ui/src/components/button.rs +++ b/crates/ui/src/components/button.rs @@ -1,12 +1,14 @@ mod button; mod button_icon; mod button_like; +mod button_link; mod icon_button; mod split_button; mod toggle_button; pub use button::*; pub use button_like::*; +pub use button_link::*; pub use icon_button::*; pub use split_button::*; pub use toggle_button::*; diff --git a/crates/ui/src/components/button/button_link.rs b/crates/ui/src/components/button/button_link.rs new file mode 100644 index 0000000000000000000000000000000000000000..caffe2772bce394be6899b1f9b3b686c3927a530 --- /dev/null +++ b/crates/ui/src/components/button/button_link.rs @@ -0,0 +1,102 @@ +use gpui::{IntoElement, Window, prelude::*}; + +use crate::{ButtonLike, prelude::*}; + +/// A button that takes an underline to look like a regular web link. +/// It also contains an arrow icon to communicate the link takes you out of Zed. +/// +/// # Usage Example +/// +/// ``` +/// use ui::ButtonLink; +/// +/// let button_link = ButtonLink::new("Click me", "https://example.com"); +/// ``` +#[derive(IntoElement, RegisterComponent)] +pub struct ButtonLink { + label: SharedString, + label_size: LabelSize, + label_color: Color, + link: String, + no_icon: bool, +} + +impl ButtonLink { + pub fn new(label: impl Into, link: impl Into) -> Self { + Self { + link: link.into(), + label: label.into(), + label_size: LabelSize::Default, + label_color: Color::Default, + no_icon: false, + } + } + + pub fn no_icon(mut self, no_icon: bool) -> Self { + self.no_icon = no_icon; + self + } + + pub fn label_size(mut self, label_size: LabelSize) -> Self { + self.label_size = label_size; + self + } + + pub fn label_color(mut self, label_color: Color) -> Self { + self.label_color = label_color; + self + } +} + +impl RenderOnce for ButtonLink { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let id = format!("{}-{}", self.label, self.link); + + ButtonLike::new(id) + .size(ButtonSize::None) + .child( + h_flex() + .gap_0p5() + .child( + Label::new(self.label) + .size(self.label_size) + .color(self.label_color) + .underline(), + ) + .when(!self.no_icon, |this| { + this.child( + Icon::new(IconName::ArrowUpRight) + .size(IconSize::Small) + .color(Color::Muted), + ) + }), + ) + .on_click(move |_, _, cx| cx.open_url(&self.link)) + .into_any_element() + } +} + +impl Component for ButtonLink { + fn scope() -> ComponentScope { + ComponentScope::Navigation + } + + fn description() -> Option<&'static str> { + Some("A button that opens a URL.") + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + Some( + v_flex() + .gap_6() + .child( + example_group(vec![single_example( + "Simple", + ButtonLink::new("zed.dev", "https://zed.dev").into_any_element(), + )]) + .vertical(), + ) + .into_any_element(), + ) + } +} diff --git a/crates/ui/src/components/divider.rs b/crates/ui/src/components/divider.rs index d6101f23203072a27febd0f8b8391af75b41d7f3..cc7ad19875d2817d98076812bb7b9ea101341107 100644 --- a/crates/ui/src/components/divider.rs +++ b/crates/ui/src/components/divider.rs @@ -144,12 +144,18 @@ impl Divider { impl RenderOnce for Divider { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let base = match self.direction { - DividerDirection::Horizontal => { - div().h_px().w_full().when(self.inset, |this| this.mx_1p5()) - } - DividerDirection::Vertical => { - div().w_px().h_full().when(self.inset, |this| this.my_1p5()) - } + DividerDirection::Horizontal => div() + .min_w_0() + .flex_none() + .h_px() + .w_full() + .when(self.inset, |this| this.mx_1p5()), + DividerDirection::Vertical => div() + .min_w_0() + .flex_none() + .w_px() + .h_full() + .when(self.inset, |this| this.my_1p5()), }; match self.style { diff --git a/crates/ui/src/components/inline_code.rs b/crates/ui/src/components/inline_code.rs new file mode 100644 index 0000000000000000000000000000000000000000..43507127fef478e5a38cfad2d84446673af15f2e --- /dev/null +++ b/crates/ui/src/components/inline_code.rs @@ -0,0 +1,64 @@ +use crate::prelude::*; +use gpui::{AnyElement, IntoElement, ParentElement, Styled}; + +/// InlineCode mimics the way inline code is rendered when wrapped in backticks in Markdown. +/// +/// # Usage Example +/// +/// ``` +/// use ui::InlineCode; +/// +/// let InlineCode = InlineCode::new("
hey
"); +/// ``` +#[derive(IntoElement, RegisterComponent)] +pub struct InlineCode { + label: SharedString, + label_size: LabelSize, +} + +impl InlineCode { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + label_size: LabelSize::Default, + } + } + + /// Sets the size of the label. + pub fn label_size(mut self, size: LabelSize) -> Self { + self.label_size = size; + self + } +} + +impl RenderOnce for InlineCode { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + h_flex() + .min_w_0() + .px_0p5() + .overflow_hidden() + .bg(cx.theme().colors().text.opacity(0.05)) + .child(Label::new(self.label).size(self.label_size).buffer_font(cx)) + } +} + +impl Component for InlineCode { + fn scope() -> ComponentScope { + ComponentScope::DataDisplay + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + Some( + v_flex() + .gap_6() + .child( + example_group(vec![single_example( + "Simple", + InlineCode::new("zed.dev").into_any_element(), + )]) + .vertical(), + ) + .into_any_element(), + ) + } +} diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index c998e29f0ed6f5bccab976b11080320d4d65a7dd..7c19953ca43c907070829f7140f97a4fde495b57 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/crates/ui/src/components/keybinding_hint.rs @@ -234,9 +234,7 @@ impl RenderOnce for KeybindingHint { let mut base = h_flex(); - base.text_style() - .get_or_insert_with(Default::default) - .font_style = Some(FontStyle::Italic); + base.text_style().font_style = Some(FontStyle::Italic); base.gap_1() .font_buffer(cx) diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index 1fa6b14c83d8359df234f33ecb9318c88e3a2714..31fb7bfd88f1343ac6145c86f228bdcbd6a22e10 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -223,11 +223,9 @@ impl RenderOnce for LabelLike { }) .when(self.italic, |this| this.italic()) .when(self.underline, |mut this| { - this.text_style() - .get_or_insert_with(Default::default) - .underline = Some(UnderlineStyle { + this.text_style().underline = Some(UnderlineStyle { thickness: px(1.), - color: None, + color: Some(cx.theme().colors().text_muted.opacity(0.4)), wavy: false, }); this diff --git a/crates/ui/src/components/list/list_bullet_item.rs b/crates/ui/src/components/list/list_bullet_item.rs index 17731488f7139522bf19aeaab18fb395d1eb68b0..934f0853dbe18b8231e15073766b6c84c1896546 100644 --- a/crates/ui/src/components/list/list_bullet_item.rs +++ b/crates/ui/src/components/list/list_bullet_item.rs @@ -1,18 +1,33 @@ -use crate::{ListItem, prelude::*}; -use component::{Component, ComponentScope, example_group_with_title, single_example}; +use crate::{ButtonLink, ListItem, prelude::*}; +use component::{Component, ComponentScope, example_group, single_example}; use gpui::{IntoElement, ParentElement, SharedString}; #[derive(IntoElement, RegisterComponent)] pub struct ListBulletItem { label: SharedString, + label_color: Option, + children: Vec, } impl ListBulletItem { pub fn new(label: impl Into) -> Self { Self { label: label.into(), + label_color: None, + children: Vec::new(), } } + + pub fn label_color(mut self, color: Color) -> Self { + self.label_color = Some(color); + self + } +} + +impl ParentElement for ListBulletItem { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } } impl RenderOnce for ListBulletItem { @@ -34,7 +49,18 @@ impl RenderOnce for ListBulletItem { .color(Color::Hidden), ), ) - .child(div().w_full().min_w_0().child(Label::new(self.label))), + .map(|this| { + if !self.children.is_empty() { + this.child(h_flex().gap_0p5().flex_wrap().children(self.children)) + } else { + this.child( + div().w_full().min_w_0().child( + Label::new(self.label) + .color(self.label_color.unwrap_or(Color::Default)), + ), + ) + } + }), ) .into_any_element() } @@ -46,37 +72,43 @@ impl Component for ListBulletItem { } fn description() -> Option<&'static str> { - Some("A list item with a bullet point indicator for unordered lists.") + Some("A list item with a dash indicator for unordered lists.") } fn preview(_window: &mut Window, _cx: &mut App) -> Option { + let basic_examples = vec![ + single_example( + "Simple", + ListBulletItem::new("First bullet item").into_any_element(), + ), + single_example( + "Multiple Lines", + v_flex() + .child(ListBulletItem::new("First item")) + .child(ListBulletItem::new("Second item")) + .child(ListBulletItem::new("Third item")) + .into_any_element(), + ), + single_example( + "Long Text", + ListBulletItem::new( + "A longer bullet item that demonstrates text wrapping behavior", + ) + .into_any_element(), + ), + single_example( + "With Link", + ListBulletItem::new("") + .child(Label::new("Create a Zed account by")) + .child(ButtonLink::new("visiting the website", "https://zed.dev")) + .into_any_element(), + ), + ]; + Some( v_flex() .gap_6() - .child(example_group_with_title( - "Bullet Items", - vec![ - single_example( - "Simple", - ListBulletItem::new("First bullet item").into_any_element(), - ), - single_example( - "Multiple Lines", - v_flex() - .child(ListBulletItem::new("First item")) - .child(ListBulletItem::new("Second item")) - .child(ListBulletItem::new("Third item")) - .into_any_element(), - ), - single_example( - "Long Text", - ListBulletItem::new( - "A longer bullet item that demonstrates text wrapping behavior", - ) - .into_any_element(), - ), - ], - )) + .child(example_group(basic_examples).vertical()) .into_any_element(), ) } diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs index 5d41466e3caadf6697b3c1681a405dafa2fb3101..86598b8c6f1ab3a479313c7775405863e9e3b49b 100644 --- a/crates/ui/src/components/tab_bar.rs +++ b/crates/ui/src/components/tab_bar.rs @@ -10,6 +10,7 @@ pub struct TabBar { start_children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>, end_children: SmallVec<[AnyElement; 2]>, + pre_end_children: SmallVec<[AnyElement; 2]>, scroll_handle: Option, } @@ -20,6 +21,7 @@ impl TabBar { start_children: SmallVec::new(), children: SmallVec::new(), end_children: SmallVec::new(), + pre_end_children: SmallVec::new(), scroll_handle: None, } } @@ -70,6 +72,15 @@ impl TabBar { self } + pub fn pre_end_child(mut self, end_child: impl IntoElement) -> Self + where + Self: Sized, + { + self.pre_end_children + .push(end_child.into_element().into_any()); + self + } + pub fn end_children(mut self, end_children: impl IntoIterator) -> Self where Self: Sized, @@ -137,18 +148,32 @@ impl RenderOnce for TabBar { .children(self.children), ), ) - .when(!self.end_children.is_empty(), |this| { - this.child( - h_flex() - .flex_none() - .gap(DynamicSpacing::Base04.rems(cx)) - .px(DynamicSpacing::Base06.rems(cx)) - .border_b_1() - .border_l_1() - .border_color(cx.theme().colors().border) - .children(self.end_children), - ) - }) + .when( + !self.end_children.is_empty() || !self.pre_end_children.is_empty(), + |this| { + this.child( + h_flex() + .flex_none() + .gap(DynamicSpacing::Base04.rems(cx)) + .px(DynamicSpacing::Base06.rems(cx)) + .children(self.pre_end_children) + .border_color(cx.theme().colors().border) + .border_b_1() + .when(!self.end_children.is_empty(), |div| { + div.child( + h_flex() + .h_full() + .flex_none() + .pl(DynamicSpacing::Base04.rems(cx)) + .gap(DynamicSpacing::Base04.rems(cx)) + .border_l_1() + .border_color(cx.theme().colors().border) + .children(self.end_children), + ) + }), + ) + }, + ) } } diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 74409a6c255645378b0b2829f4d0045776bfa019..2db1b51e72fcd862ccb1c35ff920fec7dbd47995 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -66,6 +66,7 @@ lsp = { workspace = true, features = ["test-support"] } markdown_preview.workspace = true parking_lot.workspace = true project_panel.workspace = true +outline_panel.workspace = true release_channel.workspace = true semver.workspace = true settings_ui.workspace = true diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index acd77839f2d8cc09ed72993638ff4ec66f79d3fc..2d5ed4227dcc263f56cfa0bcb337f5673df8ef3c 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -23,6 +23,7 @@ impl VimTestContext { release_channel::init(Version::new(0, 0, 0), cx); command_palette::init(cx); project_panel::init(cx); + outline_panel::init(cx); git_ui::init(cx); crate::init(cx); search::init(cx); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 9a9a1a001c32fcf8b22892ce5300d8d2aec3dd37..26fec968fb261fbb80a9f84211357623147ca0f4 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1943,6 +1943,7 @@ impl Vim { editor.set_collapse_matches(collapse_matches); editor.set_input_enabled(vim.editor_input_enabled()); editor.set_autoindent(vim.should_autoindent()); + editor.set_cursor_offset_on_selection(vim.mode.is_visual()); editor .selections .set_line_mode(matches!(vim.mode, Mode::VisualLine)); diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index d5d3016ab2704392c6cc9cc4bcebf6d50701d3be..acf95df37f5d20da65b6e9fa4460ba09b2ea81e3 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -35,6 +35,7 @@ clock.workspace = true collections.workspace = true component.workspace = true db.workspace = true +feature_flags.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index dfc341db9c71fd1059853b9480a7e679109ead40..edc5705a28ecd7d378c0f959ac82a6493c82d325 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -1,8 +1,10 @@ use crate::persistence::model::DockData; +use crate::utility_pane::utility_slot_for_dock_position; use crate::{DraggedDock, Event, ModalLayer, Pane}; use crate::{Workspace, status_bar::StatusItemView}; use anyhow::Context as _; use client::proto; + use gpui::{ Action, AnyView, App, Axis, Context, Corner, Entity, EntityId, EventEmitter, FocusHandle, Focusable, IntoElement, KeyContext, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement, @@ -13,6 +15,7 @@ use settings::SettingsStore; use std::sync::Arc; use ui::{ContextMenu, Divider, DividerColor, IconButton, Tooltip, h_flex}; use ui::{prelude::*, right_click_menu}; +use util::ResultExt as _; pub(crate) const RESIZE_HANDLE_SIZE: Pixels = px(6.); @@ -25,6 +28,72 @@ pub enum PanelEvent { pub use proto::PanelId; +pub struct MinimizePane; +pub struct ClosePane; + +pub trait UtilityPane: EventEmitter + EventEmitter + Render { + fn position(&self, window: &Window, cx: &App) -> UtilityPanePosition; + /// The icon to render in the adjacent pane's tab bar for toggling this utility pane + fn toggle_icon(&self, cx: &App) -> IconName; + fn expanded(&self, cx: &App) -> bool; + fn set_expanded(&mut self, expanded: bool, cx: &mut Context); + fn width(&self, cx: &App) -> Pixels; + fn set_width(&mut self, width: Option, cx: &mut Context); +} + +pub trait UtilityPaneHandle: 'static + Send + Sync { + fn position(&self, window: &Window, cx: &App) -> UtilityPanePosition; + fn toggle_icon(&self, cx: &App) -> IconName; + fn expanded(&self, cx: &App) -> bool; + fn set_expanded(&self, expanded: bool, cx: &mut App); + fn width(&self, cx: &App) -> Pixels; + fn set_width(&self, width: Option, cx: &mut App); + fn to_any(&self) -> AnyView; + fn box_clone(&self) -> Box; +} + +impl UtilityPaneHandle for Entity +where + T: UtilityPane, +{ + fn position(&self, window: &Window, cx: &App) -> UtilityPanePosition { + self.read(cx).position(window, cx) + } + + fn toggle_icon(&self, cx: &App) -> IconName { + self.read(cx).toggle_icon(cx) + } + + fn expanded(&self, cx: &App) -> bool { + self.read(cx).expanded(cx) + } + + fn set_expanded(&self, expanded: bool, cx: &mut App) { + self.update(cx, |this, cx| this.set_expanded(expanded, cx)) + } + + fn width(&self, cx: &App) -> Pixels { + self.read(cx).width(cx) + } + + fn set_width(&self, width: Option, cx: &mut App) { + self.update(cx, |this, cx| this.set_width(width, cx)) + } + + fn to_any(&self) -> AnyView { + self.clone().into() + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +pub enum UtilityPanePosition { + Left, + Right, +} + pub trait Panel: Focusable + EventEmitter + Render + Sized { fn persistent_name() -> &'static str; fn panel_key() -> &'static str; @@ -384,6 +453,13 @@ impl Dock { .position(|entry| entry.panel.remote_id() == Some(panel_id)) } + pub fn panel_for_id(&self, panel_id: EntityId) -> Option<&Arc> { + self.panel_entries + .iter() + .find(|entry| entry.panel.panel_id() == panel_id) + .map(|entry| &entry.panel) + } + pub fn first_enabled_panel_idx(&mut self, cx: &mut Context) -> anyhow::Result { self.panel_entries .iter() @@ -491,6 +567,9 @@ impl Dock { new_dock.update(cx, |new_dock, cx| { new_dock.remove_panel(&panel, window, cx); + }); + + new_dock.update(cx, |new_dock, cx| { let index = new_dock.add_panel(panel.clone(), workspace.clone(), window, cx); if was_visible { @@ -498,6 +577,12 @@ impl Dock { new_dock.activate_panel(index, window, cx); } }); + + workspace + .update(cx, |workspace, cx| { + workspace.serialize_workspace(window, cx); + }) + .ok(); } }), cx.subscribe_in( @@ -586,6 +671,7 @@ impl Dock { ); self.restore_state(window, cx); + if panel.read(cx).starts_open(window, cx) { self.activate_panel(index, window, cx); self.set_open(true, window, cx); @@ -637,6 +723,14 @@ impl Dock { std::cmp::Ordering::Greater => {} } } + + let slot = utility_slot_for_dock_position(self.position); + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.clear_utility_pane_if_provider(slot, Entity::entity_id(panel), cx); + }); + } + self.panel_entries.remove(panel_ix); cx.notify(); } @@ -891,7 +985,13 @@ impl Render for PanelButtons { .enumerate() .filter_map(|(i, entry)| { let icon = entry.panel.icon(window, cx)?; - let icon_tooltip = entry.panel.icon_tooltip(window, cx)?; + let icon_tooltip = entry + .panel + .icon_tooltip(window, cx) + .ok_or_else(|| { + anyhow::anyhow!("can't render a panel button without an icon tooltip") + }) + .log_err()?; let name = entry.panel.persistent_name(); let panel = entry.panel.clone(); diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index cfdc730b4db5be8e2f4a317dcf7e12072af40a88..6d37ea4d2a50637ae7c2e0287ae8f371e3b47aba 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -41,7 +41,7 @@ pub enum NotificationId { impl NotificationId { /// Returns a unique [`NotificationId`] for the given type. - pub fn unique() -> Self { + pub const fn unique() -> Self { Self::Unique(TypeId::of::()) } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e99f8d1dc959def06deebae7c4acc454c9210933..50ba58926ece8818ac5a4f44103c3b86eb2b672d 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -11,10 +11,12 @@ use crate::{ move_item, notifications::NotifyResultExt, toolbar::Toolbar, + utility_pane::UtilityPaneSlot, workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings}, }; use anyhow::Result; use collections::{BTreeSet, HashMap, HashSet, VecDeque}; +use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt}; use futures::{StreamExt, stream::FuturesUnordered}; use gpui::{ Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div, @@ -396,6 +398,10 @@ pub struct Pane { diagnostic_summary_update: Task<()>, /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here. pub project_item_restoration_data: HashMap>, + + pub in_center_group: bool, + pub is_upper_left: bool, + pub is_upper_right: bool, } pub struct ActivationHistoryEntry { @@ -540,6 +546,9 @@ impl Pane { zoom_out_on_close: true, diagnostic_summary_update: Task::ready(()), project_item_restoration_data: HashMap::default(), + in_center_group: false, + is_upper_left: false, + is_upper_right: false, } } @@ -3033,7 +3042,13 @@ impl Pane { } fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { + let Some(workspace) = self.workspace.upgrade() else { + return gpui::Empty.into_any(); + }; + let focus_handle = self.focus_handle.clone(); + let is_pane_focused = self.has_focus(window, cx); + let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft) .icon_size(IconSize::Small) .on_click({ @@ -3057,6 +3072,70 @@ impl Pane { } }); + let open_aside_left = { + let workspace = workspace.read(cx); + workspace.utility_pane(UtilityPaneSlot::Left).map(|pane| { + let toggle_icon = pane.toggle_icon(cx); + let workspace_handle = self.workspace.clone(); + + h_flex() + .h_full() + .pr_1p5() + .border_r_1() + .border_color(cx.theme().colors().border) + .child( + IconButton::new("open_aside_left", toggle_icon) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Toggle Agent Pane")) // TODO: Probably want to make this generic + .on_click(move |_, window, cx| { + workspace_handle + .update(cx, |workspace, cx| { + workspace.toggle_utility_pane( + UtilityPaneSlot::Left, + window, + cx, + ) + }) + .ok(); + }), + ) + .into_any_element() + }) + }; + + let open_aside_right = { + let workspace = workspace.read(cx); + workspace.utility_pane(UtilityPaneSlot::Right).map(|pane| { + let toggle_icon = pane.toggle_icon(cx); + let workspace_handle = self.workspace.clone(); + + h_flex() + .h_full() + .when(is_pane_focused, |this| { + this.pl(DynamicSpacing::Base04.rems(cx)) + .border_l_1() + .border_color(cx.theme().colors().border) + }) + .child( + IconButton::new("open_aside_right", toggle_icon) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Toggle Agent Pane")) // TODO: Probably want to make this generic + .on_click(move |_, window, cx| { + workspace_handle + .update(cx, |workspace, cx| { + workspace.toggle_utility_pane( + UtilityPaneSlot::Right, + window, + cx, + ) + }) + .ok(); + }), + ) + .into_any_element() + }) + }; + let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight) .icon_size(IconSize::Small) .on_click({ @@ -3103,7 +3182,44 @@ impl Pane { let unpinned_tabs = tab_items.split_off(self.pinned_tab_count); let pinned_tabs = tab_items; + let render_aside_toggle_left = cx.has_flag::() + && self + .is_upper_left + .then(|| { + self.workspace.upgrade().and_then(|entity| { + let workspace = entity.read(cx); + workspace + .utility_pane(UtilityPaneSlot::Left) + .map(|pane| !pane.expanded(cx)) + }) + }) + .flatten() + .unwrap_or(false); + + let render_aside_toggle_right = cx.has_flag::() + && self + .is_upper_right + .then(|| { + self.workspace.upgrade().and_then(|entity| { + let workspace = entity.read(cx); + workspace + .utility_pane(UtilityPaneSlot::Right) + .map(|pane| !pane.expanded(cx)) + }) + }) + .flatten() + .unwrap_or(false); + TabBar::new("tab_bar") + .map(|tab_bar| { + if let Some(open_aside_left) = open_aside_left + && render_aside_toggle_left + { + tab_bar.start_child(open_aside_left) + } else { + tab_bar + } + }) .when( self.display_nav_history_buttons.unwrap_or_default(), |tab_bar| { @@ -3196,6 +3312,15 @@ impl Pane { })), ), ) + .map(|tab_bar| { + if let Some(open_aside_right) = open_aside_right + && render_aside_toggle_right + { + tab_bar.end_child(open_aside_right) + } else { + tab_bar + } + }) .into_any_element() } @@ -6659,13 +6784,13 @@ mod tests { let tab_bar_scroll_handle = pane.update_in(cx, |pane, _window, _cx| pane.tab_bar_scroll_handle.clone()); assert_eq!(tab_bar_scroll_handle.children_count(), 6); - let tab_bounds = cx.debug_bounds("TAB-3").unwrap(); + let tab_bounds = cx.debug_bounds("TAB-4").unwrap(); let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap(); let scroll_bounds = tab_bar_scroll_handle.bounds(); let scroll_offset = tab_bar_scroll_handle.offset(); - assert!(tab_bounds.right() <= scroll_bounds.right() + scroll_offset.x); - // -39.5 is the magic number for this setup - assert_eq!(scroll_offset.x, px(-39.5)); + assert!(tab_bounds.right() <= scroll_bounds.right()); + // -43.0 is the magic number for this setup + assert_eq!(scroll_offset.x, px(-43.0)); assert!( !tab_bounds.intersects(&new_tab_button_bounds), "Tab should not overlap with the new tab button, if this is failing check if there's been a redesign!" diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index c9d98977139ed644cf5f3bfb7eb26d94ca081d19..393ed74e30c9c34bf7cdb22aabf2de2d05aa84f8 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -28,6 +28,7 @@ const VERTICAL_MIN_SIZE: f32 = 100.; #[derive(Clone)] pub struct PaneGroup { pub root: Member, + pub is_center: bool, } pub struct PaneRenderResult { @@ -37,22 +38,31 @@ pub struct PaneRenderResult { impl PaneGroup { pub fn with_root(root: Member) -> Self { - Self { root } + Self { + root, + is_center: false, + } } pub fn new(pane: Entity) -> Self { Self { root: Member::Pane(pane), + is_center: false, } } + pub fn set_is_center(&mut self, is_center: bool) { + self.is_center = is_center; + } + pub fn split( &mut self, old_pane: &Entity, new_pane: &Entity, direction: SplitDirection, + cx: &mut App, ) -> Result<()> { - match &mut self.root { + let result = match &mut self.root { Member::Pane(pane) => { if pane == old_pane { self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction); @@ -62,7 +72,11 @@ impl PaneGroup { } } Member::Axis(axis) => axis.split(old_pane, new_pane, direction), + }; + if result.is_ok() { + self.mark_positions(cx); } + result } pub fn bounding_box_for_pane(&self, pane: &Entity) -> Option> { @@ -90,6 +104,7 @@ impl PaneGroup { &mut self, active_pane: &Entity, direction: SplitDirection, + cx: &mut App, ) -> Result { if let Some(pane) = self.find_pane_at_border(direction) && pane == active_pane @@ -97,7 +112,7 @@ impl PaneGroup { return Ok(false); } - if !self.remove(active_pane)? { + if !self.remove_internal(active_pane)? { return Ok(false); } @@ -110,6 +125,7 @@ impl PaneGroup { 0 }; root.insert_pane(idx, active_pane); + self.mark_positions(cx); return Ok(true); } @@ -119,6 +135,7 @@ impl PaneGroup { vec![Member::Pane(active_pane.clone()), self.root.clone()] }; self.root = Member::Axis(PaneAxis::new(direction.axis(), members)); + self.mark_positions(cx); Ok(true) } @@ -133,7 +150,15 @@ impl PaneGroup { /// - Ok(true) if it found and removed a pane /// - Ok(false) if it found but did not remove the pane /// - Err(_) if it did not find the pane - pub fn remove(&mut self, pane: &Entity) -> Result { + pub fn remove(&mut self, pane: &Entity, cx: &mut App) -> Result { + let result = self.remove_internal(pane); + if let Ok(true) = result { + self.mark_positions(cx); + } + result + } + + fn remove_internal(&mut self, pane: &Entity) -> Result { match &mut self.root { Member::Pane(_) => Ok(false), Member::Axis(axis) => { @@ -151,6 +176,7 @@ impl PaneGroup { direction: Axis, amount: Pixels, bounds: &Bounds, + cx: &mut App, ) { match &mut self.root { Member::Pane(_) => {} @@ -158,22 +184,29 @@ impl PaneGroup { let _ = axis.resize(pane, direction, amount, bounds); } }; + self.mark_positions(cx); } - pub fn reset_pane_sizes(&mut self) { + pub fn reset_pane_sizes(&mut self, cx: &mut App) { match &mut self.root { Member::Pane(_) => {} Member::Axis(axis) => { let _ = axis.reset_pane_sizes(); } }; + self.mark_positions(cx); } - pub fn swap(&mut self, from: &Entity, to: &Entity) { + pub fn swap(&mut self, from: &Entity, to: &Entity, cx: &mut App) { match &mut self.root { Member::Pane(_) => {} Member::Axis(axis) => axis.swap(from, to), }; + self.mark_positions(cx); + } + + pub fn mark_positions(&mut self, cx: &mut App) { + self.root.mark_positions(self.is_center, true, true, cx); } pub fn render( @@ -232,8 +265,9 @@ impl PaneGroup { self.pane_at_pixel_position(target) } - pub fn invert_axies(&mut self) { + pub fn invert_axies(&mut self, cx: &mut App) { self.root.invert_pane_axies(); + self.mark_positions(cx); } } @@ -243,6 +277,43 @@ pub enum Member { Pane(Entity), } +impl Member { + pub fn mark_positions( + &mut self, + in_center_group: bool, + is_upper_left: bool, + is_upper_right: bool, + cx: &mut App, + ) { + match self { + Member::Axis(pane_axis) => { + let len = pane_axis.members.len(); + for (idx, member) in pane_axis.members.iter_mut().enumerate() { + let member_upper_left = match pane_axis.axis { + Axis::Vertical => is_upper_left && idx == 0, + Axis::Horizontal => is_upper_left && idx == 0, + }; + let member_upper_right = match pane_axis.axis { + Axis::Vertical => is_upper_right && idx == 0, + Axis::Horizontal => is_upper_right && idx == len - 1, + }; + member.mark_positions( + in_center_group, + member_upper_left, + member_upper_right, + cx, + ); + } + } + Member::Pane(entity) => entity.update(cx, |pane, _| { + pane.in_center_group = in_center_group; + pane.is_upper_left = is_upper_left; + pane.is_upper_right = is_upper_right; + }), + } + } +} + #[derive(Clone, Copy)] pub struct PaneRenderContext<'a> { pub project: &'a Entity, diff --git a/crates/workspace/src/utility_pane.rs b/crates/workspace/src/utility_pane.rs new file mode 100644 index 0000000000000000000000000000000000000000..2760000216d9164367c58d41d4f1b1893dc8cd75 --- /dev/null +++ b/crates/workspace/src/utility_pane.rs @@ -0,0 +1,282 @@ +use gpui::{ + AppContext as _, EntityId, MouseButton, Pixels, Render, StatefulInteractiveElement, + Subscription, WeakEntity, deferred, px, +}; +use ui::{ + ActiveTheme as _, Context, FluentBuilder as _, InteractiveElement as _, IntoElement, + ParentElement as _, RenderOnce, Styled as _, Window, div, +}; + +use crate::{ + DockPosition, Workspace, + dock::{ClosePane, MinimizePane, UtilityPane, UtilityPaneHandle}, +}; + +pub(crate) const UTILITY_PANE_RESIZE_HANDLE_SIZE: Pixels = px(6.0); +pub(crate) const UTILITY_PANE_MIN_WIDTH: Pixels = px(20.0); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum UtilityPaneSlot { + Left, + Right, +} + +struct UtilityPaneSlotState { + panel_id: EntityId, + utility_pane: Box, + _subscriptions: Vec, +} + +#[derive(Default)] +pub struct UtilityPaneState { + left_slot: Option, + right_slot: Option, +} + +#[derive(Clone)] +pub struct DraggedUtilityPane(pub UtilityPaneSlot); + +impl Render for DraggedUtilityPane { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + gpui::Empty + } +} + +pub fn utility_slot_for_dock_position(position: DockPosition) -> UtilityPaneSlot { + match position { + DockPosition::Left => UtilityPaneSlot::Left, + DockPosition::Right => UtilityPaneSlot::Right, + DockPosition::Bottom => UtilityPaneSlot::Left, + } +} + +impl Workspace { + pub fn utility_pane(&self, slot: UtilityPaneSlot) -> Option<&dyn UtilityPaneHandle> { + match slot { + UtilityPaneSlot::Left => self + .utility_panes + .left_slot + .as_ref() + .map(|s| s.utility_pane.as_ref()), + UtilityPaneSlot::Right => self + .utility_panes + .right_slot + .as_ref() + .map(|s| s.utility_pane.as_ref()), + } + } + + pub fn toggle_utility_pane( + &mut self, + slot: UtilityPaneSlot, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(handle) = self.utility_pane(slot) { + let current = handle.expanded(cx); + handle.set_expanded(!current, cx); + } + cx.notify(); + self.serialize_workspace(window, cx); + } + + pub fn register_utility_pane( + &mut self, + slot: UtilityPaneSlot, + panel_id: EntityId, + handle: gpui::Entity, + cx: &mut Context, + ) { + let minimize_subscription = + cx.subscribe(&handle, move |this, _, _event: &MinimizePane, cx| { + if let Some(handle) = this.utility_pane(slot) { + handle.set_expanded(false, cx); + } + cx.notify(); + }); + + let close_subscription = cx.subscribe(&handle, move |this, _, _event: &ClosePane, cx| { + this.clear_utility_pane(slot, cx); + }); + + let subscriptions = vec![minimize_subscription, close_subscription]; + let boxed_handle: Box = Box::new(handle); + + match slot { + UtilityPaneSlot::Left => { + self.utility_panes.left_slot = Some(UtilityPaneSlotState { + panel_id, + utility_pane: boxed_handle, + _subscriptions: subscriptions, + }); + } + UtilityPaneSlot::Right => { + self.utility_panes.right_slot = Some(UtilityPaneSlotState { + panel_id, + utility_pane: boxed_handle, + _subscriptions: subscriptions, + }); + } + } + cx.notify(); + } + + pub fn clear_utility_pane(&mut self, slot: UtilityPaneSlot, cx: &mut Context) { + match slot { + UtilityPaneSlot::Left => { + self.utility_panes.left_slot = None; + } + UtilityPaneSlot::Right => { + self.utility_panes.right_slot = None; + } + } + cx.notify(); + } + + pub fn clear_utility_pane_if_provider( + &mut self, + slot: UtilityPaneSlot, + provider_panel_id: EntityId, + cx: &mut Context, + ) { + let should_clear = match slot { + UtilityPaneSlot::Left => self + .utility_panes + .left_slot + .as_ref() + .is_some_and(|slot| slot.panel_id == provider_panel_id), + UtilityPaneSlot::Right => self + .utility_panes + .right_slot + .as_ref() + .is_some_and(|slot| slot.panel_id == provider_panel_id), + }; + + if should_clear { + self.clear_utility_pane(slot, cx); + } + } + + pub fn resize_utility_pane( + &mut self, + slot: UtilityPaneSlot, + new_width: Pixels, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(handle) = self.utility_pane(slot) { + let max_width = self.max_utility_pane_width(window, cx); + let width = new_width.max(UTILITY_PANE_MIN_WIDTH).min(max_width); + handle.set_width(Some(width), cx); + cx.notify(); + self.serialize_workspace(window, cx); + } + } + + pub fn reset_utility_pane_width( + &mut self, + slot: UtilityPaneSlot, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(handle) = self.utility_pane(slot) { + handle.set_width(None, cx); + cx.notify(); + self.serialize_workspace(window, cx); + } + } +} + +#[derive(IntoElement)] +pub struct UtilityPaneFrame { + workspace: WeakEntity, + slot: UtilityPaneSlot, + handle: Box, +} + +impl UtilityPaneFrame { + pub fn new( + slot: UtilityPaneSlot, + handle: Box, + cx: &mut Context, + ) -> Self { + let workspace = cx.weak_entity(); + Self { + workspace, + slot, + handle, + } + } +} + +impl RenderOnce for UtilityPaneFrame { + fn render(self, _window: &mut Window, cx: &mut ui::App) -> impl IntoElement { + let workspace = self.workspace.clone(); + let slot = self.slot; + let width = self.handle.width(cx); + + let create_resize_handle = || { + let workspace_handle = workspace.clone(); + let handle = div() + .id(match slot { + UtilityPaneSlot::Left => "utility-pane-resize-handle-left", + UtilityPaneSlot::Right => "utility-pane-resize-handle-right", + }) + .on_drag(DraggedUtilityPane(slot), move |pane, _, _, cx| { + cx.stop_propagation(); + cx.new(|_| pane.clone()) + }) + .on_mouse_down(MouseButton::Left, move |_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up( + MouseButton::Left, + move |e: &gpui::MouseUpEvent, window, cx| { + if e.click_count == 2 { + workspace_handle + .update(cx, |workspace, cx| { + workspace.reset_utility_pane_width(slot, window, cx); + }) + .ok(); + cx.stop_propagation(); + } + }, + ) + .occlude(); + + match slot { + UtilityPaneSlot::Left => deferred( + handle + .absolute() + .right(-UTILITY_PANE_RESIZE_HANDLE_SIZE / 2.) + .top(px(0.)) + .h_full() + .w(UTILITY_PANE_RESIZE_HANDLE_SIZE) + .cursor_col_resize(), + ), + UtilityPaneSlot::Right => deferred( + handle + .absolute() + .left(-UTILITY_PANE_RESIZE_HANDLE_SIZE / 2.) + .top(px(0.)) + .h_full() + .w(UTILITY_PANE_RESIZE_HANDLE_SIZE) + .cursor_col_resize(), + ), + } + }; + + div() + .h_full() + .bg(cx.theme().colors().tab_bar_background) + .w(width) + .border_color(cx.theme().colors().border) + .when(self.slot == UtilityPaneSlot::Left, |this| this.border_r_1()) + .when(self.slot == UtilityPaneSlot::Right, |this| { + this.border_l_1() + }) + .child(create_resize_handle()) + .child(self.handle.to_any()) + .into_any_element() + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ec557187e0852355dbd9f083397fda17d2ddbf42..0dbe8371247db6b3577e880bb88345428476747d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -16,6 +16,7 @@ pub mod tasks; mod theme_preview; mod toast_layer; mod toolbar; +pub mod utility_pane; mod workspace_settings; pub use crate::notifications::NotificationFrame; @@ -31,6 +32,7 @@ use client::{ }; use collections::{HashMap, HashSet, hash_map}; use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; +use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt}; use futures::{ Future, FutureExt, StreamExt, channel::{ @@ -127,11 +129,16 @@ pub use workspace_settings::{ }; use zed_actions::{Spawn, feedback::FileBugReport}; -use crate::persistence::{ - SerializedAxis, - model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup}, +use crate::{ + item::ItemBufferKind, notifications::NotificationId, utility_pane::UTILITY_PANE_MIN_WIDTH, +}; +use crate::{ + persistence::{ + SerializedAxis, + model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup}, + }, + utility_pane::{DraggedUtilityPane, UtilityPaneFrame, UtilityPaneSlot, UtilityPaneState}, }; -use crate::{item::ItemBufferKind, notifications::NotificationId}; pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200); @@ -570,44 +577,43 @@ pub fn init(app_state: Arc, cx: &mut App) { toast_layer::init(cx); history_manager::init(cx); - cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx)); - cx.on_action(|_: &Reload, cx| reload(cx)); - - cx.on_action({ - let app_state = Arc::downgrade(&app_state); - move |_: &Open, cx: &mut App| { - if let Some(app_state) = app_state.upgrade() { - prompt_and_open_paths( - app_state, - PathPromptOptions { - files: true, - directories: true, - multiple: true, - prompt: None, - }, - cx, - ); + cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx)) + .on_action(|_: &Reload, cx| reload(cx)) + .on_action({ + let app_state = Arc::downgrade(&app_state); + move |_: &Open, cx: &mut App| { + if let Some(app_state) = app_state.upgrade() { + prompt_and_open_paths( + app_state, + PathPromptOptions { + files: true, + directories: true, + multiple: true, + prompt: None, + }, + cx, + ); + } } - } - }); - cx.on_action({ - let app_state = Arc::downgrade(&app_state); - move |_: &OpenFiles, cx: &mut App| { - let directories = cx.can_select_mixed_files_and_dirs(); - if let Some(app_state) = app_state.upgrade() { - prompt_and_open_paths( - app_state, - PathPromptOptions { - files: true, - directories, - multiple: true, - prompt: None, - }, - cx, - ); + }) + .on_action({ + let app_state = Arc::downgrade(&app_state); + move |_: &OpenFiles, cx: &mut App| { + let directories = cx.can_select_mixed_files_and_dirs(); + if let Some(app_state) = app_state.upgrade() { + prompt_and_open_paths( + app_state, + PathPromptOptions { + files: true, + directories, + multiple: true, + prompt: None, + }, + cx, + ); + } } - } - }); + }); } type BuildProjectItemFn = @@ -1176,6 +1182,7 @@ pub struct Workspace { scheduled_tasks: Vec>, last_open_dock_positions: Vec, removing: bool, + utility_panes: UtilityPaneState, } impl EventEmitter for Workspace {} @@ -1467,12 +1474,17 @@ impl Workspace { this.update_window_title(window, cx); this.show_initial_notifications(cx); }); + + let mut center = PaneGroup::new(center_pane.clone()); + center.set_is_center(true); + center.mark_positions(cx); + Workspace { weak_self: weak_handle.clone(), zoomed: None, zoomed_position: None, previous_dock_drag_coordinates: None, - center: PaneGroup::new(center_pane.clone()), + center, panes: vec![center_pane.clone()], panes_by_item: Default::default(), active_pane: center_pane.clone(), @@ -1520,6 +1532,7 @@ impl Workspace { scheduled_tasks: Vec::new(), last_open_dock_positions: Vec::new(), removing: false, + utility_panes: UtilityPaneState::default(), } } @@ -3772,7 +3785,7 @@ impl Workspace { let new_pane = self.add_pane(window, cx); if self .center - .split(&split_off_pane, &new_pane, direction) + .split(&split_off_pane, &new_pane, direction, cx) .log_err() .is_none() { @@ -3957,7 +3970,7 @@ impl Workspace { let new_pane = self.add_pane(window, cx); if self .center - .split(&self.active_pane, &new_pane, action.direction) + .split(&self.active_pane, &new_pane, action.direction, cx) .log_err() .is_none() { @@ -4011,7 +4024,7 @@ impl Workspace { pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context) { if let Some(to) = self.find_pane_in_direction(direction, cx) { - self.center.swap(&self.active_pane, &to); + self.center.swap(&self.active_pane, &to, cx); cx.notify(); } } @@ -4019,7 +4032,7 @@ impl Workspace { pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context) { if self .center - .move_to_border(&self.active_pane, direction) + .move_to_border(&self.active_pane, direction, cx) .unwrap() { cx.notify(); @@ -4049,13 +4062,13 @@ impl Workspace { } } else { self.center - .resize(&self.active_pane, axis, amount, &self.bounds); + .resize(&self.active_pane, axis, amount, &self.bounds, cx); } cx.notify(); } pub fn reset_pane_sizes(&mut self, cx: &mut Context) { - self.center.reset_pane_sizes(); + self.center.reset_pane_sizes(cx); cx.notify(); } @@ -4241,7 +4254,7 @@ impl Workspace { ) -> Entity { let new_pane = self.add_pane(window, cx); self.center - .split(&pane_to_split, &new_pane, split_direction) + .split(&pane_to_split, &new_pane, split_direction, cx) .unwrap(); cx.notify(); new_pane @@ -4261,7 +4274,7 @@ impl Workspace { new_pane.update(cx, |pane, cx| { pane.add_item(item, true, true, None, window, cx) }); - self.center.split(&pane, &new_pane, direction).unwrap(); + self.center.split(&pane, &new_pane, direction, cx).unwrap(); cx.notify(); } @@ -4286,7 +4299,7 @@ impl Workspace { new_pane.update(cx, |pane, cx| { pane.add_item(clone, true, true, None, window, cx) }); - this.center.split(&pane, &new_pane, direction).unwrap(); + this.center.split(&pane, &new_pane, direction, cx).unwrap(); cx.notify(); new_pane }) @@ -4333,7 +4346,7 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) { - if self.center.remove(&pane).unwrap() { + if self.center.remove(&pane, cx).unwrap() { self.force_remove_pane(&pane, &focus_on, window, cx); self.unfollow_in_pane(&pane, window, cx); self.last_leaders_by_pane.remove(&pane.downgrade()); @@ -5685,6 +5698,9 @@ impl Workspace { // Swap workspace center group workspace.center = PaneGroup::with_root(center_group); + workspace.center.set_is_center(true); + workspace.center.mark_positions(cx); + if let Some(active_pane) = active_pane { workspace.set_active_pane(&active_pane, window, cx); cx.focus_self(window); @@ -6310,6 +6326,7 @@ impl Workspace { left_dock.resize_active_panel(Some(size), window, cx); } }); + self.clamp_utility_pane_widths(window, cx); } fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) { @@ -6332,6 +6349,7 @@ impl Workspace { right_dock.resize_active_panel(Some(size), window, cx); } }); + self.clamp_utility_pane_widths(window, cx); } fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) { @@ -6346,6 +6364,42 @@ impl Workspace { bottom_dock.resize_active_panel(Some(size), window, cx); } }); + self.clamp_utility_pane_widths(window, cx); + } + + fn max_utility_pane_width(&self, window: &Window, cx: &App) -> Pixels { + let left_dock_width = self + .left_dock + .read(cx) + .active_panel_size(window, cx) + .unwrap_or(px(0.0)); + let right_dock_width = self + .right_dock + .read(cx) + .active_panel_size(window, cx) + .unwrap_or(px(0.0)); + let center_pane_width = self.bounds.size.width - left_dock_width - right_dock_width; + center_pane_width - px(10.0) + } + + fn clamp_utility_pane_widths(&mut self, window: &mut Window, cx: &mut App) { + let max_width = self.max_utility_pane_width(window, cx); + + // Clamp left slot utility pane if it exists + if let Some(handle) = self.utility_pane(UtilityPaneSlot::Left) { + let current_width = handle.width(cx); + if current_width > max_width { + handle.set_width(Some(max_width.max(UTILITY_PANE_MIN_WIDTH)), cx); + } + } + + // Clamp right slot utility pane if it exists + if let Some(handle) = self.utility_pane(UtilityPaneSlot::Right) { + let current_width = handle.width(cx); + if current_width > max_width { + handle.set_width(Some(max_width.max(UTILITY_PANE_MIN_WIDTH)), cx); + } + } } fn toggle_edit_predictions_all_files( @@ -6813,6 +6867,34 @@ impl Render for Workspace { } }, )) + .on_drag_move(cx.listener( + move |workspace, + e: &DragMoveEvent, + window, + cx| { + let slot = e.drag(cx).0; + match slot { + UtilityPaneSlot::Left => { + let left_dock_width = workspace.left_dock.read(cx) + .active_panel_size(window, cx) + .unwrap_or(gpui::px(0.0)); + let new_width = e.event.position.x + - workspace.bounds.left() + - left_dock_width; + workspace.resize_utility_pane(slot, new_width, window, cx); + } + UtilityPaneSlot::Right => { + let right_dock_width = workspace.right_dock.read(cx) + .active_panel_size(window, cx) + .unwrap_or(gpui::px(0.0)); + let new_width = workspace.bounds.right() + - e.event.position.x + - right_dock_width; + workspace.resize_utility_pane(slot, new_width, window, cx); + } + } + }, + )) }) .child({ match bottom_dock_layout { @@ -6832,6 +6914,15 @@ impl Render for Workspace { window, cx, )) + .when(cx.has_flag::(), |this| { + this.when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| { + this.when(pane.expanded(cx), |this| { + this.child( + UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx) + ) + }) + }) + }) .child( div() .flex() @@ -6873,6 +6964,15 @@ impl Render for Workspace { ), ), ) + .when(cx.has_flag::(), |this| { + this.when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| { + this.when(pane.expanded(cx), |this| { + this.child( + UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx) + ) + }) + }) + }) .children(self.render_dock( DockPosition::Right, &self.right_dock, @@ -6903,6 +7003,15 @@ impl Render for Workspace { .flex_row() .flex_1() .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx)) + .when(cx.has_flag::(), |this| { + this.when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| { + this.when(pane.expanded(cx), |this| { + this.child( + UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx) + ) + }) + }) + }) .child( div() .flex() @@ -6930,6 +7039,13 @@ impl Render for Workspace { .when_some(paddings.1, |this, p| this.child(p.border_l_1())), ) ) + .when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| { + this.when(pane.expanded(cx), |this| { + this.child( + UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx) + ) + }) + }) ) .child( div() @@ -6954,6 +7070,15 @@ impl Render for Workspace { window, cx, )) + .when(cx.has_flag::(), |this| { + this.when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| { + this.when(pane.expanded(cx), |this| { + this.child( + UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx) + ) + }) + }) + }) .child( div() .flex() @@ -6992,6 +7117,15 @@ impl Render for Workspace { .when_some(paddings.1, |this, p| this.child(p.border_l_1())), ) ) + .when(cx.has_flag::(), |this| { + this.when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| { + this.when(pane.expanded(cx), |this| { + this.child( + UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx) + ) + }) + }) + }) .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx)) ) .child( @@ -7011,6 +7145,13 @@ impl Render for Workspace { window, cx, )) + .when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| { + this.when(pane.expanded(cx), |this| { + this.child( + UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx) + ) + }) + }) .child( div() .flex() @@ -7048,6 +7189,15 @@ impl Render for Workspace { cx, )), ) + .when(cx.has_flag::(), |this| { + this.when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| { + this.when(pane.expanded(cx), |this| { + this.child( + UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx) + ) + }) + }) + }) .children(self.render_dock( DockPosition::Right, &self.right_dock, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 92a274da9640bbe9ee3afefeacd7566c853bdd2d..141de1139fb571020377ef9b115ed8204bad100b 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -26,6 +26,7 @@ acp_tools.workspace = true activity_indicator.workspace = true agent_settings.workspace = true agent_ui.workspace = true +agent_ui_v2.workspace = true anyhow.workspace = true askpass.workspace = true assets.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 41893ab94d754a64d37d0701ea33e705e144c55a..654356e0c49997643b3f498205b52b089fcd92ab 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -601,6 +601,7 @@ pub fn main() { false, cx, ); + agent_ui_v2::agents_panel::init(cx); repl::init(app_state.fs.clone(), cx); recent_projects::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 71653124b1c4af993d9878b2b689d07f4f2acd02..ed22d7ef510e367b71b2a1057513471a4e32306a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -10,6 +10,7 @@ mod quick_action_bar; pub(crate) mod windows_only_instance; use agent_ui::{AgentDiffToolbar, AgentPanelDelegate}; +use agent_ui_v2::agents_panel::AgentsPanel; use anyhow::Context as _; pub use app_menus::*; use assets::Assets; @@ -81,8 +82,9 @@ use vim_mode_setting::VimModeSetting; use workspace::notifications::{ NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification, }; +use workspace::utility_pane::utility_slot_for_dock_position; use workspace::{ - AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, + AppState, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace, WorkspaceSettings, create_and_open_local_file, notifications::simple_message_notification::MessageNotification, open_new, }; @@ -159,15 +161,15 @@ pub fn init(cx: &mut App) { || flag.await { cx.update(|cx| { - cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action")); - cx.on_action(|_: &TestCrash, _| { - unsafe extern "C" { - fn puts(s: *const i8); - } - unsafe { - puts(0xabad1d3a as *const i8); - } - }); + cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action")) + .on_action(|_: &TestCrash, _| { + unsafe extern "C" { + fn puts(s: *const i8); + } + unsafe { + puts(0xabad1d3a as *const i8); + } + }); }) .ok(); }; @@ -177,11 +179,11 @@ pub fn init(cx: &mut App) { with_active_or_new_workspace(cx, |workspace, window, cx| { open_log_file(workspace, window, cx); }); - }); - cx.on_action(|_: &workspace::RevealLogInFileManager, cx| { + }) + .on_action(|_: &workspace::RevealLogInFileManager, cx| { cx.reveal_path(paths::log_file().as_path()); - }); - cx.on_action(|_: &zed_actions::OpenLicenses, cx| { + }) + .on_action(|_: &zed_actions::OpenLicenses, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { open_bundled_file( workspace, @@ -192,13 +194,13 @@ pub fn init(cx: &mut App) { cx, ); }); - }); - cx.on_action(|_: &zed_actions::OpenTelemetryLog, cx| { + }) + .on_action(|_: &zed_actions::OpenTelemetryLog, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { open_telemetry_log_file(workspace, window, cx); }); - }); - cx.on_action(|&zed_actions::OpenKeymapFile, cx| { + }) + .on_action(|&zed_actions::OpenKeymapFile, cx| { with_active_or_new_workspace(cx, |_, window, cx| { open_settings_file( paths::keymap_file(), @@ -207,8 +209,8 @@ pub fn init(cx: &mut App) { cx, ); }); - }); - cx.on_action(|_: &OpenSettingsFile, cx| { + }) + .on_action(|_: &OpenSettingsFile, cx| { with_active_or_new_workspace(cx, |_, window, cx| { open_settings_file( paths::settings_file(), @@ -217,13 +219,13 @@ pub fn init(cx: &mut App) { cx, ); }); - }); - cx.on_action(|_: &OpenAccountSettings, cx| { + }) + .on_action(|_: &OpenAccountSettings, cx| { with_active_or_new_workspace(cx, |_, _, cx| { cx.open_url(&zed_urls::account_url(cx)); }); - }); - cx.on_action(|_: &OpenTasks, cx| { + }) + .on_action(|_: &OpenTasks, cx| { with_active_or_new_workspace(cx, |_, window, cx| { open_settings_file( paths::tasks_file(), @@ -232,8 +234,8 @@ pub fn init(cx: &mut App) { cx, ); }); - }); - cx.on_action(|_: &OpenDebugTasks, cx| { + }) + .on_action(|_: &OpenDebugTasks, cx| { with_active_or_new_workspace(cx, |_, window, cx| { open_settings_file( paths::debug_scenarios_file(), @@ -242,8 +244,8 @@ pub fn init(cx: &mut App) { cx, ); }); - }); - cx.on_action(|_: &OpenDefaultSettings, cx| { + }) + .on_action(|_: &OpenDefaultSettings, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { open_bundled_file( workspace, @@ -254,8 +256,8 @@ pub fn init(cx: &mut App) { cx, ); }); - }); - cx.on_action(|_: &zed_actions::OpenDefaultKeymap, cx| { + }) + .on_action(|_: &zed_actions::OpenDefaultKeymap, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { open_bundled_file( workspace, @@ -266,8 +268,8 @@ pub fn init(cx: &mut App) { cx, ); }); - }); - cx.on_action(|_: &zed_actions::About, cx| { + }) + .on_action(|_: &zed_actions::About, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { about(workspace, window, cx); }); @@ -679,7 +681,8 @@ fn initialize_panels( add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()), add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()), add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()), - initialize_agent_panel(workspace_handle, prompt_builder, cx.clone()).map(|r| r.log_err()) + initialize_agent_panel(workspace_handle.clone(), prompt_builder, cx.clone()).map(|r| r.log_err()), + initialize_agents_panel(workspace_handle, cx.clone()).map(|r| r.log_err()) ); anyhow::Ok(()) @@ -687,58 +690,65 @@ fn initialize_panels( .detach(); } +fn setup_or_teardown_ai_panel( + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + load_panel: impl FnOnce( + WeakEntity, + AsyncWindowContext, + ) -> Task>> + + 'static, +) -> Task> { + let disable_ai = SettingsStore::global(cx) + .get::(None) + .disable_ai + || cfg!(test); + let existing_panel = workspace.panel::

(cx); + + match (disable_ai, existing_panel) { + (false, None) => cx.spawn_in(window, async move |workspace, cx| { + let panel = load_panel(workspace.clone(), cx.clone()).await?; + workspace.update_in(cx, |workspace, window, cx| { + let disable_ai = SettingsStore::global(cx) + .get::(None) + .disable_ai; + let have_panel = workspace.panel::

(cx).is_some(); + if !disable_ai && !have_panel { + workspace.add_panel(panel, window, cx); + } + }) + }), + (true, Some(existing_panel)) => { + workspace.remove_panel::

(&existing_panel, window, cx); + Task::ready(Ok(())) + } + _ => Task::ready(Ok(())), + } +} + async fn initialize_agent_panel( workspace_handle: WeakEntity, prompt_builder: Arc, mut cx: AsyncWindowContext, ) -> anyhow::Result<()> { - fn setup_or_teardown_agent_panel( - workspace: &mut Workspace, - prompt_builder: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let disable_ai = SettingsStore::global(cx) - .get::(None) - .disable_ai - || cfg!(test); - let existing_panel = workspace.panel::(cx); - match (disable_ai, existing_panel) { - (false, None) => cx.spawn_in(window, async move |workspace, cx| { - let panel = - agent_ui::AgentPanel::load(workspace.clone(), prompt_builder, cx.clone()) - .await?; - workspace.update_in(cx, |workspace, window, cx| { - let disable_ai = SettingsStore::global(cx) - .get::(None) - .disable_ai; - let have_panel = workspace.panel::(cx).is_some(); - if !disable_ai && !have_panel { - workspace.add_panel(panel, window, cx); - } - }) - }), - (true, Some(existing_panel)) => { - workspace.remove_panel::(&existing_panel, window, cx); - Task::ready(Ok(())) - } - _ => Task::ready(Ok(())), - } - } - workspace_handle .update_in(&mut cx, |workspace, window, cx| { - setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx) + let prompt_builder = prompt_builder.clone(); + setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| { + agent_ui::AgentPanel::load(workspace, prompt_builder, cx) + }) })? .await?; workspace_handle.update_in(&mut cx, |workspace, window, cx| { - cx.observe_global_in::(window, { + let prompt_builder = prompt_builder.clone(); + cx.observe_global_in::(window, move |workspace, window, cx| { let prompt_builder = prompt_builder.clone(); - move |workspace, window, cx| { - setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx) - .detach_and_log_err(cx); - } + setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| { + agent_ui::AgentPanel::load(workspace, prompt_builder, cx) + }) + .detach_and_log_err(cx); }) .detach(); @@ -763,6 +773,31 @@ async fn initialize_agent_panel( anyhow::Ok(()) } +async fn initialize_agents_panel( + workspace_handle: WeakEntity, + mut cx: AsyncWindowContext, +) -> anyhow::Result<()> { + workspace_handle + .update_in(&mut cx, |workspace, window, cx| { + setup_or_teardown_ai_panel(workspace, window, cx, |workspace, cx| { + AgentsPanel::load(workspace, cx) + }) + })? + .await?; + + workspace_handle.update_in(&mut cx, |_workspace, window, cx| { + cx.observe_global_in::(window, move |workspace, window, cx| { + setup_or_teardown_ai_panel(workspace, window, cx, |workspace, cx| { + AgentsPanel::load(workspace, cx) + }) + .detach_and_log_err(cx); + }) + .detach(); + })?; + + anyhow::Ok(()) +} + fn register_actions( app_state: Arc, workspace: &mut Workspace, @@ -1052,6 +1087,18 @@ fn register_actions( workspace.toggle_panel_focus::(window, cx); }, ) + .register_action( + |workspace: &mut Workspace, + _: &zed_actions::agent::ToggleAgentPane, + window: &mut Window, + cx: &mut Context| { + if let Some(panel) = workspace.panel::(cx) { + let position = panel.read(cx).position(window, cx); + let slot = utility_slot_for_dock_position(position); + workspace.toggle_utility_pane(slot, window, cx); + } + }, + ) .register_action({ let app_state = Arc::downgrade(&app_state); move |_, _: &NewWindow, _, cx| { @@ -4714,6 +4761,7 @@ mod tests { "action", "activity_indicator", "agent", + "agents", #[cfg(not(target_os = "macos"))] "app_menu", "assistant", @@ -4941,6 +4989,7 @@ mod tests { false, cx, ); + agent_ui_v2::agents_panel::init(cx); repl::init(app_state.fs.clone(), cx); repl::notebook::init(cx); tasks_ui::init(cx); diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index a89e943e021e79058953de46bca57713f51598bc..f69baa03b002fdcac5207f977a23cfc924283e2d 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -350,6 +350,8 @@ pub mod agent { AddSelectionToThread, /// Resets the agent panel zoom levels (agent UI and buffer font sizes). ResetAgentZoom, + /// Toggles the utility/agent pane open/closed state. + ToggleAgentPane, ] ); } diff --git a/crates/zed_env_vars/src/zed_env_vars.rs b/crates/zed_env_vars/src/zed_env_vars.rs index 53b9c22bb207e81831d1d9ae6087d1a297331d3f..e601cc9536602ac943bd76bf1bfd8b8ac8979dd9 100644 --- a/crates/zed_env_vars/src/zed_env_vars.rs +++ b/crates/zed_env_vars/src/zed_env_vars.rs @@ -5,6 +5,7 @@ use std::sync::LazyLock; /// When true, Zed will use in-memory databases instead of persistent storage. pub static ZED_STATELESS: LazyLock = bool_env_var!("ZED_STATELESS"); +#[derive(Clone)] pub struct EnvVar { pub name: SharedString, /// Value of the environment variable. Also `None` when set to an empty string. @@ -30,7 +31,7 @@ impl EnvVar { #[macro_export] macro_rules! env_var { ($name:expr) => { - LazyLock::new(|| $crate::EnvVar::new(($name).into())) + ::std::sync::LazyLock::new(|| $crate::EnvVar::new(($name).into())) }; } @@ -39,6 +40,6 @@ macro_rules! env_var { #[macro_export] macro_rules! bool_env_var { ($name:expr) => { - LazyLock::new(|| $crate::EnvVar::new(($name).into()).value.is_some()) + ::std::sync::LazyLock::new(|| $crate::EnvVar::new(($name).into()).value.is_some()) }; } diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 63ad054bfae1a2388d4283546842a0b246ab95d9..c1ee463dee7ffe76aefee18022ba12df49a18dac 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -4306,6 +4306,7 @@ Run the {#action theme_selector::Toggle} action in the command palette to see a "show_project_items": true, "show_onboarding_banner": true, "show_user_picture": true, + "show_user_menu": true, "show_sign_in": true, "show_menus": false } @@ -4318,6 +4319,7 @@ Run the {#action theme_selector::Toggle} action in the command palette to see a - `show_project_items`: Whether to show the project host and name in the titlebar - `show_onboarding_banner`: Whether to show onboarding banners in the titlebar - `show_user_picture`: Whether to show user picture in the titlebar +- `show_user_menu`: Whether to show the user menu button in the titlebar (the one that displays your avatar by default and contains options like Settings, Keymap, Themes, etc.) - `show_sign_in`: Whether to show the sign in button in the titlebar - `show_menus`: Whether to show the menus in the titlebar diff --git a/docs/src/languages/cpp.md b/docs/src/languages/cpp.md index c20dd58335caca45a6923cc0527605d6cc4b5564..629a0ab640e245bdfec41370fa966589728c2c94 100644 --- a/docs/src/languages/cpp.md +++ b/docs/src/languages/cpp.md @@ -158,3 +158,26 @@ You can use CodeLLDB or GDB to debug native binaries. (Make sure that your build } ] ``` + +## Protocol Extensions + +Zed currently implements the following `clangd` [extensions](https://clangd.llvm.org/extensions): + +### Inactive Regions + +Automatically dims inactive sections of code due to preprocessor directives, such as `#if`, `#ifdef`, or `#ifndef` blocks that evaluate to false. + +### Switch Between Source and Header Files + +Allows switching between corresponding C++ source files (e.g., `.cpp`) and header files (e.g., `.h`). +by running the command {#action editor::SwitchSourceHeader} from the command palette or by setting +a keybinding for the `editor::SwitchSourceHeader` action. + +```json [settings] +{ + "context": "Editor", + "bindings": { + "alt-enter": "editor::SwitchSourceHeader" + } +} +``` diff --git a/docs/src/languages/python.md b/docs/src/languages/python.md index 5051a72209121176e05d41f57cb8d341db2ca351..2323fe2f9560cf03c586eced0052627705addcc3 100644 --- a/docs/src/languages/python.md +++ b/docs/src/languages/python.md @@ -258,6 +258,25 @@ quote-style = "single" For more details, refer to the Ruff documentation about [configuration files](https://docs.astral.sh/ruff/configuration/) and [language server settings](https://docs.astral.sh/ruff/editors/settings/), and the [list of options](https://docs.astral.sh/ruff/settings/). +### Embedded Language Highlighting + +Zed supports syntax highlighting for code embedded in Python strings by adding a comment with the language name. + +```python +# sql +query = "SELECT * FROM users" + +#sql +query = """ + SELECT * + FROM users +""" + +result = func( #sql + "SELECT * FROM users" +) +``` + ## Debugging Zed supports Python debugging through the `debugpy` adapter. You can start with no configuration or define custom launch profiles in `.zed/debug.json`. diff --git a/docs/src/vim.md b/docs/src/vim.md index c9a0cd09f2dafb9f07a26ef07b71205f5ddbdf15..09baa9b54f7e1aeb5f16777f4292131315d18928 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -471,7 +471,7 @@ But you cannot use the same shortcuts to move between all the editor docks (the } ``` -Subword motion, which allows you to navigate and select individual words in camelCase or snake_case, is not enabled by default. To enable it, add these bindings to your keymap. +Subword motion, which allows you to navigate and select individual words in `camelCase` or `snake_case`, is not enabled by default. To enable it, add these bindings to your keymap. ```json [settings] { @@ -485,6 +485,9 @@ Subword motion, which allows you to navigate and select individual words in came } ``` +> Note: Operations like `dw` remain unaffected. If you would like operations to +> also use subword motion, remove `vim_mode != operator` from the `context`. + Vim mode comes with shortcuts to surround the selection in normal mode (`ys`), but it doesn't have a shortcut to add surrounds in visual mode. By default, `shift-s` substitutes the selection (erases the text and enters insert mode). To use `shift-s` to add surrounds in visual mode, you can add the following object to your keymap. ```json [settings] @@ -566,7 +569,8 @@ You can change the following settings to modify vim mode's behavior: | use_system_clipboard | Determines how system clipboard is used:

  • "always": use for all operations
  • "never": only use when explicitly specified
  • "on_yank": use for yank operations
| "always" | | use_multiline_find | deprecated | | use_smartcase_find | If `true`, `f` and `t` motions are case-insensitive when the target letter is lowercase. | false | -| toggle_relative_line_numbers | If `true`, line numbers are relative in normal mode and absolute in insert mode, giving you the best of both options. | false | +| toggle_relative_line_numbers | deprecated | false | +| relative_line_numbers | If "enabled", line numbers are relative in normal mode and absolute in insert mode, giving you the best of both options. | "disabled" | | custom_digraphs | An object that allows you to add custom digraphs. Read below for an example. | {} | | highlight_on_yank_duration | The duration of the highlight animation(in ms). Set to `0` to disable | 200 | @@ -590,7 +594,7 @@ Here's an example of these settings changed: "default_mode": "insert", "use_system_clipboard": "never", "use_smartcase_find": true, - "toggle_relative_line_numbers": true, + "relative_line_numbers": "enabled", "highlight_on_yank_duration": 50, "custom_digraphs": { "fz": "🧟‍♀️" diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index e5185719279dde488c40573d94fd842c06860f4d..234776b1d3223a4b8634b42df1973a27c736616c 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -118,6 +118,7 @@ To disable this behavior use: "show_project_items": true, // Show/hide project host and name "show_onboarding_banner": true, // Show/hide onboarding banners "show_user_picture": true, // Show/hide user avatar + "show_user_menu": true, // Show/hide app user button "show_sign_in": true, // Show/hide sign-in button "show_menus": false // Show/hide menus }, diff --git a/extensions/anthropic/src/anthropic.rs b/extensions/anthropic/src/anthropic.rs index 253821f83dfff55e0f59b0ea96f1a159d13fa920..1a65844bff205ace82dc7034d34168902897b31b 100644 --- a/extensions/anthropic/src/anthropic.rs +++ b/extensions/anthropic/src/anthropic.rs @@ -690,6 +690,7 @@ impl zed::Extension for AnthropicProvider { id: tool_use.id, name: tool_use.name, input: tool_use.input_json, + is_input_complete: true, thought_signature: state.pending_signature.take(), }))); } diff --git a/extensions/copilot-chat/src/copilot_chat.rs b/extensions/copilot-chat/src/copilot_chat.rs index dfd0e9aa570ecc94f9cda627b6da995af297d693..866e86098badb560f76a3bbf04ec80a59e0c2391 100644 --- a/extensions/copilot-chat/src/copilot_chat.rs +++ b/extensions/copilot-chat/src/copilot_chat.rs @@ -732,6 +732,7 @@ impl zed::Extension for CopilotChatProvider { id: tc.id, name: tc.name, input: tc.arguments, + is_input_complete: true, thought_signature: None, }))); } diff --git a/extensions/google-ai/src/google_ai.rs b/extensions/google-ai/src/google_ai.rs index 259b4a8da856cf3109b9b935eff1006f6bcd6be3..76bd0164935041eb5d780e315597994965f885dc 100644 --- a/extensions/google-ai/src/google_ai.rs +++ b/extensions/google-ai/src/google_ai.rs @@ -728,6 +728,7 @@ impl zed::Extension for GoogleAiProvider { id, name: fc_part.function_call.name, input: fc_part.function_call.args.to_string(), + is_input_complete: true, thought_signature, }))); } diff --git a/extensions/html/Cargo.toml b/extensions/html/Cargo.toml index 22cdb401a7ebcf4bb6afab7702fb81f345b7aa14..2c89f86cb450b7ea8476bffdff003a94b137d213 100644 --- a/extensions/html/Cargo.toml +++ b/extensions/html/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_html" -version = "0.2.3" +version = "0.3.0" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/html/extension.toml b/extensions/html/extension.toml index 1ded7af6413d0f1990a178a19a4014caadf48240..68ab0e4b9d3f56fca17cbd518d5990edc2ec711a 100644 --- a/extensions/html/extension.toml +++ b/extensions/html/extension.toml @@ -1,7 +1,7 @@ id = "html" name = "HTML" description = "HTML support." -version = "0.2.3" +version = "0.3.0" schema_version = 1 authors = ["Isaac Clayton "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/openai/src/openai.rs b/extensions/openai/src/openai.rs index dea953a50c1c551e46866575c1e41a892537bdf6..5e8403d47d2945fe7be76e530a6a01f01a767fbd 100644 --- a/extensions/openai/src/openai.rs +++ b/extensions/openai/src/openai.rs @@ -595,6 +595,7 @@ impl zed::Extension for OpenAiProvider { id: tool_call.id, name: tool_call.name, input: tool_call.arguments, + is_input_complete: true, thought_signature: None, }))); } @@ -658,6 +659,7 @@ impl zed::Extension for OpenAiProvider { id: tool_call.id, name: tool_call.name, input: tool_call.arguments, + is_input_complete: true, thought_signature: None, }))); } diff --git a/extensions/openrouter/src/open_router.rs b/extensions/openrouter/src/open_router.rs index 81a75e4a69aac0e8df2781d5e8353df15430c8a5..13af9efa1d295002ea588c5837d53af08238bdfb 100644 --- a/extensions/openrouter/src/open_router.rs +++ b/extensions/openrouter/src/open_router.rs @@ -697,6 +697,7 @@ impl zed::Extension for OpenRouterProvider { id: tc.id, name: tc.name, input: tc.arguments, + is_input_complete: true, thought_signature: None, }))); } diff --git a/extensions/proto/Cargo.toml b/extensions/proto/Cargo.toml index 1013d62cfa085275a1230d0816049da6c35ba38a..c3606f668aa01d7a8baa20d54d073a7004a6f8c0 100644 --- a/extensions/proto/Cargo.toml +++ b/extensions/proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_proto" -version = "0.2.3" +version = "0.3.0" edition.workspace = true publish.workspace = true license = "Apache-2.0" @@ -13,4 +13,4 @@ path = "src/proto.rs" crate-type = ["cdylib"] [dependencies] -zed_extension_api = "0.1.0" +zed_extension_api = "0.7.0" diff --git a/extensions/proto/extension.toml b/extensions/proto/extension.toml index 9bb8625065fe957308c47488c4aeb9010a773984..13c4054eef083e131ab311b1ec6e5a63aff545d8 100644 --- a/extensions/proto/extension.toml +++ b/extensions/proto/extension.toml @@ -1,15 +1,24 @@ id = "proto" name = "Proto" description = "Protocol Buffers support." -version = "0.2.3" +version = "0.3.0" schema_version = 1 authors = ["Zed Industries "] repository = "https://github.com/zed-industries/zed" [grammars.proto] -repository = "https://github.com/zed-industries/tree-sitter-proto" -commit = "0848bd30a64be48772e15fbb9d5ba8c0cc5772ad" +repository = "https://github.com/coder3101/tree-sitter-proto" +commit = "a6caac94b5aa36b322b5b70040d5b67132f109d0" + + +[language_servers.buf] +name = "Buf" +languages = ["Proto"] [language_servers.protobuf-language-server] name = "Protobuf Language Server" languages = ["Proto"] + +[language_servers.protols] +name = "Protols" +languages = ["Proto"] diff --git a/extensions/proto/src/language_servers.rs b/extensions/proto/src/language_servers.rs new file mode 100644 index 0000000000000000000000000000000000000000..47a5e72d8aadf5d0286667148f0a7dd95fea10ba --- /dev/null +++ b/extensions/proto/src/language_servers.rs @@ -0,0 +1,8 @@ +mod buf; +mod protobuf_language_server; +mod protols; +mod util; + +pub(crate) use buf::*; +pub(crate) use protobuf_language_server::*; +pub(crate) use protols::*; diff --git a/extensions/proto/src/language_servers/buf.rs b/extensions/proto/src/language_servers/buf.rs new file mode 100644 index 0000000000000000000000000000000000000000..92106298d3d1deb6ed2b0f4194ab09321fa09552 --- /dev/null +++ b/extensions/proto/src/language_servers/buf.rs @@ -0,0 +1,114 @@ +use std::fs; + +use zed_extension_api::{ + self as zed, Architecture, DownloadedFileType, GithubReleaseOptions, Os, Result, + settings::LspSettings, +}; + +use crate::language_servers::util; + +pub(crate) struct BufLsp { + cached_binary_path: Option, +} + +impl BufLsp { + pub(crate) const SERVER_NAME: &str = "buf"; + + pub(crate) fn new() -> Self { + BufLsp { + cached_binary_path: None, + } + } + + pub(crate) fn language_server_binary( + &mut self, + worktree: &zed::Worktree, + ) -> Result { + let binary_settings = LspSettings::for_worktree(Self::SERVER_NAME, worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.binary); + + let args = binary_settings + .as_ref() + .and_then(|binary_settings| binary_settings.arguments.clone()) + .unwrap_or_else(|| ["lsp", "serve"].map(ToOwned::to_owned).into()); + + if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { + return Ok(zed::Command { + command: path, + args, + env: Default::default(), + }); + } else if let Some(path) = self.cached_binary_path.clone() { + return Ok(zed::Command { + command: path, + args, + env: Default::default(), + }); + } else if let Some(path) = worktree.which(Self::SERVER_NAME) { + self.cached_binary_path = Some(path.clone()); + return Ok(zed::Command { + command: path, + args, + env: Default::default(), + }); + } + + let latest_release = zed::latest_github_release( + "bufbuild/buf", + GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (os, arch) = zed::current_platform(); + + let release_suffix = match (os, arch) { + (Os::Mac, Architecture::Aarch64) => "Darwin-arm64", + (Os::Mac, Architecture::X8664) => "Darwin-x86_64", + (Os::Linux, Architecture::Aarch64) => "Linux-aarch64", + (Os::Linux, Architecture::X8664) => "Linux-x86_64", + (Os::Windows, Architecture::Aarch64) => "Windows-arm64.exe", + (Os::Windows, Architecture::X8664) => "Windows-x86_64.exe", + _ => { + return Err("Platform and architecture not supported by buf CLI".to_string()); + } + }; + + let release_name = format!("buf-{release_suffix}"); + + let version_dir = format!("{}-{}", Self::SERVER_NAME, latest_release.version); + fs::create_dir_all(&version_dir).map_err(|_| "Could not create directory")?; + + let binary_path = format!("{version_dir}/buf"); + + let download_target = latest_release + .assets + .into_iter() + .find(|asset| asset.name == release_name) + .ok_or_else(|| { + format!( + "Could not find asset with name {} in buf CLI release", + &release_name + ) + })?; + + zed::download_file( + &download_target.download_url, + &binary_path, + DownloadedFileType::Uncompressed, + )?; + zed::make_file_executable(&binary_path)?; + + util::remove_outdated_versions(Self::SERVER_NAME, &version_dir)?; + + self.cached_binary_path = Some(binary_path.clone()); + + Ok(zed::Command { + command: binary_path, + args, + env: Default::default(), + }) + } +} diff --git a/extensions/proto/src/language_servers/protobuf_language_server.rs b/extensions/proto/src/language_servers/protobuf_language_server.rs new file mode 100644 index 0000000000000000000000000000000000000000..f4b13077f73182dd0c30486ee274ade26ec1e40e --- /dev/null +++ b/extensions/proto/src/language_servers/protobuf_language_server.rs @@ -0,0 +1,52 @@ +use zed_extension_api::{self as zed, Result, settings::LspSettings}; + +pub(crate) struct ProtobufLanguageServer { + cached_binary_path: Option, +} + +impl ProtobufLanguageServer { + pub(crate) const SERVER_NAME: &str = "protobuf-language-server"; + + pub(crate) fn new() -> Self { + ProtobufLanguageServer { + cached_binary_path: None, + } + } + + pub(crate) fn language_server_binary( + &mut self, + worktree: &zed::Worktree, + ) -> Result { + let binary_settings = LspSettings::for_worktree(Self::SERVER_NAME, worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.binary); + + let args = binary_settings + .as_ref() + .and_then(|binary_settings| binary_settings.arguments.clone()) + .unwrap_or_else(|| vec!["-logs".into(), "".into()]); + + if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { + Ok(zed::Command { + command: path, + args, + env: Default::default(), + }) + } else if let Some(path) = self.cached_binary_path.clone() { + Ok(zed::Command { + command: path, + args, + env: Default::default(), + }) + } else if let Some(path) = worktree.which(Self::SERVER_NAME) { + self.cached_binary_path = Some(path.clone()); + Ok(zed::Command { + command: path, + args, + env: Default::default(), + }) + } else { + Err(format!("{} not found in PATH", Self::SERVER_NAME)) + } + } +} diff --git a/extensions/proto/src/language_servers/protols.rs b/extensions/proto/src/language_servers/protols.rs new file mode 100644 index 0000000000000000000000000000000000000000..90d365eae7d99ccb27d60f774ed700b47323d8d0 --- /dev/null +++ b/extensions/proto/src/language_servers/protols.rs @@ -0,0 +1,113 @@ +use zed_extension_api::{ + self as zed, Architecture, DownloadedFileType, GithubReleaseOptions, Os, Result, + settings::LspSettings, +}; + +use crate::language_servers::util; + +pub(crate) struct ProtoLs { + cached_binary_path: Option, +} + +impl ProtoLs { + pub(crate) const SERVER_NAME: &str = "protols"; + + pub(crate) fn new() -> Self { + ProtoLs { + cached_binary_path: None, + } + } + + pub(crate) fn language_server_binary( + &mut self, + worktree: &zed::Worktree, + ) -> Result { + let binary_settings = LspSettings::for_worktree(Self::SERVER_NAME, worktree) + .ok() + .and_then(|lsp_settings| lsp_settings.binary); + + let args = binary_settings + .as_ref() + .and_then(|binary_settings| binary_settings.arguments.clone()) + .unwrap_or_default(); + + let env = worktree.shell_env(); + + if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { + return Ok(zed::Command { + command: path, + args, + env, + }); + } else if let Some(path) = self.cached_binary_path.clone() { + return Ok(zed::Command { + command: path, + args, + env, + }); + } else if let Some(path) = worktree.which(Self::SERVER_NAME) { + self.cached_binary_path = Some(path.clone()); + return Ok(zed::Command { + command: path, + args, + env, + }); + } + + let latest_release = zed::latest_github_release( + "coder3101/protols", + GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (os, arch) = zed::current_platform(); + + let release_suffix = match (os, arch) { + (Os::Mac, Architecture::Aarch64) => "aarch64-apple-darwin.tar.gz", + (Os::Mac, Architecture::X8664) => "x86_64-apple-darwin.tar.gz", + (Os::Linux, Architecture::Aarch64) => "aarch64-unknown-linux-gnu.tar.gz", + (Os::Linux, Architecture::X8664) => "x86_64-unknown-linux-gnu.tar.gz", + (Os::Windows, Architecture::X8664) => "x86_64-pc-windows-msvc.zip", + _ => { + return Err("Platform and architecture not supported by Protols".to_string()); + } + }; + + let release_name = format!("protols-{release_suffix}"); + + let file_type = if os == Os::Windows { + DownloadedFileType::Zip + } else { + DownloadedFileType::GzipTar + }; + + let version_dir = format!("{}-{}", Self::SERVER_NAME, latest_release.version); + let binary_path = format!("{version_dir}/protols"); + + let download_target = latest_release + .assets + .into_iter() + .find(|asset| asset.name == release_name) + .ok_or_else(|| { + format!( + "Could not find asset with name {} in Protols release", + &release_name + ) + })?; + + zed::download_file(&download_target.download_url, &version_dir, file_type)?; + zed::make_file_executable(&binary_path)?; + + util::remove_outdated_versions(Self::SERVER_NAME, &version_dir)?; + + self.cached_binary_path = Some(binary_path.clone()); + + Ok(zed::Command { + command: binary_path, + args, + env, + }) + } +} diff --git a/extensions/proto/src/language_servers/util.rs b/extensions/proto/src/language_servers/util.rs new file mode 100644 index 0000000000000000000000000000000000000000..3036c9bc3aaf9cc3fccd462fe0ad70aa31892012 --- /dev/null +++ b/extensions/proto/src/language_servers/util.rs @@ -0,0 +1,19 @@ +use std::fs; + +use zed_extension_api::Result; + +pub(super) fn remove_outdated_versions( + language_server_id: &'static str, + version_dir: &str, +) -> Result<()> { + let entries = fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str().is_none_or(|file_name| { + file_name.starts_with(language_server_id) && file_name != version_dir + }) { + fs::remove_dir_all(entry.path()).ok(); + } + } + Ok(()) +} diff --git a/extensions/proto/src/proto.rs b/extensions/proto/src/proto.rs index 36ba0faf5feda66af8824387240e34a730a476b7..07e0ccedcee287f037576db56d5a9d7958ea83f9 100644 --- a/extensions/proto/src/proto.rs +++ b/extensions/proto/src/proto.rs @@ -1,48 +1,22 @@ use zed_extension_api::{self as zed, Result, settings::LspSettings}; -const PROTOBUF_LANGUAGE_SERVER_NAME: &str = "protobuf-language-server"; +use crate::language_servers::{BufLsp, ProtoLs, ProtobufLanguageServer}; -struct ProtobufLanguageServerBinary { - path: String, - args: Option>, -} - -struct ProtobufExtension; - -impl ProtobufExtension { - fn language_server_binary( - &self, - _language_server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - let binary_settings = LspSettings::for_worktree("protobuf-language-server", worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.binary); - let binary_args = binary_settings - .as_ref() - .and_then(|binary_settings| binary_settings.arguments.clone()); - - if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { - return Ok(ProtobufLanguageServerBinary { - path, - args: binary_args, - }); - } - - if let Some(path) = worktree.which(PROTOBUF_LANGUAGE_SERVER_NAME) { - return Ok(ProtobufLanguageServerBinary { - path, - args: binary_args, - }); - } +mod language_servers; - Err(format!("{PROTOBUF_LANGUAGE_SERVER_NAME} not found in PATH",)) - } +struct ProtobufExtension { + protobuf_language_server: Option, + protols: Option, + buf_lsp: Option, } impl zed::Extension for ProtobufExtension { fn new() -> Self { - Self + Self { + protobuf_language_server: None, + protols: None, + buf_lsp: None, + } } fn language_server_command( @@ -50,14 +24,24 @@ impl zed::Extension for ProtobufExtension { language_server_id: &zed_extension_api::LanguageServerId, worktree: &zed_extension_api::Worktree, ) -> zed_extension_api::Result { - let binary = self.language_server_binary(language_server_id, worktree)?; - Ok(zed::Command { - command: binary.path, - args: binary - .args - .unwrap_or_else(|| vec!["-logs".into(), "".into()]), - env: Default::default(), - }) + match language_server_id.as_ref() { + ProtobufLanguageServer::SERVER_NAME => self + .protobuf_language_server + .get_or_insert_with(ProtobufLanguageServer::new) + .language_server_binary(worktree), + + ProtoLs::SERVER_NAME => self + .protols + .get_or_insert_with(ProtoLs::new) + .language_server_binary(worktree), + + BufLsp::SERVER_NAME => self + .buf_lsp + .get_or_insert_with(BufLsp::new) + .language_server_binary(worktree), + + _ => Err(format!("Unknown language server ID {}", language_server_id)), + } } fn language_server_workspace_configuration( @@ -65,10 +49,8 @@ impl zed::Extension for ProtobufExtension { server_id: &zed::LanguageServerId, worktree: &zed::Worktree, ) -> Result> { - let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.settings); - Ok(settings) + LspSettings::for_worktree(server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.settings) } fn language_server_initialization_options( @@ -76,10 +58,8 @@ impl zed::Extension for ProtobufExtension { server_id: &zed::LanguageServerId, worktree: &zed::Worktree, ) -> Result> { - let initialization_options = LspSettings::for_worktree(server_id.as_ref(), worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.initialization_options); - Ok(initialization_options) + LspSettings::for_worktree(server_id.as_ref(), worktree) + .map(|lsp_settings| lsp_settings.initialization_options) } } diff --git a/typos.toml b/typos.toml index 20a7b511a85676e3c5e49c23cab71c52e471cee9..8e42bd674a64d8adc1e684df181c8e4ce67988e9 100644 --- a/typos.toml +++ b/typos.toml @@ -31,6 +31,9 @@ extend-exclude = [ "crates/rpc/src/auth.rs", # glsl isn't recognized by this tool. "extensions/glsl/languages/glsl/", + # Protols is the name of the language server. + "extensions/proto/extension.toml", + "extensions/proto/src/language_servers/protols.rs", # Windows likes its abbreviations. "crates/gpui/src/platform/windows/directx_renderer.rs", "crates/gpui/src/platform/windows/events.rs",