Detailed changes
@@ -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]]
@@ -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" }
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.2224 1.32129L5.2036 4.41875C5.15145 4.57727 5.06282 4.72134 4.94481 4.83934C4.82681 4.95735 4.68274 5.04598 4.52422 5.09813L1.42676 6.11693L4.52422 7.13574C4.68274 7.18788 4.82681 7.27652 4.94481 7.39453C5.06282 7.51253 5.15145 7.6566 5.2036 7.81512L6.2224 10.9126L7.24121 7.81512C7.29335 7.6566 7.38199 7.51253 7.5 7.39453C7.618 7.27652 7.76207 7.18788 7.9206 7.13574L11.018 6.11693L7.9206 5.09813C7.76207 5.04598 7.618 4.95735 7.5 4.83934C7.38199 4.72134 7.29335 4.57727 7.24121 4.41875L6.2224 1.32129Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.76681 13.9373C9.76681 13.6048 9.95997 13.3083 10.5126 12.7917L11.8872 11.4978C12.3545 11.0575 12.5612 10.77 12.5612 10.4735C12.5612 10.1411 12.3185 9.91643 11.9681 9.91643C11.6986 9.91643 11.5054 10.0242 11.2673 10.3208C10.9933 10.6622 10.7956 10.779 10.4946 10.779C10.0633 10.779 9.75781 10.4915 9.75781 10.0916C9.75781 9.21559 10.8136 8.44287 12.067 8.44287C13.3743 8.44287 14.3492 9.22907 14.3492 10.2848C14.3492 10.9452 13.9988 11.5742 13.2845 12.2077L12.2242 13.1511V13.223H13.7292C14.2503 13.223 14.5738 13.5015 14.5738 13.9552C14.5738 14.4089 14.2593 14.6785 13.7292 14.6785H10.5979C10.1037 14.6785 9.76681 14.3775 9.76681 13.9373Z" fill="black"/>
+<path d="M12.8994 1.32129V4.00482M11.5576 2.66302H14.2412" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -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",
@@ -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",
@@ -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",
@@ -10,12 +10,12 @@
"context": "Workspace",
"bindings": {
// "shift shift": "file_finder::Toggle"
- }
+ },
},
{
"context": "Editor && vim_mode == insert",
"bindings": {
// "j k": "vim::NormalBefore"
- }
- }
+ },
+ },
]
@@ -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
+ },
+ },
]
@@ -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",
+ },
+ },
]
@@ -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",
+ },
+ },
]
@@ -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",
+ },
+ },
]
@@ -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",
+ },
+ },
]
@@ -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",
+ },
+ },
]
@@ -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",
+ },
+ },
]
@@ -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",
+ },
+ },
]
@@ -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",
+ },
+ },
]
@@ -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",
+ },
+ },
]
@@ -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": {},
+ },
]
@@ -27,7 +27,7 @@
"backspace": "editor::Backspace",
"delete": "editor::Delete",
"left": "editor::MoveLeft",
- "right": "editor::MoveRight"
- }
- }
+ "right": "editor::MoveRight",
+ },
+ },
]
@@ -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",
+ },
+ },
]
@@ -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.
@@ -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,
@@ -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",
+ },
]
@@ -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": {},
}
@@ -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": []
- }
+ },
]
@@ -12,6 +12,6 @@
"theme": {
"mode": "system",
"light": "One Light",
- "dark": "One Dark"
- }
+ "dark": "One Dark",
+ },
}
@@ -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",
@@ -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 {
@@ -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
@@ -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()
@@ -1343,6 +1343,7 @@ fn run_eval(eval: EvalInput) -> eval_utils::EvalOutput<EditEvalMetadata> {
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(),
@@ -89,7 +89,7 @@ impl AcpConnection {
cx: &mut AsyncApp,
) -> Result<Self> {
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
@@ -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<LanguageModelSelection>,
pub inline_assistant_model: Option<LanguageModelSelection>,
+ pub inline_assistant_use_streaming_tools: bool,
pub commit_message_model: Option<LanguageModelSelection>,
pub thread_summary_model: Option<LanguageModelSelection>,
pub inline_alternatives: Vec<LanguageModelSelection>,
@@ -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(),
@@ -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
@@ -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 {
@@ -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)
@@ -130,7 +130,12 @@ impl AgentDiffPane {
.action_log()
.read(cx)
.changed_buffers(cx);
- let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
+ let mut paths_to_delete = self
+ .multibuffer
+ .read(cx)
+ .paths()
+ .cloned()
+ .collect::<HashSet<_>>();
for (buffer, diff_handle) in changed_buffers {
if buffer.read(cx).file().is_none() {
@@ -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())
@@ -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::<AgentV2FeatureFlag>();
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::<zed_actions::OpenZedPredictOnboarding>()]);
+ if !agent_v2_enabled {
+ filter.hide_action_types(&[TypeId::of::<zed_actions::agent::ToggleAgentPane>()]);
+ }
}
});
}
@@ -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![],
@@ -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 <rewrite_this> 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 <rewrite_this></rewrite_this> 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<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
- telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
pub is_insertion: bool,
+ session_id: Uuid,
}
impl BufferCodegen {
@@ -80,7 +80,7 @@ impl BufferCodegen {
buffer: Entity<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
- telemetry: Arc<Telemetry>,
+ session_id: Uuid,
builder: Arc<PromptBuilder>,
cx: &mut Context<Self>,
) -> 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<LanguageName> {
+ 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<CodegenEvent> for BufferCodegen {}
@@ -264,7 +272,6 @@ pub struct CodegenAlternative {
status: CodegenStatus,
generation: Task<()>,
diff: Diff,
- telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
builder: Arc<PromptBuilder>,
active: bool,
@@ -274,7 +281,9 @@ pub struct CodegenAlternative {
completion: Option<String>,
selected_text: Option<String>,
pub message_id: Option<String>,
- pub model_explanation: Option<SharedString>,
+ session_id: Uuid,
+ pub description: Option<String>,
+ pub failure: Option<String>,
}
impl EventEmitter<CodegenEvent> for CodegenAlternative {}
@@ -284,8 +293,8 @@ impl CodegenAlternative {
buffer: Entity<MultiBuffer>,
range: Range<Anchor>,
active: bool,
- telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
+ session_id: Uuid,
cx: &mut Context<Self>,
) -> 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<LanguageName> {
+ self.old_buffer
+ .read(cx)
+ .language()
+ .map(|language| language.name())
+ }
+
pub fn set_active(&mut self, active: bool, cx: &mut Context<Self>) {
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::<InlineAssistantUseToolFeatureFlag>()
+ && 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::<InlineAssistantV2FeatureFlag>() {
+ 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<Result<LanguageModelTextStream>> =
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<dyn LanguageModel>,
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<Task<Option<LoadedContext>>>,
cx: &mut App,
) -> Result<Task<LanguageModelRequest>> {
- if cx.has_flag::<InlineAssistantV2FeatureFlag>() {
- 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<String>,
+ model: Arc<dyn LanguageModel>,
stream: impl 'static + Future<Output = Result<LanguageModelTextStream>>,
cx: &mut Context<Self>,
- ) {
+ ) -> 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<anyhow::Result<()>> =
- cx.background_spawn(async move {
+ let line_based_stream_diff: Task<anyhow::Result<()>> = 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<String> {
self.completion.clone()
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn current_description(&self) -> Option<String> {
+ self.description.clone()
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn current_failure(&self) -> Option<String> {
+ 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<String>,
- tool_use: impl 'static
- + Future<
- Output = Result<language_model::LanguageModelToolUse, LanguageModelCompletionError>,
+ model: Arc<dyn LanguageModel>,
+ completion_stream: Task<
+ Result<
+ BoxStream<
+ 'static,
+ Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
+ >,
+ LanguageModelCompletionError,
+ >,
>,
cx: &mut Context<Self>,
- ) {
+ ) -> 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::<RewriteSectionInput>(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<String>,
+ },
+ 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<ToolUseOutput> {
+ 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::<RewriteSectionInput>(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::<FailureMessageInput>(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::<FailureMessageInput>(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::<ModelUpdate>();
+
+ 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<String> {
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(),
@@ -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)
-}
@@ -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<dyn Fs>,
- prompt_builder: Arc<PromptBuilder>,
- telemetry: Arc<Telemetry>,
- cx: &mut App,
-) {
- cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
+pub fn init(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>, cx: &mut App) {
+ cx.set_global(InlineAssistant::new(fs, prompt_builder));
cx.observe_global::<SettingsStore>(|cx| {
if DisableAiSettings::get_global(cx).disable_ai {
@@ -100,7 +95,6 @@ pub struct InlineAssistant {
confirmed_assists: HashMap<InlineAssistId, Entity<CodegenAlternative>>,
prompt_history: VecDeque<String>,
prompt_builder: Arc<PromptBuilder>,
- telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
_inline_assistant_completions: Option<mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>>,
}
@@ -108,11 +102,7 @@ pub struct InlineAssistant {
impl Global for InlineAssistant {}
impl InlineAssistant {
- pub fn new(
- fs: Arc<dyn Fs>,
- prompt_builder: Arc<PromptBuilder>,
- telemetry: Arc<Telemetry>,
- ) -> Self {
+ pub fn new(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>) -> 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<anyhow::Result<InlineAssistId>>,
- ) {
- self._inline_assistant_completions = Some(sender);
- }
-
pub fn register_workspace(
&mut self,
workspace: &Entity<Workspace>,
@@ -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<anyhow::Result<InlineAssistId>>,
+ ) {
+ 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<Entity<CodegenAlternative>> {
+ 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<Range<Anchor>>, 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<String>,
+ description: Option<String>,
+ full_buffer_text: String,
+ },
+ Failure {
+ failure: String,
+ },
+ // These fields are used for logging
+ #[allow(unused)]
+ Malformed {
+ completion: Option<String>,
+ description: Option<String>,
+ failure: Option<String>,
+ },
+ }
+
pub fn run_inline_assistant_test<SetupF, TestF>(
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<String>,
+ buffer: impl Into<String>,
+ 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<String>,
+ ) -> 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
+ )),
+ }
}
}
@@ -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<String>),
- Rated(Uuid),
+ Generated { completion_text: Option<String> },
+ Rated,
}
-impl RatingState {
- fn is_pending(&self) -> bool {
- matches!(self, RatingState::Pending)
- }
-
- fn rating_id(&self) -> Option<Uuid> {
- match self {
- RatingState::Pending => None,
- RatingState::GeneratedCompletion(_) => None,
- RatingState::Rated(id) => Some(*id),
- }
- }
-
- fn rate(&mut self) -> (Uuid, Option<String>) {
- 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<String>) {
- *self = RatingState::GeneratedCompletion(generated_completion);
- }
+struct SessionState {
+ session_id: Uuid,
+ completion: CompletionState,
}
pub struct PromptEditor<T> {
@@ -109,7 +70,7 @@ pub struct PromptEditor<T> {
_codegen_subscription: Subscription,
editor_subscriptions: Vec<Subscription>,
show_rate_limit_notice: bool,
- rated: RatingState,
+ session_state: SessionState,
_phantom: std::marker::PhantomData<T>,
}
@@ -140,11 +101,11 @@ impl<T: 'static> Render for PromptEditor<T> {
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<T: 'static> Render for PromptEditor<T> {
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<T: 'static> PromptEditor<T> {
}
self.edited_since_done = true;
- self.rated.reset();
+ self.session_state.completion = CompletionState::Pending;
cx.notify();
}
EditorEvent::Blurred => {
@@ -559,109 +520,179 @@ impl<T: 'static> PromptEditor<T> {
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
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<Self>) {
- 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<Self>) {
+ 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<Self>) {
+ 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<Self>) {
- 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<Self>) {
+ 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<Uuid>, cx: &mut Context<'_, PromptEditor<T>>) {
@@ -795,8 +826,8 @@ impl<T: 'static> PromptEditor<T> {
.into_any_element(),
]
} else {
- let show_rating_buttons = cx.has_flag::<InlineAssistRatingFeatureFlag>();
- let rated = self.rated.rating_id().is_some();
+ let show_rating_buttons = cx.has_flag::<InlineAssistantUseToolFeatureFlag>();
+ 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<BufferCodegen> {
prompt_history: VecDeque<String>,
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<BufferCodegen>,
+ session_id: Uuid,
fs: Arc<dyn Fs>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
@@ -1190,7 +1222,10 @@ impl PromptEditor<BufferCodegen> {
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<BufferCodegen> {
.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<TerminalCodegen> {
prompt_history: VecDeque<String>,
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<TerminalCodegen>,
+ session_id: Uuid,
fs: Arc<dyn Fs>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
@@ -1337,7 +1375,10 @@ impl PromptEditor<TerminalCodegen> {
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<TerminalCodegen> {
.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));
@@ -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<Arc<Telemetry>>,
terminal: Entity<Terminal>,
generation: Task<()>,
pub message_id: Option<String>,
transaction: Option<TerminalTransaction>,
+ session_id: Uuid,
}
impl EventEmitter<CodegenEvent> for TerminalCodegen {}
impl TerminalCodegen {
- pub fn new(terminal: Entity<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
+ pub fn new(terminal: Entity<Terminal>, 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<LanguageModelRequest>, cx: &mut Context<Self>) {
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(())
}
@@ -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<dyn Fs>,
- prompt_builder: Arc<PromptBuilder>,
- telemetry: Arc<Telemetry>,
- cx: &mut App,
-) {
- cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry));
+pub fn init(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>, 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<TerminalInlineAssistId, TerminalInlineAssist>,
prompt_history: VecDeque<String>,
- telemetry: Option<Arc<Telemetry>>,
fs: Arc<dyn Fs>,
prompt_builder: Arc<PromptBuilder>,
}
@@ -52,16 +46,11 @@ pub struct TerminalInlineAssistant {
impl Global for TerminalInlineAssistant {}
impl TerminalInlineAssistant {
- pub fn new(
- fs: Arc<dyn Fs>,
- prompt_builder: Arc<PromptBuilder>,
- telemetry: Arc<Telemetry>,
- ) -> Self {
+ pub fn new(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>) -> 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,
);
}
@@ -3333,7 +3333,6 @@ mod tests {
let mut text_thread = TextThread::local(
registry,
None,
- None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
cx,
@@ -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
@@ -0,0 +1 @@
+LICENSE-GPL
@@ -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<HistoryEntryId> 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<Pixels>,
+ pub thread_id: Option<SerializedHistoryEntryId>,
+}
+
+pub enum AgentsUtilityPaneEvent {
+ StateChanged,
+}
+
+impl EventEmitter<AgentsUtilityPaneEvent> for AgentThreadPane {}
+impl EventEmitter<MinimizePane> for AgentThreadPane {}
+impl EventEmitter<ClosePane> for AgentThreadPane {}
+
+struct ActiveThreadView {
+ view: Entity<AcpThreadView>,
+ thread_id: HistoryEntryId,
+ _notify: Subscription,
+}
+
+pub struct AgentThreadPane {
+ focus_handle: gpui::FocusHandle,
+ expanded: bool,
+ width: Option<Pixels>,
+ thread_view: Option<ActiveThreadView>,
+ workspace: WeakEntity<Workspace>,
+}
+
+impl AgentThreadPane {
+ pub fn new(workspace: WeakEntity<Workspace>, cx: &mut ui::Context<Self>) -> Self {
+ let focus_handle = cx.focus_handle();
+ Self {
+ focus_handle,
+ expanded: false,
+ width: None,
+ thread_view: None,
+ workspace,
+ }
+ }
+
+ pub fn thread_id(&self) -> Option<HistoryEntryId> {
+ 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<dyn Fs>,
+ workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let thread_id = entry.id();
+
+ let resume_thread = match &entry {
+ HistoryEntry::AcpThread(thread) => Some(thread.clone()),
+ HistoryEntry::TextThread(_) => None,
+ };
+
+ let agent: Rc<dyn AgentServer> = 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<Self>) -> 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<Workspace>| {
+ 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>) {
+ 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<Pixels>, cx: &mut Context<Self>) {
+ self.width = width;
+ cx.emit(AgentsUtilityPaneEvent::StateChanged);
+ cx.notify();
+ }
+}
+
+impl Render for AgentThreadPane {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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)
+ }
+}
@@ -0,0 +1,4 @@
+mod agent_thread_pane;
+mod thread_history;
+
+pub mod agents_panel;
@@ -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<Pixels>,
+ pane: Option<SerializedAgentThreadPane>,
+}
+
+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::<AgentsPanel>(window, cx);
+ });
+ })
+ .detach();
+}
+
+pub struct AgentsPanel {
+ focus_handle: gpui::FocusHandle,
+ workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
+ agent_thread_pane: Option<Entity<AgentThreadPane>>,
+ history: Entity<AcpThreadHistory>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ fs: Arc<dyn Fs>,
+ width: Option<Pixels>,
+ pending_serialization: Task<Option<()>>,
+ _subscriptions: Vec<Subscription>,
+}
+
+impl AgentsPanel {
+ pub fn load(
+ workspace: WeakEntity<Workspace>,
+ cx: AsyncWindowContext,
+ ) -> Task<Result<Entity<Self>, 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::<SerializedAgentsPanel>(&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<Workspace>,
+ fs: Arc<dyn Fs>,
+ project: Entity<Project>,
+ prompt_store: Option<Entity<PromptStore>>,
+ text_thread_store: Entity<TextThreadStore>,
+ window: &mut Window,
+ cx: &mut ui::Context<Self>,
+ ) -> 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<Self>,
+ ) {
+ 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<AgentThreadPane>,
+ event: &AgentsUtilityPaneEvent,
+ cx: &mut Context<Self>,
+ ) {
+ match event {
+ AgentsUtilityPaneEvent::StateChanged => {
+ self.serialize(cx);
+ cx.notify();
+ }
+ }
+ }
+
+ fn handle_close_pane_event(
+ &mut self,
+ _utility_pane: Entity<AgentThreadPane>,
+ _event: &ClosePane,
+ cx: &mut Context<Self>,
+ ) {
+ self.agent_thread_pane = None;
+ self.serialize(cx);
+ cx.notify();
+ }
+
+ fn handle_history_event(
+ &mut self,
+ _history: &Entity<AcpThreadHistory>,
+ event: &ThreadHistoryEvent,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ 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<Pixels>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ 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<Self>) {
+ 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<Self>) {
+ 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<PanelEvent> 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<Self>,
+ ) {
+ 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<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
+ 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<IconName> {
+ (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<dyn Action> {
+ Box::new(ToggleAgentsPanel)
+ }
+
+ fn activation_priority(&self) -> u32 {
+ 4
+ }
+
+ fn enabled(&self, cx: &App) -> bool {
+ AgentSettings::get_global(cx).enabled(cx) && cx.has_flag::<AgentV2FeatureFlag>()
+ }
+}
+
+impl Render for AgentsPanel {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ gpui::div().size_full().child(self.history.clone())
+ }
+}
@@ -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<HistoryStore>,
+ scroll_handle: UniformListScrollHandle,
+ selected_index: usize,
+ hovered_index: Option<usize>,
+ search_editor: Entity<Editor>,
+ search_query: SharedString,
+ visible_items: Vec<ListItemType>,
+ local_timezone: UtcOffset,
+ confirming_delete_history: bool,
+ _update_task: Task<()>,
+ _subscriptions: Vec<gpui::Subscription>,
+}
+
+enum ListItemType {
+ BucketSeparator(TimeBucket),
+ Entry {
+ entry: HistoryEntry,
+ format: EntryTimeFormat,
+ },
+ SearchResult {
+ entry: HistoryEntry,
+ positions: Vec<usize>,
+ },
+}
+
+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<ThreadHistoryEvent> for AcpThreadHistory {}
+
+impl AcpThreadHistory {
+ pub fn new(
+ history_store: Entity<agent::HistoryStore>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> 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<Self>) {
+ 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<HistoryEntry>, cx: &App) -> Task<Vec<ListItemType>> {
+ 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<HistoryEntry>,
+ cx: &App,
+ ) -> Task<Vec<ListItemType>> {
+ 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<Self>) {
+ 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<Self>,
+ ) {
+ 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<Self>,
+ ) {
+ 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>,
+ ) {
+ self.set_selected_index(0, Bias::Right, cx);
+ }
+
+ fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
+ 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>) {
+ self.confirm_entry(self.selected_index, cx);
+ }
+
+ fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
+ 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>,
+ ) {
+ self.remove_thread(self.selected_index, cx)
+ }
+
+ fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
+ 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>) {
+ 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>) {
+ self.confirming_delete_history = true;
+ cx.notify();
+ }
+
+ fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
+ self.confirming_delete_history = false;
+ cx.notify();
+ }
+
+ fn render_list_items(
+ &mut self,
+ range: Range<usize>,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Vec<AnyElement> {
+ 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<Self>) -> 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<usize>,
+ cx: &Context<Self>,
+ ) -> 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::<IconButton>(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<Self>) -> 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<usize>, 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<TimeBucket> 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);
+ }
+}
@@ -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, ..
@@ -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
@@ -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::<anyhow::Result<Vec<custom_path_matcher::PathMatcher>>>()
+ .collect::<anyhow::Result<Vec<util::paths::PathMatcher>>>()
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<Arc<RelPath>> = Vec::new();
+ let mut folded_directory_path: Option<Arc<RelPath>> = None;
let mut folded_directory_names: Arc<RelPath> = 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<String>,
- sources_with_trailing_slash: Vec<String>,
- 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<Self, globset::Error> {
- let globs = globs
- .iter()
- .map(|glob| Glob::new(&SanitizedPath::new(glob).to_string()))
- .collect::<Result<Vec<_>, _>>()?;
- 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>,
@@ -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
@@ -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,
@@ -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<Option<()>>,
path: Option<Arc<Path>>,
_subscriptions: Vec<Subscription>,
- telemetry: Option<Arc<Telemetry>>,
language_registry: Arc<LanguageRegistry>,
project: Option<WeakEntity<Project>>,
prompt_builder: Arc<PromptBuilder>,
@@ -709,7 +709,6 @@ impl TextThread {
pub fn local(
language_registry: Arc<LanguageRegistry>,
project: Option<WeakEntity<Project>>,
- telemetry: Option<Arc<Telemetry>>,
prompt_builder: Arc<PromptBuilder>,
slash_commands: Arc<SlashCommandWorkingSet>,
cx: &mut Context<Self>,
@@ -722,7 +721,6 @@ impl TextThread {
prompt_builder,
slash_commands,
project,
- telemetry,
cx,
)
}
@@ -743,7 +741,6 @@ impl TextThread {
prompt_builder: Arc<PromptBuilder>,
slash_commands: Arc<SlashCommandWorkingSet>,
project: Option<WeakEntity<Project>>,
- telemetry: Option<Arc<Telemetry>>,
cx: &mut Context<Self>,
) -> 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<PromptBuilder>,
slash_commands: Arc<SlashCommandWorkingSet>,
project: Option<WeakEntity<Project>>,
- telemetry: Option<Arc<Telemetry>>,
cx: &mut Context<Self>,
) -> 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 => {}
@@ -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<dyn Fs>,
languages: Arc<LanguageRegistry>,
slash_commands: Arc<SlashCommandWorkingSet>,
- telemetry: Arc<Telemetry>,
_watch_updates: Task<Option<()>>,
client: Arc<Client>,
project: WeakEntity<Project>,
@@ -88,7 +87,6 @@ impl TextThreadStore {
) -> Task<Result<Entity<Self>>> {
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,
)
})?;
@@ -150,9 +150,8 @@ pub fn init(client: &Arc<Client>, 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<Client>, cx: &mut App) {
.detach();
}
}
- });
-
- cx.on_action({
+ })
+ .on_action({
let client = client;
move |_: &Reconnect, cx| {
if let Some(client) = client.upgrade() {
@@ -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,
@@ -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::<lsp::request::Completion, _, _>(
+ |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<E>".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::<lsp::request::Completion, _, _>(
+ |_, _| 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::<lsp::request::Completion, _, _>(|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<E>".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::<lsp::request::Completion, _, _>(|_, _| 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();
@@ -32,7 +32,7 @@ impl StdioTransport {
cx: &AsyncApp,
) -> Result<Self> {
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);
@@ -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<Self>) -> Task<Result<()>> {
+ 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<Self>) -> Task<Result<()>> {
if let CopilotServer::Running(server) = &mut self.server {
let task = match &server.sign_in_status {
SignInStatus::Authorized => Task::ready(Ok(())).shared(),
@@ -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::<Workspace>().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::<Workspace>().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<Copilot>, 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::<CopilotStatusToast>();
+
let Some(workspace) = window.root::<Workspace>().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<Copilot>,
- window: &mut Window,
- cx: &mut Context<Workspace>,
-) {
- 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<Copilot>,
- is_reinstall: bool,
- window: &mut Window,
- cx: &mut Context<Workspace>,
-) {
+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::<CopilotStatusToast>(),
- 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::<CopilotStatusToast>(),
- "Copilot has started.",
- ),
- cx,
- ),
- _ => {
- workspace.dismiss_toast(
- &NotificationId::unique::<CopilotStatusToast>(),
- 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<Copilot>,
- cx: &mut Context<Workspace>,
-) {
- workspace.show_toast(
- Toast::new(
- NotificationId::unique::<CopilotStatusToast>(),
- "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::<CopilotStatusToast>(),
- "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<DismissEvent> for CopilotCodeVerification {}
-impl ModalView for CopilotCodeVerification {
- fn on_before_dismiss(
- &mut self,
- _: &mut Window,
- cx: &mut Context<Self>,
- ) -> 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<Copilot>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+ window.on_window_should_close(cx, |window, cx| {
+ if let Some(this) = window.root::<CopilotCodeVerification>().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<Copilot>, cx: &mut Context<Self>) -> 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<Self>,
) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> impl IntoElement {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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<Status>,
+ is_authenticated: fn(cx: &App) -> bool,
+ edit_prediction: bool,
+ _subscription: Option<Subscription>,
+}
+
+pub enum ConfigurationMode {
+ Chat,
+ EditPrediction,
+}
+
+impl ConfigurationView {
+ pub fn new(
+ is_authenticated: fn(cx: &App) -> bool,
+ mode: ConfigurationMode,
+ cx: &mut Context<Self>,
+ ) -> 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<SharedString> {
+ 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<SharedString>,
+ 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<Self>) -> 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()
+ }
+ }
+}
@@ -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))
})
});
}
@@ -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);
}
}
@@ -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
@@ -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<Arc<zeta_prompt::Event>> {
+ 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<Arc<dyn File>>,
+ snapshot: TextBufferSnapshot,
last_position: Option<Anchor>,
_subscriptions: [gpui::Subscription; 2],
}
+#[derive(Clone)]
struct LastEvent {
- old_snapshot: BufferSnapshot,
- new_snapshot: BufferSnapshot,
+ old_snapshot: TextBufferSnapshot,
+ new_snapshot: TextBufferSnapshot,
+ old_file: Option<Arc<dyn File>>,
+ new_file: Option<Arc<dyn File>>,
end_edit_anchor: Option<Anchor>,
+ snapshot_after_last_editing_pause: Option<TextBufferSnapshot>,
+ last_edit_time: Option<Instant>,
}
impl LastEvent {
@@ -401,19 +423,19 @@ impl LastEvent {
license_detection_watchers: &HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
cx: &App,
) -> Option<Arc<zeta_prompt::Event>> {
- 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<LastEvent>) {
+ 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<Path> {
- if let Some(file) = snapshot.file() {
+fn buffer_path_with_id_fallback(
+ file: Option<&Arc<dyn File>>,
+ snapshot: &TextBufferSnapshot,
+ cx: &App,
+) -> Arc<Path> {
+ 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<Project>,
+ cx: &App,
+ ) -> Vec<Arc<zeta_prompt::Event>> {
+ 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<Project>,
+ cx: &App,
) -> Vec<Arc<zeta_prompt::Event>> {
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::<Point>(&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),
});
}
@@ -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]
@@ -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<Path>,
+ 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<Self> {
+ 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)
+ }
+}
@@ -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<Task<Option<String>>>,
+ pub api_token: Entity<ApiKeyState>,
}
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<String>, cx: &mut App) -> Task<Result<()>> {
- 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<Result<Option<EditPredictionResult>>> {
- 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<Path> = 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<EnvVar> = env_var!("MERCURY_AI_TOKEN");
+pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
-pub fn load_api_token(cx: &App) -> Task<Option<String>> {
- 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 = <dyn CredentialsProvider>::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<String>, cx: &App) -> Task<Result<()>> {
- let credentials_provider = <dyn CredentialsProvider>::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<ApiKeyState> {
+ MERCURY_API_KEY
+ .get_or_init(|| {
+ cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
+ })
+ .clone()
}
@@ -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<Task<Option<String>>>,
+ pub api_token: Entity<ApiKeyState>,
pub debug_info: Arc<str>,
}
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<String>, cx: &mut App) -> Task<Result<()>> {
- 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<Result<Option<EditPredictionResult>>> {
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<Path> = 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<EnvVar> = env_var!("SWEEP_AI_TOKEN");
+pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
-pub fn load_api_token(cx: &App) -> Task<Option<String>> {
- 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 = <dyn CredentialsProvider>::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<String>, cx: &App) -> Task<Result<()>> {
- let credentials_provider = <dyn CredentialsProvider>::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<ApiKeyState> {
+ SWEEP_API_KEY
+ .get_or_init(|| {
+ cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
+ })
+ .clone()
}
#[derive(Debug, Clone, Serialize)]
@@ -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
}
@@ -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<String> {
+ 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())
}
@@ -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
@@ -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(())
}
@@ -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<Path>,
- 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<Example> {
serde_json::from_str::<Example>(&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<Example> {
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<Vec<&mut Example>
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<Example> {
- 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<Example> {
+ 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)
+ })
}
@@ -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<EpAppState>,
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<String> {
// 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');
}
@@ -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<EpAppState>, mut cx: AsyncApp) {
+pub async fn run_load_project(
+ example: &mut Example,
+ app_state: Arc<EpAppState>,
+ 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<EpAppState>,
cursor_position,
_open_buffers,
});
+ Ok(())
}
async fn cursor_position(
example: &Example,
project: &Entity<Project>,
cx: &mut AsyncApp,
-) -> (Entity<Buffer>, Anchor) {
- let language_registry = project
- .read_with(cx, |project, _| project.languages().clone())
- .unwrap();
+) -> Result<(Entity<Buffer>, 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::<LanguageNotFound>()
{
- 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<EpAppState>,
step_progress: &StepProgress,
cx: &mut AsyncApp,
-) -> Entity<Project> {
+) -> Result<Entity<Project>> {
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::<Vec<_>>()
- })
- .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::<Vec<_>>()
+ })?;
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<PathBuf> {
+ 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<Project>,
cx: &mut AsyncApp,
) -> Result<OpenedBuffers> {
- 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! {
@@ -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<PathBuf>,
#[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());
})
@@ -18,6 +18,8 @@ pub static RUN_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
});
pub static LATEST_EXAMPLE_RUN_DIR: LazyLock<PathBuf> = LazyLock::new(|| DATA_DIR.join("latest"));
pub static LLM_CACHE_DB: LazyLock<PathBuf> = LazyLock::new(|| CACHE_DIR.join("llm_cache.sqlite"));
+pub static FAILED_EXAMPLES_DIR: LazyLock<PathBuf> =
+ 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");
@@ -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<EpAppState>,
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::<Vec<String>>()
.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(())
}
@@ -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<Arc<Progress>> = 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
+ );
+ }
}
}
@@ -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<EpAppState>,
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<StepProgress> = 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<Buffer>,
step_progress: &Arc<StepProgress>,
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(())
}
@@ -15,7 +15,7 @@ pub async fn run_scoring(
args: &PredictArgs,
app_state: Arc<EpAppState>,
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<DiffLine<'_>> {
@@ -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,
@@ -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
@@ -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::<Zeta2FeatureFlag>() {
+ 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::<SweepFeatureFlag>() {
+ let ep_store = EditPredictionStore::try_global(cx);
+
+ if cx.has_flag::<SweepFeatureFlag>()
+ && 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::<MercuryFeatureFlag>() {
+ if cx.has_flag::<MercuryFeatureFlag>()
+ && 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::<Zeta2FeatureFlag>() {
- 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::<Workspace>().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::<Workspace>().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::<PredictEditsRatePredictionsFeatureFlag>(),
- |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
})
}
@@ -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::<Zeta2FeatureFlag>();
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::<ResetOnboarding>()];
let all_action_types = [
TypeId::of::<RatePredictions>(),
+ TypeId::of::<CaptureExample>(),
TypeId::of::<edit_prediction::ResetOnboarding>(),
zed_actions::OpenZedPredictOnboarding.type_id(),
TypeId::of::<edit_prediction::ClearHistory>(),
@@ -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<Workspace>,
+) {
+ 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::<Editor>(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<Path> = 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::<String>();
+ 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);
+}
@@ -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<InputField>,
- focus_handle: FocusHandle,
- on_confirm: Box<dyn Fn(Option<String>, &mut EditPredictionStore, &mut App)>,
-}
-
-impl ExternalProviderApiKeyModal {
- pub fn new(
- window: &mut Window,
- cx: &mut Context<Self>,
- on_confirm: impl Fn(Option<String>, &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<Self>) {
- cx.emit(DismissEvent);
- }
-
- fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
- 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<DismissEvent> 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<Self>) -> 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);
- })),
- ),
- )
- }
-}
@@ -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.)),
@@ -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<RenameState>,
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<CurrentLineHighlight>,
pub collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
@@ -1118,6 +1122,7 @@ pub struct Editor {
remote_id: Option<ViewId>,
pub hover_state: HoverState,
pending_mouse_down: Option<Rc<RefCell<Option<MouseDownEvent>>>>,
+ prev_pressure_stage: Option<PressureStage>,
gutter_hovered: bool,
hovered_link_state: Option<HoveredLinkState>,
edit_prediction_provider: Option<RegisteredEditPredictionDelegate>,
@@ -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<CurrentLineHighlight>,
@@ -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<Self>,
) -> Option<Task<Result<()>>> {
- if input.len() != 1 {
+ if input.chars().count() != 1 {
return None;
}
@@ -22956,10 +22976,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
- 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()
});
@@ -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<T: ToPoint + ToDisplayPoint + Clone>(
selection: Selection<T>,
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<Editor>,
+ ) {
+ 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),
@@ -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::<GotoDefinition, _, _>(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(); }
+ "});
+ }
}
@@ -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;
@@ -842,7 +842,6 @@ impl Item for Editor {
.map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone()))
.collect::<HashSet<_>>();
- // let mut buffers_to_save =
let buffers_to_save = if self.buffer.read(cx).is_singleton() && !options.autosave {
buffers
} else {
@@ -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);
@@ -40,6 +40,24 @@ pub struct EvalOutput<M> {
pub metadata: M,
}
+impl<M: Default> EvalOutput<M> {
+ pub fn passed(message: impl Into<String>) -> Self {
+ EvalOutput {
+ outcome: OutcomeKind::Passed,
+ data: message.into(),
+ metadata: M::default(),
+ }
+ }
+
+ pub fn failed(message: impl Into<String>) -> Self {
+ EvalOutput {
+ outcome: OutcomeKind::Failed,
+ data: message.into(),
+ metadata: M::default(),
+ }
+ }
+}
+
pub struct NoProcessor;
impl EvalOutputProcessor for NoProcessor {
type Metadata = ();
@@ -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<string>,
}
@@ -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,
},
))
@@ -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";
}
@@ -44,8 +44,9 @@ impl OpenPathDelegate {
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
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>,
) {
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);
@@ -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::<String>::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<AppState> {
fn build_open_path_prompt(
project: Entity<Project>,
creating_path: bool,
- path_style: PathStyle,
cx: &mut TestAppContext,
) -> (Entity<Picker<OpenPathDelegate>>, &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.))
@@ -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"] }
@@ -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>,
) {
- 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<WeakEntity<Workspace>>,
matches: Vec<Entry>,
@@ -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<Picker<BranchListDelegate>>, 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<str> {
- "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<SharedString> {
+ match self.state {
+ PickerState::CreateRemote(_) => {
+ Some(SharedString::new_static("Remote name can't be empty"))
+ }
+ _ => None,
+ }
}
fn render_editor(
&self,
editor: &Entity<Editor>,
- window: &mut Window,
- cx: &mut Context<Picker<Self>>,
+ _window: &mut Window,
+ _cx: &mut Context<Picker<Self>>,
) -> 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<Entry> = 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<Picker<Self>>) {
- 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<Picker<Self>>,
) -> Option<AnyElement> {
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<Entity<Repository>>,
branches: Vec<Branch>,
- ) -> (VisualTestContext, Entity<BranchList>) {
+ cx: &mut TestAppContext,
+ ) -> (Entity<BranchList>, 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<Repository> {
@@ -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<Branch> = (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);
+ })
+ });
+ }
}
@@ -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)
@@ -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<Self>) {
+ 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<Self>,
+ ) {
+ 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<Self>) {
+ 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<Self>) {
+ 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<usize>, cx: &mut Context<Self>) {
+ 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>) {
+ self.open_commit_view(window, cx);
+ }
+
+ fn open_commit_view(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ 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(
@@ -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()
@@ -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::<Self>(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>) {
+ 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<Self>) {
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<Self>,
) {
- 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::<GitPanel>(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::<GitPanel>(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<Self>, cx: &mut AsyncWindowContext) -> Result<()> {
+ pub async fn refresh(
+ this: WeakEntity<Self>,
+ 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::<HashSet<_>>();
+ let mut previous_paths = this
+ .multibuffer
+ .read(cx)
+ .paths()
+ .cloned()
+ .collect::<HashSet<_>>();
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<std::sync::Arc<util::rel_path::RelPath>> {
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(),
);
}
@@ -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"
@@ -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<Self>) -> 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>,
+ ) {
+ 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);
+ });
+}
@@ -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<A: Action>(&mut self, listener: impl Fn(&A, &mut Self) + 'static) {
+ pub fn on_action<A: Action>(
+ &mut self,
+ listener: impl Fn(&A, &mut Self) + 'static,
+ ) -> &mut Self {
self.global_action_listeners
.entry(TypeId::of::<A>())
.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
@@ -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<U>
where
U: Clone + Debug + Default + PartialEq,
{
- root: Option<usize>,
+ /// All nodes stored contiguously for cache efficiency.
nodes: Vec<Node<U>>,
- stack: Vec<usize>,
+ /// Index of the root node, if any.
+ root: Option<usize>,
+ /// Index of the leaf with the highest ordering (for fast-path lookups).
+ max_leaf: Option<usize>,
+ /// Reusable stack for tree traversal during insertion.
+ insert_path: Vec<usize>,
+ /// Reusable stack for search operations.
+ search_stack: Vec<usize>,
+}
+
+/// A node in the bounds tree.
+#[derive(Debug, Clone)]
+struct Node<U>
+where
+ U: Clone + Debug + Default + PartialEq,
+{
+ /// Bounding box containing this node and all descendants.
+ bounds: Bounds<U>,
+ /// 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<U> BoundsTree<U>
@@ -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<U>) -> 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<U>) -> 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<U>, 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<U>, 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<U>, 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<U>
-where
- U: Clone + Debug + Default + PartialEq,
-{
- Leaf {
- bounds: Bounds<U>,
- order: u32,
- },
- Internal {
- left: usize,
- right: usize,
- bounds: Bounds<U>,
- max_order: u32,
- },
-}
-
-impl<U> Node<U>
-where
- U: Clone + Debug + Default + PartialEq,
-{
- fn bounds(&self) -> &Bounds<U> {
- 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(),
}
}
}
@@ -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<dyn Fn(&MouseDownEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
pub(crate) type MouseUpListener =
Box<dyn Fn(&MouseUpEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
-
+pub(crate) type MousePressureListener =
+ Box<dyn Fn(&MousePressureEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
pub(crate) type MouseMoveListener =
Box<dyn Fn(&MouseMoveEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
@@ -1521,6 +1578,7 @@ pub struct Interactivity {
pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>,
pub(crate) mouse_down_listeners: Vec<MouseDownListener>,
pub(crate) mouse_up_listeners: Vec<MouseUpListener>,
+ pub(crate) mouse_pressure_listeners: Vec<MousePressureListener>,
pub(crate) mouse_move_listeners: Vec<MouseMoveListener>,
pub(crate) scroll_wheel_listeners: Vec<ScrollWheelListener>,
pub(crate) key_down_listeners: Vec<KeyDownListener>,
@@ -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.));
+ }
+}
@@ -2648,6 +2648,18 @@ impl Debug for Pixels {
}
}
+impl std::iter::Sum for Pixels {
+ fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
+ iter.fold(Self::ZERO, |a, b| a + b)
+ }
+}
+
+impl<'a> std::iter::Sum<&'a Pixels> for Pixels {
+ fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
+ iter.fold(Self::ZERO, |a, b| a + *b)
+ }
+}
+
impl TryFrom<&'_ str> for Pixels {
type Error = anyhow::Error;
@@ -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<Pixels>,
+ /// 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,
@@ -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::<libc::sched_param>::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 {
@@ -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() {
@@ -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),
@@ -58,8 +58,7 @@ impl<T> PriorityQueueState<T> {
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<T> Iterator for Iter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
- self.0.pop_inner(true).ok().flatten()
+ self.0.pop().ok()
}
}
impl<T> FusedIterator for Iter<T> {}
@@ -283,7 +282,7 @@ impl<T> Iterator for TryIter<T> {
return None;
}
- let res = self.receiver.pop_inner(false);
+ let res = self.receiver.try_pop();
self.ended = res.is_err();
res.transpose()
@@ -252,6 +252,7 @@ pub struct Style {
pub box_shadow: Vec<BoxShadow>,
/// 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
+ );
+ }
}
@@ -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<TextStyleRefinement> {
+ 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<Hsla>) -> 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<Hsla>) -> 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<AbsoluteLength>) -> 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<Hsla>) -> 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<SharedString>) -> 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<DefiniteLength>) -> 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
}
@@ -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)
@@ -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();
@@ -260,6 +260,7 @@ pub enum IconName {
XCircle,
XCircleFilled,
ZedAgent,
+ ZedAgentTwo,
ZedAssistant,
ZedBurnMode,
ZedBurnModeOn,
@@ -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::<KeymapEditor>(cx);
}
@@ -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"] }
@@ -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<future::Shared<Task<()>>>,
}
@@ -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<Ent: 'static>(
&mut self,
url: SharedString,
- env_var: &EnvVar,
get_this: impl Fn(&mut Ent) -> &mut Self + Clone + 'static,
cx: &mut Context<Ent>,
) {
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<Ent: 'static>(
&mut self,
url: SharedString,
- env_var: &EnvVar,
get_this: impl Fn(&mut Ent) -> &mut Self + Clone + 'static,
cx: &mut Context<Ent>,
) -> Task<Result<(), AuthenticateError>> {
@@ -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;
@@ -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("<dyn LanguageModel>")
+ .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 {
@@ -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<Arc<Telemetry>>,
- client: Arc<dyn HttpClient>,
- model_api_key: Option<String>,
- executor: &BackgroundExecutor,
+#[derive(Clone, Debug)]
+pub struct AnthropicEventData {
+ pub completion_type: AnthropicCompletionType,
+ pub event: AnthropicEventType,
+ pub language_name: Option<String>,
+ pub message_id: Option<String>,
+}
+
+#[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<dyn crate::LanguageModel>,
+ 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<dyn HttpClient>,
+ executor: BackgroundExecutor,
+ api_key: Option<String>,
+ is_anthropic: bool,
+}
+
+impl AnthropicEventReporter {
+ pub fn new(model: &Arc<dyn crate::LanguageModel>, 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<dyn HttpClient>,
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,
@@ -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"] }
@@ -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;
@@ -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<Self>) -> Task<Result<(), AuthenticateError>> {
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::<SettingsStore>(|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 {
@@ -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())
@@ -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
@@ -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<Result<()>> {
@@ -1474,92 +1482,3 @@ mod tests {
);
}
}
-struct ConfigurationView {
- copilot_status: Option<copilot::Status>,
- state: Entity<State>,
- _subscription: Option<Subscription>,
-}
-
-impl ConfigurationView {
- pub fn new(state: Entity<State>, cx: &mut Context<Self>) -> 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<Self>) -> 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(),
- }
- }
- }
-}
@@ -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<Self>) -> Task<Result<(), AuthenticateError>> {
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::<SettingsStore>(|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",
)),
)
@@ -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<Self>) -> Task<Result<(), AuthenticateError>> {
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::<SettingsStore>(|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(
@@ -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(
@@ -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<dyn HttpClient>,
- state: Entity<State>,
+ pub state: Entity<State>,
}
pub struct State {
api_key_state: ApiKeyState,
- codestral_api_key_state: ApiKeyState,
+ codestral_api_key_state: Entity<ApiKeyState>,
+}
+
+struct CodestralApiKey(Entity<ApiKeyState>);
+impl Global for CodestralApiKey {}
+
+pub fn codestral_api_key(cx: &mut App) -> Entity<ApiKeyState> {
+ if cx.has_global::<CodestralApiKey>() {
+ cx.global::<CodestralApiKey>().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<String>,
- cx: &mut Context<Self>,
- ) -> Task<Result<()>> {
- 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<Self>) -> Task<Result<(), AuthenticateError>> {
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<Self>,
) -> Task<Result<(), AuthenticateError>> {
- 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::<SettingsStore>(|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<Arc<str>> {
- 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<dyn LanguageModel> {
@@ -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<InputField>,
- codestral_api_key_editor: Entity<InputField>,
state: Entity<State>,
load_credentials_task: Option<Task<()>>,
}
@@ -756,8 +745,6 @@ impl ConfigurationView {
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> 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<Self>,
- ) {
- 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>) {
- 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<Self>) -> bool {
!self.state.read(cx).is_authenticated()
}
-
- fn render_codestral_api_key_editor(&mut self, cx: &mut Context<Self>) -> 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 = <dyn 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::*;
@@ -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<Self>) -> Task<Result<(), AuthenticateError>> {
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",
)),
)
@@ -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<Self>) -> Task<Result<(), AuthenticateError>> {
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::<SettingsStore>(|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(
@@ -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<str>,
- api_key_env_var: EnvVar,
api_key_state: ApiKeyState,
settings: OpenAiCompatibleSettings,
}
@@ -56,12 +53,8 @@ impl State {
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
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<Self>) -> 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()
@@ -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<Self>) -> Task<Result<(), AuthenticateError>> {
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(
@@ -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<Self>) -> Task<Result<(), AuthenticateError>> {
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::<SettingsStore>(|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(
@@ -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<Self>) -> Task<Result<(), AuthenticateError>> {
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::<SettingsStore>(|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(
@@ -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;
@@ -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<SharedString>,
- button_link: Option<String>,
-}
-
-impl InstructionListItem {
- pub fn new(
- label: impl Into<SharedString>,
- button_label: Option<impl Into<SharedString>>,
- button_link: Option<impl Into<String>>,
- ) -> 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<SharedString>) -> 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()
- }
-}
@@ -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
@@ -1,5 +1,6 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
+("<" @open ">" @close)
(("\"" @open "\"" @close) (#set! rainbow.exclude))
(("'" @open "'" @close) (#set! rainbow.exclude))
@@ -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
@@ -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")
+)
@@ -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!(
"\"{}\"",
@@ -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,
@@ -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())),
@@ -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();
}
}
@@ -43,8 +43,8 @@ impl PathKey {
}
impl MultiBuffer {
- pub fn paths(&self) -> impl Iterator<Item = PathKey> + '_ {
- self.excerpts_by_path.keys().cloned()
+ pub fn paths(&self) -> impl Iterator<Item = &PathKey> + '_ {
+ self.excerpts_by_path.keys()
}
pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
@@ -58,15 +58,18 @@ impl MultiBuffer {
}
}
- pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option<Anchor> {
+ pub fn buffer_for_path(&self, path: &PathKey, cx: &App) -> Option<Entity<Buffer>> {
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<Item = &PathKey> {
- self.excerpts_by_path.keys()
+ pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option<Anchor> {
+ 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.
@@ -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| {
@@ -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<ContextMenu>, Point<Pixels>, Subscription)>,
focus_handle: FocusHandle,
pending_serialization: Task<Option<()>>,
@@ -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<Self>) {
+ 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<Self>) {
+ 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<Self>,
+ ) {
+ 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<Self>) {
+ 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<Self>,
+ ) {
+ 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<Self>) {
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<usize>, 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))
@@ -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"))]
@@ -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,
@@ -217,14 +217,13 @@ impl ProjectPicker {
connection: RemoteConnectionOptions,
project: Entity<Project>,
home_dir: RemotePathBuf,
- path_style: PathStyle,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<RemoteServerProjects>,
) -> Entity<Self> {
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<Project>,
home_dir: RemotePathBuf,
- path_style: PathStyle,
window: &mut Window,
cx: &mut Context<Self>,
workspace: WeakEntity<Workspace>,
@@ -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,
@@ -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 {
@@ -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<T>`): Remain as `Option<T>`
/// - **Regular fields**: Become `Option<T>`
///
@@ -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)]
@@ -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
@@ -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;
@@ -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);
}
@@ -286,6 +286,10 @@ pub struct TitleBarSettingsContent {
///
/// Default: true
pub show_sign_in: Option<bool>,
+ /// Whether to show the user menu button in the title bar.
+ ///
+ /// Default: true
+ pub show_user_menu: Option<bool>,
/// Whether to show the menus in the title bar.
///
/// Default: false
@@ -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<DockPosition>,
+ /// Where to dock the utility pane (the thread view pane).
+ ///
+ /// Default: left
+ pub agents_panel_dock: Option<DockSide>,
/// 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<LanguageModelSelection>,
/// Model to use for the inline assistant. Defaults to default_model when not specified.
pub inline_assistant_model: Option<LanguageModelSelection>,
- /// 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<bool>,
+ /// Model to use for generating git commit messages.
+ ///
+ /// Default: true
pub commit_message_model: Option<LanguageModelSelection>,
/// Model to use for generating thread summaries. Defaults to default_model when not specified.
pub thread_summary_model: Option<LanguageModelSelection>,
@@ -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 {
@@ -186,22 +186,20 @@ pub struct CopilotSettingsContent {
pub enterprise_uri: Option<String>,
}
+#[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<String>,
/// Maximum tokens to generate.
///
/// Default: 150
- #[serde(default)]
pub max_tokens: Option<u32>,
/// Api URL to use for completions.
///
/// Default: "https://codestral.mistral.ai"
- #[serde(default)]
pub api_url: Option<String>,
}
@@ -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
@@ -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;
@@ -13,6 +13,7 @@ pub struct SettingsInputField {
tab_index: Option<isize>,
}
+// TODO: Update the `ui_input::InputField` to use `window.use_state` and `RenceOnce` and remove this component
impl SettingsInputField {
pub fn new() -> Self {
Self {
@@ -0,0 +1,56 @@
+use gpui::{IntoElement, ParentElement, Styled};
+use ui::{Divider, DividerColor, prelude::*};
+
+#[derive(IntoElement)]
+pub struct SettingsSectionHeader {
+ icon: Option<IconName>,
+ label: SharedString,
+ no_padding: bool,
+}
+
+impl SettingsSectionHeader {
+ pub fn new(label: impl Into<SharedString>) -> 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))
+ }
+}
@@ -2330,8 +2330,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
// 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<SettingsPage> {
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<SettingsPage> {
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<SettingsPage> {
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<SettingsPageItem> {
fn edit_prediction_language_settings_section() -> Vec<SettingsPageItem> {
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<SettingsPageItem> {
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 {
@@ -0,0 +1,2 @@
+mod edit_prediction_provider_setup;
+pub use edit_prediction_provider_setup::EditPredictionSetupPage;
@@ -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<SettingsWindow>,
+ scroll_handle: ScrollHandle,
+}
+
+impl EditPredictionSetupPage {
+ pub fn new(settings_window: Entity<SettingsWindow>) -> Self {
+ Self {
+ settings_window,
+ scroll_handle: ScrollHandle::new(),
+ }
+ }
+}
+
+impl Render for EditPredictionSetupPage {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let settings_window = self.settings_window.clone();
+
+ let providers = [
+ Some(render_github_copilot_provider(window, cx).into_any_element()),
+ cx.has_flag::<Zeta2FeatureFlag>().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::<Zeta2FeatureFlag>().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<ApiKeyState>,
+ current_url: fn(&mut App) -> SharedString,
+ additional_fields: Option<AnyElement>,
+ window: &mut Window,
+ cx: &mut Context<EditPredictionSetupPage>,
+) -> 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<String>, 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)
+}
@@ -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<bool>,
}
-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<ElementId>,
+ 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<SharedString>,
+ /// 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<SettingsWindow>) -> 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<Item = (usize, &'a SettingsPageItem)>>(
+ fn render_sub_page_items<'a, Items>(
&self,
items: Items,
page_index: Option<usize>,
window: &mut Window,
cx: &mut Context<SettingsWindow>,
- ) -> impl IntoElement {
- let mut page_content = v_flex()
+ ) -> impl IntoElement
+ where
+ Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
+ {
+ 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<usize>,
+ window: &mut Window,
+ cx: &mut Context<SettingsWindow>,
+ ) -> impl IntoElement
+ where
+ Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
+ {
+ 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<Div>,
+ items: Items,
+ page_index: Option<usize>,
+ window: &mut Window,
+ cx: &mut Context<SettingsWindow>,
+ ) -> impl IntoElement
+ where
+ Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
+ {
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();
@@ -250,11 +250,11 @@ impl<T: Item> SumTree<T> {
<T::Summary as Summary>::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<T: Item> SumTree<T> {
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: <T::Summary as 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: <T::Summary as 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();
<T::Summary as 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<T: Item> SumTree<T> {
Self::new(cx)
} else {
debug_assert_eq!(nodes.len(), 1);
- Self(Arc::new(nodes.pop().unwrap()))
+ nodes.pop().unwrap()
}
}
@@ -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
@@ -420,6 +420,10 @@ impl TerminalBuilder {
) -> Task<Result<TerminalBuilder>> {
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
@@ -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<T: EventListener>(
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/Ῥόδος/
@@ -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<Self>) {
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);
@@ -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<Self>) -> Option<SharedString> {
+ 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<Self>) -> Option<AnyElement> {
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<Self>) -> 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<Self>) -> Option<impl IntoElement> {
+ pub fn render_project_repo(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
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::<String>()
})
})?;
+ 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<Self>) -> impl Element {
+ pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
let user_store = self.user_store.read(cx);
let user = user_store.current_user();
@@ -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(),
}
}
@@ -128,67 +128,61 @@ impl AddToolchainState {
) -> (OpenPathDelegate, oneshot::Receiver<Option<Vec<PathBuf>>>) {
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)
}
@@ -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::*;
@@ -0,0 +1,3 @@
+mod configured_api_card;
+
+pub use configured_api_card::*;
@@ -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<SharedString>,
+ button_tab_index: Option<isize>,
tooltip_label: Option<SharedString>,
disabled: bool,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
@@ -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)
@@ -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::*;
@@ -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<SharedString>, link: impl Into<String>) -> 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<AnyElement> {
+ 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(),
+ )
+ }
+}
@@ -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 {
@@ -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("<div>hey</div>");
+/// ```
+#[derive(IntoElement, RegisterComponent)]
+pub struct InlineCode {
+ label: SharedString,
+ label_size: LabelSize,
+}
+
+impl InlineCode {
+ pub fn new(label: impl Into<SharedString>) -> 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<AnyElement> {
+ Some(
+ v_flex()
+ .gap_6()
+ .child(
+ example_group(vec![single_example(
+ "Simple",
+ InlineCode::new("zed.dev").into_any_element(),
+ )])
+ .vertical(),
+ )
+ .into_any_element(),
+ )
+ }
+}
@@ -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)
@@ -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
@@ -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<Color>,
+ children: Vec<AnyElement>,
}
impl ListBulletItem {
pub fn new(label: impl Into<SharedString>) -> 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<Item = AnyElement>) {
+ 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<AnyElement> {
+ 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(),
)
}
@@ -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<ScrollHandle>,
}
@@ -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<Item = impl IntoElement>) -> 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),
+ )
+ }),
+ )
+ },
+ )
}
}
@@ -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
@@ -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);
@@ -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));
@@ -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
@@ -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<MinimizePane> + EventEmitter<ClosePane> + 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<Self>);
+ fn width(&self, cx: &App) -> Pixels;
+ fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
+}
+
+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<Pixels>, cx: &mut App);
+ fn to_any(&self) -> AnyView;
+ fn box_clone(&self) -> Box<dyn UtilityPaneHandle>;
+}
+
+impl<T> UtilityPaneHandle for Entity<T>
+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<Pixels>, 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<dyn UtilityPaneHandle> {
+ Box::new(self.clone())
+ }
+}
+
+pub enum UtilityPanePosition {
+ Left,
+ Right,
+}
+
pub trait Panel: Focusable + EventEmitter<PanelEvent> + 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<dyn PanelHandle>> {
+ 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<Self>) -> anyhow::Result<usize> {
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();
@@ -41,7 +41,7 @@ pub enum NotificationId {
impl NotificationId {
/// Returns a unique [`NotificationId`] for the given type.
- pub fn unique<T: 'static>() -> Self {
+ pub const fn unique<T: 'static>() -> Self {
Self::Unique(TypeId::of::<T>())
}
@@ -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<ProjectItemKind, Box<dyn Any + Send>>,
+
+ 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<Pane>) -> 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::<AgentV2FeatureFlag>()
+ && 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::<AgentV2FeatureFlag>()
+ && 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!"
@@ -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<Pane>) -> 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<Pane>,
new_pane: &Entity<Pane>,
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<Pane>) -> Option<Bounds<Pixels>> {
@@ -90,6 +104,7 @@ impl PaneGroup {
&mut self,
active_pane: &Entity<Pane>,
direction: SplitDirection,
+ cx: &mut App,
) -> Result<bool> {
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<Pane>) -> Result<bool> {
+ pub fn remove(&mut self, pane: &Entity<Pane>, cx: &mut App) -> Result<bool> {
+ let result = self.remove_internal(pane);
+ if let Ok(true) = result {
+ self.mark_positions(cx);
+ }
+ result
+ }
+
+ fn remove_internal(&mut self, pane: &Entity<Pane>) -> Result<bool> {
match &mut self.root {
Member::Pane(_) => Ok(false),
Member::Axis(axis) => {
@@ -151,6 +176,7 @@ impl PaneGroup {
direction: Axis,
amount: Pixels,
bounds: &Bounds<Pixels>,
+ 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<Pane>, to: &Entity<Pane>) {
+ pub fn swap(&mut self, from: &Entity<Pane>, to: &Entity<Pane>, 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<Pane>),
}
+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<Project>,
@@ -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<dyn UtilityPaneHandle>,
+ _subscriptions: Vec<Subscription>,
+}
+
+#[derive(Default)]
+pub struct UtilityPaneState {
+ left_slot: Option<UtilityPaneSlotState>,
+ right_slot: Option<UtilityPaneSlotState>,
+}
+
+#[derive(Clone)]
+pub struct DraggedUtilityPane(pub UtilityPaneSlot);
+
+impl Render for DraggedUtilityPane {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> 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<Self>,
+ ) {
+ 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<T: UtilityPane>(
+ &mut self,
+ slot: UtilityPaneSlot,
+ panel_id: EntityId,
+ handle: gpui::Entity<T>,
+ cx: &mut Context<Self>,
+ ) {
+ 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<dyn UtilityPaneHandle> = 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<Self>) {
+ 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<Self>,
+ ) {
+ 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<Self>,
+ ) {
+ 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<Self>,
+ ) {
+ 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<Workspace>,
+ slot: UtilityPaneSlot,
+ handle: Box<dyn UtilityPaneHandle>,
+}
+
+impl UtilityPaneFrame {
+ pub fn new(
+ slot: UtilityPaneSlot,
+ handle: Box<dyn UtilityPaneHandle>,
+ cx: &mut Context<Workspace>,
+ ) -> 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()
+ }
+}
@@ -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<AppState>, 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<Task<()>>,
last_open_dock_positions: Vec<DockPosition>,
removing: bool,
+ utility_panes: UtilityPaneState,
}
impl EventEmitter<Event> 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<Self>) {
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<Self>) {
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>) {
- self.center.reset_pane_sizes();
+ self.center.reset_pane_sizes(cx);
cx.notify();
}
@@ -4241,7 +4254,7 @@ impl Workspace {
) -> Entity<Pane> {
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<Self>,
) {
- 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<DraggedUtilityPane>,
+ 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::<AgentV2FeatureFlag>(), |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::<AgentV2FeatureFlag>(), |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::<AgentV2FeatureFlag>(), |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::<AgentV2FeatureFlag>(), |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::<AgentV2FeatureFlag>(), |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::<AgentV2FeatureFlag>(), |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,
@@ -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
@@ -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);
@@ -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<P: Panel>(
+ workspace: &mut Workspace,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ load_panel: impl FnOnce(
+ WeakEntity<Workspace>,
+ AsyncWindowContext,
+ ) -> Task<anyhow::Result<Entity<P>>>
+ + 'static,
+) -> Task<anyhow::Result<()>> {
+ let disable_ai = SettingsStore::global(cx)
+ .get::<DisableAiSettings>(None)
+ .disable_ai
+ || cfg!(test);
+ let existing_panel = workspace.panel::<P>(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::<DisableAiSettings>(None)
+ .disable_ai;
+ let have_panel = workspace.panel::<P>(cx).is_some();
+ if !disable_ai && !have_panel {
+ workspace.add_panel(panel, window, cx);
+ }
+ })
+ }),
+ (true, Some(existing_panel)) => {
+ workspace.remove_panel::<P>(&existing_panel, window, cx);
+ Task::ready(Ok(()))
+ }
+ _ => Task::ready(Ok(())),
+ }
+}
+
async fn initialize_agent_panel(
workspace_handle: WeakEntity<Workspace>,
prompt_builder: Arc<PromptBuilder>,
mut cx: AsyncWindowContext,
) -> anyhow::Result<()> {
- fn setup_or_teardown_agent_panel(
- workspace: &mut Workspace,
- prompt_builder: Arc<PromptBuilder>,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) -> Task<anyhow::Result<()>> {
- let disable_ai = SettingsStore::global(cx)
- .get::<DisableAiSettings>(None)
- .disable_ai
- || cfg!(test);
- let existing_panel = workspace.panel::<agent_ui::AgentPanel>(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::<DisableAiSettings>(None)
- .disable_ai;
- let have_panel = workspace.panel::<agent_ui::AgentPanel>(cx).is_some();
- if !disable_ai && !have_panel {
- workspace.add_panel(panel, window, cx);
- }
- })
- }),
- (true, Some(existing_panel)) => {
- workspace.remove_panel::<agent_ui::AgentPanel>(&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::<SettingsStore>(window, {
+ let prompt_builder = prompt_builder.clone();
+ cx.observe_global_in::<SettingsStore>(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<Workspace>,
+ 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::<SettingsStore>(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<AppState>,
workspace: &mut Workspace,
@@ -1052,6 +1087,18 @@ fn register_actions(
workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
},
)
+ .register_action(
+ |workspace: &mut Workspace,
+ _: &zed_actions::agent::ToggleAgentPane,
+ window: &mut Window,
+ cx: &mut Context<Workspace>| {
+ if let Some(panel) = workspace.panel::<AgentsPanel>(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);
@@ -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,
]
);
}
@@ -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> = 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())
};
}
@@ -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
@@ -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"
+ }
+}
+```
@@ -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`.
@@ -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:<br><ul><li>"always": use for all operations</li><li>"never": only use when explicitly specified</li><li>"on_yank": use for yank operations</li></ul> | "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": "🧟♀️"
@@ -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
},
@@ -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(),
})));
}
@@ -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,
})));
}
@@ -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,
})));
}
@@ -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"
@@ -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 <slightknack@gmail.com>"]
repository = "https://github.com/zed-industries/zed"
@@ -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,
})));
}
@@ -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,
})));
}
@@ -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"
@@ -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 <support@zed.dev>"]
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"]
@@ -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::*;
@@ -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<String>,
+}
+
+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<zed::Command> {
+ 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(),
+ })
+ }
+}
@@ -0,0 +1,52 @@
+use zed_extension_api::{self as zed, Result, settings::LspSettings};
+
+pub(crate) struct ProtobufLanguageServer {
+ cached_binary_path: Option<String>,
+}
+
+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<zed::Command> {
+ 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))
+ }
+ }
+}
@@ -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<String>,
+}
+
+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<zed::Command> {
+ 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,
+ })
+ }
+}
@@ -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(())
+}
@@ -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<Vec<String>>,
-}
-
-struct ProtobufExtension;
-
-impl ProtobufExtension {
- fn language_server_binary(
- &self,
- _language_server_id: &zed::LanguageServerId,
- worktree: &zed::Worktree,
- ) -> Result<ProtobufLanguageServerBinary> {
- 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<ProtobufLanguageServer>,
+ protols: Option<ProtoLs>,
+ buf_lsp: Option<BufLsp>,
}
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<zed_extension_api::Command> {
- 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<Option<zed::serde_json::Value>> {
- 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<Option<zed_extension_api::serde_json::Value>> {
- 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)
}
}
@@ -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",