Compare commits

...
Sign in to create a new pull request.

17 commits

Author SHA1 Message Date
Danilo Leal
bd4e943597
acp: Add onboarding modal & title bar banner (#36784)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-08-26 16:59:12 -03:00
Danilo Leal
c5d3c7d790
thread view: Improve agent installation UI (#36957)
Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-26 16:58:23 -03:00
张小白
fff0ecead1
windows: Fix keystroke & keymap (#36572)
Closes #36300

This PR follows Windows conventions by introducing
`KeybindingKeystroke`, so shortcuts now show up as `ctrl-shift-4`
instead of `ctrl-$`.

It also fixes issues with keyboard layouts: when `use_key_equivalents`
is set to true, keys are remapped based on their virtual key codes. For
example, `ctrl-\` on a standard English layout will be mapped to
`ctrl-ё` on a Russian layout.


Release Notes:

- N/A

---------

Co-authored-by: Kate <kate@zed.dev>
2025-08-27 03:24:50 +08:00
Max Brunsfeld
b1b60bb7fe
Work around duplicate ssh projects in workspace migration (#36946)
Fixes another case where the sqlite migration could fail, reported by
@SomeoneToIgnore.

Release Notes:

- N/A
2025-08-26 10:54:39 -07:00
Adam Mulvany
0e575b2809
helix: Fix buffer search: deploy reset to normal mode (#36917)
## Fix: Preserve Helix mode when using  search

### Problem
When using `buffer search: deploy` in Helix mode, pressing Enter to
dismiss the search incorrectly returned to Vim NORMAL mode instead of
Helix NORMAL mode.

### Root Cause
The `search_deploy` function was resetting the entire `SearchState` to
default values when buffer search: deploy was activated. Since the
default `Mode` is `Normal`, this caused `prior_mode` to be set to Vim's
Normal mode regardless of the actual mode before search.

### Solution
Modified `search_deploy` to preserve the current mode when resetting
search state:
- Store the current mode before resetting
- Reset search state to default
- Restore the saved mode to `prior_mode`

This ensures the editor returns to the correct mode (Helix NORMAL or Vim
NORMAL) after dismissing buffer search.

### Settings

I was able to reproduce and then test the fix was successful with the
following config and have also tested with vim: default_mode commented
out to ensure that's not influencing the mode selection flow:

```
  "helix_mode": true,
  "vim_mode": true,
  "vim": {
    "default_mode": "helix_normal"
  },
```

This is on Kubuntu 24.04.

The following test combinations pass locally:

- `cargo test -p search`
- `cargo test -p vim` 
- `cargo test -p editor`
- `cargo test -p workspace`
- `cargo test -p gpui -- vim`
- `cargo test -p gpui -- helix`

Release Notes:

- Fixed Helix mode switching to Vim normal mode after using `buffer
search: deploy` to search

Closes #36872
2025-08-26 10:38:53 -06:00
Danilo Leal
65c6c709fd
thread view: Refine tool call UI (#36937)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-08-26 12:55:40 -03:00
Bennet Bo Fenner
858ab9cc23
Revert "ai: Auto select user model when there's no default" (#36932)
Reverts zed-industries/zed#36722

Release Notes:

- N/A
2025-08-26 13:55:09 +00:00
Daniel Martín
2c64b05ea4
emacs: Add editor::FindAllReferences keybinding (#36840)
This commit maps `editor::FindAllReferences` to Alt+? in the Emacs
keymap.

Release Notes:

- N/A
2025-08-26 13:43:58 +00:00
Peter Tripp
b7dad2cf71
Fix initial_tasks.json triggering diagnostic warning (#36523)
`zed::OpenProjectTasks` without an existing tasks.json will recreate it
from the template.
This file will immediately show a warning.

<img width="810" height="168" alt="Screenshot 2025-08-19 at 17 16 07"
src="https://github.com/user-attachments/assets/bbc8c7a0-7036-4927-8e85-b81b79aeaacb"
/>

Release Notes:

- N/A
2025-08-26 13:41:57 +00:00
Peter Tripp
76dbcde628
Support disabling drag-and-drop in Project Panel (#36719)
Release Notes:

- Added setting for disabling drag and drop in project panel. `{
"project_panel": {"drag_and_drop": false } }`
2025-08-26 13:35:45 +00:00
Peter Tripp
aa0f7a2d09
Fix conflicts in Linux default keymap (#36519)
Closes https://github.com/zed-industries/zed/issues/29746

| Action | New Key | Old Key | Former Conflict |
| - | - | - | - |
| `edit_prediction::ToggleMenu` | `ctrl-alt-shift-i` | `ctrl-shift-i` |
`editor::Format` |
| `editor::ToggleEditPrediction` | `ctrl-alt-shift-e` | `ctrl-shift-e` |
`project_panel::ToggleFocus` |

These aren't great keys and I'm open to alternate suggestions, but the
will work out of the box without conflict.

Release Notes:

- N/A
2025-08-26 09:33:42 -04:00
Bennet Bo Fenner
372b3c7af6
acp: Enable feature flag for everyone (#36928)
Release Notes:

- N/A
2025-08-26 15:30:26 +02:00
Bennet Bo Fenner
10a1140d49
acp: Improve matching logic when adding new entry to agent_servers (#36926)
Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-26 11:18:50 +00:00
Bennet Bo Fenner
e96b68bc15
acp: Polish UI (#36927)
Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-26 10:55:45 +00:00
Ben Brandt
b249593abe
agent2: Always finalize diffs from the edit tool (#36918)
Previously, we wouldn't finalize the diff if an error occurred during
editing or the tool call was canceled.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-26 09:46:29 +00:00
Bennet Bo Fenner
c14d84cfdb
acp: Add button to configure custom agent in the configuration view (#36923)
Release Notes:

- N/A
2025-08-26 09:20:33 +00:00
Dan Dascalescu
428fc6d483
chore: Fix typo in 10_bug_report.yml (#36922)
Release Notes:

- N/A
2025-08-26 11:05:40 +02:00
68 changed files with 6036 additions and 2328 deletions

View file

@ -14,7 +14,7 @@ body:
### Description ### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. <!-- Describe with sufficient detail to reproduce from a clean Zed install.
- Any code must be sufficient to reproduce (include context!) - Any code must be sufficient to reproduce (include context!)
- Code must as text, not just as a screenshot. - Include code as text, not just as a screenshot.
- Issues with insufficient detail may be summarily closed. - Issues with insufficient detail may be summarily closed.
--> -->

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 12.375H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 11.125L6.75003 7.375L3 3.62497" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 336 B

1257
assets/images/acp_grid.svg Normal file

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 176 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="61" fill="none"><g clip-path="url(#a)"><path fill="#000" d="M130.75.385c5.428 0 10.297 2.81 13.011 7.511l14.214 24.618-.013-.005c2.599 4.504 2.707 9.932.28 14.513-2.618 4.944-7.862 8.015-13.679 8.015h-31.811c-.452 0-.873-.242-1.103-.637a1.268 1.268 0 0 1 0-1.274l3.919-6.78c.223-.394.65-.636 1.102-.636h28.288a5.622 5.622 0 0 0 4.925-2.849 5.615 5.615 0 0 0 0-5.69l-14.214-24.617a5.621 5.621 0 0 0-4.925-2.848 5.621 5.621 0 0 0-4.925 2.848l-14.214 24.618a6.267 6.267 0 0 0-.319.643.998.998 0 0 1-.069.14L101.724 54.4l-.823 1.313-2.529 4.39a1.27 1.27 0 0 1-1.103.636h-7.83c-.452 0-.873-.242-1.102-.637-.23-.394-.23-.879 0-1.274l2.188-3.791H66.803c-3.32 0-6.454-1.122-8.818-3.167a17.141 17.141 0 0 1-3.394-3.96 1.261 1.261 0 0 1-.091-.137L34.2 12.573a5.622 5.622 0 0 0-4.925-2.849 5.621 5.621 0 0 0-4.924 2.85L10.137 37.19a5.615 5.615 0 0 0 0 5.69 5.63 5.63 0 0 0 4.925 2.841h29.862a1.276 1.276 0 0 1 1.102 1.912l-3.912 6.778a1.27 1.27 0 0 1-1.102.638H14.495c-3.32 0-6.454-1.128-8.817-3.173-5.906-5.104-7.36-12.883-3.62-19.363L16.267 7.89C18.872 3.385 23.517.583 28.697.39c.184-.006.356-.006.534-.006 5.378 0 10.45 3.007 13.246 7.85l12.986 22.372L68.58 7.891C71.186 3.385 75.83.582 81.01.39c.185-.006.358-.006.536-.006 4.453 0 8.71 2.039 11.672 5.588.337.407.388.98.127 1.446l-3.765 6.6a1.268 1.268 0 0 1-2.205.006l-.847-1.465a5.623 5.623 0 0 0-4.926-2.848 5.622 5.622 0 0 0-4.924 2.848L62.464 37.18a5.614 5.614 0 0 0 0 5.689 5.628 5.628 0 0 0 4.925 2.842H95.91L117.76 7.87c2.714-4.683 7.575-7.486 12.99-7.486Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .385h160v60.36H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -40,7 +40,7 @@
"shift-f11": "debugger::StepOut", "shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen", "f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions", "ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-shift-i": "edit_prediction::ToggleMenu", "ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu" "ctrl-alt-l": "lsp_tool::ToggleMenu"
} }
}, },
@ -120,7 +120,7 @@
"alt-g m": "git::OpenModifiedFiles", "alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu", "menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu", "shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction", "ctrl-alt-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint", "f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint" "shift-f9": "editor::EditLogBreakpoint"
} }

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,7 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments", "ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions "alt-.": "editor::GoToDefinition", // xref-find-definitions
"alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack "alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char "ctrl-d": "editor::Delete", // delete-char

View file

@ -38,6 +38,7 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments", "ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions "alt-.": "editor::GoToDefinition", // xref-find-definitions
"alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack "alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char "ctrl-d": "editor::Delete", // delete-char

View file

@ -653,6 +653,8 @@
// "never" // "never"
"show": "always" "show": "always"
}, },
// Whether to enable drag-and-drop operations in the project panel.
"drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window. // Whether to hide the root entry when only one folder is open in the window.
"hide_root": false "hide_root": false
}, },

View file

@ -43,8 +43,8 @@
// "args": ["--login"] // "args": ["--login"]
// } // }
// } // }
"shell": "system", "shell": "system"
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once. // Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
"tags": [] // "tags": []
} }
] ]

View file

@ -664,7 +664,7 @@ impl Thread {
} }
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> { pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
if self.configured_model.is_none() || self.messages.is_empty() { if self.configured_model.is_none() {
self.configured_model = LanguageModelRegistry::read_global(cx).default_model(); self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
} }
self.configured_model.clone() self.configured_model.clone()
@ -2097,7 +2097,7 @@ impl Thread {
} }
pub fn summarize(&mut self, cx: &mut Context<Self>) { pub fn summarize(&mut self, cx: &mut Context<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
println!("No thread summary model"); println!("No thread summary model");
return; return;
}; };
@ -2416,7 +2416,7 @@ impl Thread {
} }
let Some(ConfiguredModel { model, provider }) = let Some(ConfiguredModel { model, provider }) =
LanguageModelRegistry::read_global(cx).thread_summary_model(cx) LanguageModelRegistry::read_global(cx).thread_summary_model()
else { else {
return; return;
}; };
@ -5410,10 +5410,13 @@ fn main() {{
}), }),
cx, cx,
); );
registry.set_thread_summary_model(Some(ConfiguredModel { registry.set_thread_summary_model(
Some(ConfiguredModel {
provider, provider,
model: model.clone(), model: model.clone(),
})); }),
cx,
);
}) })
}); });

View file

@ -228,7 +228,7 @@ impl NativeAgent {
) -> Entity<AcpThread> { ) -> Entity<AcpThread> {
let connection = Rc::new(NativeAgentConnection(cx.entity())); let connection = Rc::new(NativeAgentConnection(cx.entity()));
let registry = LanguageModelRegistry::read_global(cx); let registry = LanguageModelRegistry::read_global(cx);
let summarization_model = registry.thread_summary_model(cx).map(|c| c.model); let summarization_model = registry.thread_summary_model().map(|c| c.model);
thread_handle.update(cx, |thread, cx| { thread_handle.update(cx, |thread, cx| {
thread.set_summarization_model(summarization_model, cx); thread.set_summarization_model(summarization_model, cx);
@ -524,7 +524,7 @@ impl NativeAgent {
let registry = LanguageModelRegistry::read_global(cx); let registry = LanguageModelRegistry::read_global(cx);
let default_model = registry.default_model().map(|m| m.model); let default_model = registry.default_model().map(|m| m.model);
let summarization_model = registry.thread_summary_model(cx).map(|m| m.model); let summarization_model = registry.thread_summary_model().map(|m| m.model);
for session in self.sessions.values_mut() { for session in self.sessions.values_mut() {
session.thread.update(cx, |thread, cx| { session.thread.update(cx, |thread, cx| {

View file

@ -472,7 +472,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
tool_name: ToolRequiringPermission::name().into(), tool_name: ToolRequiringPermission::name().into(),
is_error: true, is_error: true,
content: "Permission to run tool denied by user".into(), content: "Permission to run tool denied by user".into(),
output: None output: Some("Permission to run tool denied by user".into())
}) })
] ]
); );
@ -1822,11 +1822,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
let clock = Arc::new(clock::FakeSystemClock::new()); let clock = Arc::new(clock::FakeSystemClock::new());
let client = Client::new(clock, http_client, cx); let client = Client::new(clock, http_client, cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
Project::init_settings(cx);
agent_settings::init(cx);
language_model::init(client.clone(), cx); language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx); language_models::init(user_store, client.clone(), cx);
Project::init_settings(cx);
LanguageModelRegistry::test(cx); LanguageModelRegistry::test(cx);
agent_settings::init(cx);
}); });
cx.executor().forbid_parking(); cx.executor().forbid_parking();

View file

@ -732,7 +732,17 @@ impl Thread {
stream.update_tool_call_fields( stream.update_tool_call_fields(
&tool_use.id, &tool_use.id,
acp::ToolCallUpdateFields { acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed), status: Some(
tool_result
.as_ref()
.map_or(acp::ToolCallStatus::Failed, |result| {
if result.is_error {
acp::ToolCallStatus::Failed
} else {
acp::ToolCallStatus::Completed
}
}),
),
raw_output: output, raw_output: output,
..Default::default() ..Default::default()
}, },
@ -1557,7 +1567,7 @@ impl Thread {
tool_name: tool_use.name, tool_name: tool_use.name,
is_error: true, is_error: true,
content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())),
output: None, output: Some(error.to_string().into()),
}, },
} }
})) }))
@ -2459,6 +2469,30 @@ impl ToolCallEventStreamReceiver {
} }
} }
pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields {
let event = self.0.next().await;
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
update,
)))) = event
{
update.fields
} else {
panic!("Expected update fields but got: {:?}", event);
}
}
pub async fn expect_diff(&mut self) -> Entity<acp_thread::Diff> {
let event = self.0.next().await;
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff(
update,
)))) = event
{
update.diff
} else {
panic!("Expected diff but got: {:?}", event);
}
}
pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> { pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> {
let event = self.0.next().await; let event = self.0.next().await;
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal( if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(

View file

@ -273,6 +273,13 @@ impl AgentTool for EditFileTool {
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?; let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
event_stream.update_diff(diff.clone()); event_stream.update_diff(diff.clone());
let _finalize_diff = util::defer({
let diff = diff.downgrade();
let mut cx = cx.clone();
move || {
diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
}
});
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx let old_text = cx
@ -389,8 +396,6 @@ impl AgentTool for EditFileTool {
}) })
.await; .await;
diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
let input_path = input.path.display(); let input_path = input.path.display();
if unified_diff.is_empty() { if unified_diff.is_empty() {
anyhow::ensure!( anyhow::ensure!(
@ -1545,6 +1550,100 @@ mod tests {
); );
} }
#[gpui::test]
async fn test_diff_finalization(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/", json!({"main.rs": ""})).await;
let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
let languages = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
Templates::new(),
Some(model.clone()),
cx,
)
});
// Ensure the diff is finalized after the edit completes.
{
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
cx.run_until_parked();
model.end_last_completion_stream();
edit.await.unwrap();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
}
// Ensure the diff is finalized if an error occurs while editing.
{
model.forbid_requests();
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
edit.await.unwrap_err();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
model.allow_requests();
}
// Ensure the diff is finalized if the tool call gets dropped.
{
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
drop(edit);
cx.run_until_parked();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
}
}
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);

View file

@ -6,7 +6,7 @@ use agent2::HistoryStore;
use collections::HashMap; use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility}; use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{ use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
TextStyleRefinement, WeakEntity, Window, TextStyleRefinement, WeakEntity, Window,
}; };
use language::language_settings::SoftWrap; use language::language_settings::SoftWrap;
@ -154,10 +154,22 @@ impl EntryViewState {
}); });
} }
} }
AgentThreadEntry::AssistantMessage(_) => { AgentThreadEntry::AssistantMessage(message) => {
if index == self.entries.len() { let entry = if let Some(Entry::AssistantMessage(entry)) =
self.entries.push(Entry::empty()) self.entries.get_mut(index)
} {
entry
} else {
self.set_entry(
index,
Entry::AssistantMessage(AssistantMessageEntry::default()),
);
let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
unreachable!()
};
entry
};
entry.sync(message);
} }
}; };
} }
@ -177,7 +189,7 @@ impl EntryViewState {
pub fn settings_changed(&mut self, cx: &mut App) { pub fn settings_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() { for entry in self.entries.iter() {
match entry { match entry {
Entry::UserMessage { .. } => {} Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
Entry::Content(response_views) => { Entry::Content(response_views) => {
for view in response_views.values() { for view in response_views.values() {
if let Ok(diff_editor) = view.clone().downcast::<Editor>() { if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
@ -208,9 +220,29 @@ pub enum ViewEvent {
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent), MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
} }
#[derive(Default, Debug)]
pub struct AssistantMessageEntry {
scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,
}
impl AssistantMessageEntry {
pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> {
self.scroll_handles_by_chunk_index.get(&ix).cloned()
}
pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
let ix = message.chunks.len() - 1;
let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
handle.scroll_to_bottom();
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum Entry { pub enum Entry {
UserMessage(Entity<MessageEditor>), UserMessage(Entity<MessageEditor>),
AssistantMessage(AssistantMessageEntry),
Content(HashMap<EntityId, AnyEntity>), Content(HashMap<EntityId, AnyEntity>),
} }
@ -218,7 +250,7 @@ impl Entry {
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> { pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
match self { match self {
Self::UserMessage(editor) => Some(editor), Self::UserMessage(editor) => Some(editor),
Entry::Content(_) => None, Self::AssistantMessage(_) | Self::Content(_) => None,
} }
} }
@ -239,6 +271,16 @@ impl Entry {
.map(|entity| entity.downcast::<TerminalView>().unwrap()) .map(|entity| entity.downcast::<TerminalView>().unwrap())
} }
pub fn scroll_handle_for_assistant_message_chunk(
&self,
chunk_ix: usize,
) -> Option<ScrollHandle> {
match self {
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
Self::UserMessage(_) | Self::Content(_) => None,
}
}
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> { fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
match self { match self {
Self::Content(map) => Some(map), Self::Content(map) => Some(map),
@ -254,7 +296,7 @@ impl Entry {
pub fn has_content(&self) -> bool { pub fn has_content(&self) -> bool {
match self { match self {
Self::Content(map) => !map.is_empty(), Self::Content(map) => !map.is_empty(),
Self::UserMessage(_) => false, Self::UserMessage(_) | Self::AssistantMessage(_) => false,
} }
} }
} }

View file

@ -20,11 +20,11 @@ use file_icons::FileIcons;
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage,
prelude::*, pulsating_between, point, prelude::*, pulsating_between,
}; };
use language::Buffer; use language::Buffer;
@ -43,7 +43,7 @@ use text::Anchor;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
}; };
use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace}; use workspace::{CollaboratorId, Workspace};
@ -66,7 +66,6 @@ use crate::{
KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector, KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
}; };
const RESPONSE_PADDING_X: Pixels = px(19.);
pub const MIN_EDITOR_LINES: usize = 4; pub const MIN_EDITOR_LINES: usize = 4;
pub const MAX_EDITOR_LINES: usize = 8; pub const MAX_EDITOR_LINES: usize = 8;
@ -279,6 +278,7 @@ pub struct AcpThreadView {
editing_message: Option<usize>, editing_message: Option<usize>,
prompt_capabilities: Rc<Cell<PromptCapabilities>>, prompt_capabilities: Rc<Cell<PromptCapabilities>>,
is_loading_contents: bool, is_loading_contents: bool,
install_command_markdown: Entity<Markdown>,
_cancel_task: Option<Task<()>>, _cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 3], _subscriptions: [Subscription; 3],
} }
@ -392,6 +392,7 @@ impl AcpThreadView {
hovered_recent_history_item: None, hovered_recent_history_item: None,
prompt_capabilities, prompt_capabilities,
is_loading_contents: false, is_loading_contents: false,
install_command_markdown: cx.new(|cx| Markdown::new("".into(), None, None, cx)),
_subscriptions: subscriptions, _subscriptions: subscriptions,
_cancel_task: None, _cancel_task: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
@ -667,7 +668,12 @@ impl AcpThreadView {
match &self.thread_state { match &self.thread_state {
ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
ThreadState::Loading { .. } => "Loading…".into(), ThreadState::Loading { .. } => "Loading…".into(),
ThreadState::LoadError(_) => "Failed to load".into(), ThreadState::LoadError(error) => match error {
LoadError::NotInstalled { .. } => format!("Install {}", self.agent.name()).into(),
LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(),
LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(),
},
} }
} }
@ -1334,6 +1340,10 @@ impl AcpThreadView {
window: &mut Window, window: &mut Window,
cx: &Context<Self>, cx: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let is_generating = self
.thread()
.is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
let primary = match &entry { let primary = match &entry {
AgentThreadEntry::UserMessage(message) => { AgentThreadEntry::UserMessage(message) => {
let Some(editor) = self let Some(editor) = self
@ -1493,6 +1503,20 @@ impl AcpThreadView {
.into_any() .into_any()
} }
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
let is_last = entry_ix + 1 == total_entries;
let pending_thinking_chunk_ix = if is_generating && is_last {
chunks
.iter()
.enumerate()
.next_back()
.filter(|(_, segment)| {
matches!(segment, AssistantMessageChunk::Thought { .. })
})
.map(|(index, _)| index)
} else {
None
};
let style = default_markdown_style(false, false, window, cx); let style = default_markdown_style(false, false, window, cx);
let message_body = v_flex() let message_body = v_flex()
.w_full() .w_full()
@ -1511,6 +1535,7 @@ impl AcpThreadView {
entry_ix, entry_ix,
chunk_ix, chunk_ix,
md.clone(), md.clone(),
Some(chunk_ix) == pending_thinking_chunk_ix,
window, window,
cx, cx,
) )
@ -1524,7 +1549,7 @@ impl AcpThreadView {
v_flex() v_flex()
.px_5() .px_5()
.py_1() .py_1()
.when(entry_ix + 1 == total_entries, |this| this.pb_4()) .when(is_last, |this| this.pb_4())
.w_full() .w_full()
.text_ui(cx) .text_ui(cx)
.child(message_body) .child(message_body)
@ -1533,7 +1558,7 @@ impl AcpThreadView {
AgentThreadEntry::ToolCall(tool_call) => { AgentThreadEntry::ToolCall(tool_call) => {
let has_terminals = tool_call.terminals().next().is_some(); let has_terminals = tool_call.terminals().next().is_some();
div().w_full().py_1().px_5().map(|this| { div().w_full().map(|this| {
if has_terminals { if has_terminals {
this.children(tool_call.terminals().map(|terminal| { this.children(tool_call.terminals().map(|terminal| {
self.render_terminal_tool_call( self.render_terminal_tool_call(
@ -1609,64 +1634,90 @@ impl AcpThreadView {
entry_ix: usize, entry_ix: usize,
chunk_ix: usize, chunk_ix: usize,
chunk: Entity<Markdown>, chunk: Entity<Markdown>,
pending: bool,
window: &Window, window: &Window,
cx: &Context<Self>, cx: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix)); let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
let card_header_id = SharedString::from("inner-card-header"); let card_header_id = SharedString::from("inner-card-header");
let key = (entry_ix, chunk_ix); let key = (entry_ix, chunk_ix);
let is_open = self.expanded_thinking_blocks.contains(&key); let is_open = self.expanded_thinking_blocks.contains(&key);
let editor_bg = cx.theme().colors().editor_background;
let gradient_overlay = div()
.rounded_b_lg()
.h_full()
.absolute()
.w_full()
.bottom_0()
.left_0()
.bg(linear_gradient(
180.,
linear_color_stop(editor_bg, 1.),
linear_color_stop(editor_bg.opacity(0.2), 0.),
));
let scroll_handle = self
.entry_view_state
.read(cx)
.entry(entry_ix)
.and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
v_flex() v_flex()
.rounded_md()
.border_1()
.border_color(self.tool_card_border_color(cx))
.child( .child(
h_flex() h_flex()
.id(header_id) .id(header_id)
.group(&card_header_id) .group(&card_header_id)
.relative() .relative()
.w_full() .w_full()
.gap_1p5() .py_0p5()
.px_1p5()
.rounded_t_md()
.bg(self.tool_card_header_bg(cx))
.justify_between()
.border_b_1()
.border_color(self.tool_card_border_color(cx))
.child( .child(
h_flex() h_flex()
.size_4() .h(window.line_height())
.justify_center() .gap_1p5()
.child(
div()
.group_hover(&card_header_id, |s| s.invisible().w_0())
.child( .child(
Icon::new(IconName::ToolThink) Icon::new(IconName::ToolThink)
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Muted), .color(Color::Muted),
),
)
.child(
h_flex()
.absolute()
.inset_0()
.invisible()
.justify_center()
.group_hover(&card_header_id, |s| s.visible())
.child(
Disclosure::new(("expand", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronRight)
.on_click(cx.listener({
move |this, _event, _window, cx| {
if is_open {
this.expanded_thinking_blocks.remove(&key);
} else {
this.expanded_thinking_blocks.insert(key);
}
cx.notify();
}
})),
),
),
) )
.child( .child(
div() div()
.text_size(self.tool_name_font_size()) .text_size(self.tool_name_font_size())
.text_color(cx.theme().colors().text_muted) .text_color(cx.theme().colors().text_muted)
.child("Thinking"), .map(|this| {
if pending {
this.child("Thinking")
} else {
this.child("Thought Process")
}
}),
),
)
.child(
Disclosure::new(("expand", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.visible_on_hover(&card_header_id)
.on_click(cx.listener({
move |this, _event, _window, cx| {
if is_open {
this.expanded_thinking_blocks.remove(&key);
} else {
this.expanded_thinking_blocks.insert(key);
}
cx.notify();
}
})),
) )
.on_click(cx.listener({ .on_click(cx.listener({
move |this, _event, _window, cx| { move |this, _event, _window, cx| {
@ -1679,22 +1730,28 @@ impl AcpThreadView {
} }
})), })),
) )
.when(is_open, |this| { .child(
this.child(
div() div()
.relative() .relative()
.mt_1p5() .bg(editor_bg)
.ml(px(7.)) .rounded_b_lg()
.pl_4() .child(
.border_l_1() div()
.border_color(self.tool_card_border_color(cx)) .id(("thinking-content", chunk_ix))
.when_some(scroll_handle, |this, scroll_handle| {
this.track_scroll(&scroll_handle)
})
.p_2()
.when(!is_open, |this| this.max_h_20())
.text_ui_sm(cx) .text_ui_sm(cx)
.overflow_hidden()
.child(self.render_markdown( .child(self.render_markdown(
chunk, chunk,
default_markdown_style(false, false, window, cx), default_markdown_style(false, false, window, cx),
)), )),
) )
}) .when(!is_open && pending, |this| this.child(gradient_overlay)),
)
.into_any_element() .into_any_element()
} }
@ -1705,7 +1762,6 @@ impl AcpThreadView {
window: &Window, window: &Window,
cx: &Context<Self>, cx: &Context<Self>,
) -> Div { ) -> Div {
let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
let card_header_id = SharedString::from("inner-tool-call-header"); let card_header_id = SharedString::from("inner-tool-call-header");
let tool_icon = let tool_icon =
@ -1734,11 +1790,7 @@ impl AcpThreadView {
_ => false, _ => false,
}; };
let failed_tool_call = matches!( let has_location = tool_call.locations.len() == 1;
tool_call.status,
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
);
let needs_confirmation = matches!( let needs_confirmation = matches!(
tool_call.status, tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. } ToolCallStatus::WaitingForConfirmation { .. }
@ -1751,23 +1803,31 @@ impl AcpThreadView {
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
let gradient_overlay = |color: Hsla| { let gradient_overlay = {
div() div()
.absolute() .absolute()
.top_0() .top_0()
.right_0() .right_0()
.w_12() .w_12()
.h_full() .h_full()
.bg(linear_gradient( .map(|this| {
if use_card_layout {
this.bg(linear_gradient(
90., 90.,
linear_color_stop(color, 1.), linear_color_stop(self.tool_card_header_bg(cx), 1.),
linear_color_stop(color.opacity(0.2), 0.), linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
)) ))
};
let gradient_color = if use_card_layout {
self.tool_card_header_bg(cx)
} else { } else {
cx.theme().colors().panel_background this.bg(linear_gradient(
90.,
linear_color_stop(cx.theme().colors().panel_background, 1.),
linear_color_stop(
cx.theme().colors().panel_background.opacity(0.2),
0.,
),
))
}
})
}; };
let tool_output_display = if is_open { let tool_output_display = if is_open {
@ -1818,40 +1878,58 @@ impl AcpThreadView {
}; };
v_flex() v_flex()
.when(use_card_layout, |this| { .map(|this| {
this.rounded_md() if use_card_layout {
this.my_2()
.rounded_md()
.border_1() .border_1()
.border_color(self.tool_card_border_color(cx)) .border_color(self.tool_card_border_color(cx))
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.overflow_hidden() .overflow_hidden()
} else {
this.my_1()
}
}) })
.map(|this| {
if has_location && !use_card_layout {
this.ml_4()
} else {
this.ml_5()
}
})
.mr_5()
.child( .child(
h_flex() h_flex()
.id(header_id)
.group(&card_header_id) .group(&card_header_id)
.relative() .relative()
.w_full() .w_full()
.max_w_full()
.gap_1() .gap_1()
.justify_between()
.when(use_card_layout, |this| { .when(use_card_layout, |this| {
this.pl_1p5() this.p_0p5()
.pr_1()
.py_0p5()
.rounded_t_md() .rounded_t_md()
.when(is_open && !failed_tool_call, |this| { .bg(self.tool_card_header_bg(cx))
.when(is_open && !failed_or_canceled, |this| {
this.border_b_1() this.border_b_1()
.border_color(self.tool_card_border_color(cx)) .border_color(self.tool_card_border_color(cx))
}) })
.bg(self.tool_card_header_bg(cx))
}) })
.child( .child(
h_flex() h_flex()
.relative() .relative()
.w_full() .w_full()
.h(window.line_height() - px(2.)) .h(window.line_height())
.text_size(self.tool_name_font_size()) .text_size(self.tool_name_font_size())
.gap_1p5()
.when(has_location || use_card_layout, |this| this.px_1())
.when(has_location, |this| {
this.cursor(CursorStyle::PointingHand)
.rounded_sm()
.hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
})
.overflow_hidden()
.child(tool_icon) .child(tool_icon)
.child(if tool_call.locations.len() == 1 { .child(if has_location {
let name = tool_call.locations[0] let name = tool_call.locations[0]
.path .path
.file_name() .file_name()
@ -1862,13 +1940,6 @@ impl AcpThreadView {
h_flex() h_flex()
.id(("open-tool-call-location", entry_ix)) .id(("open-tool-call-location", entry_ix))
.w_full() .w_full()
.max_w_full()
.px_1p5()
.rounded_sm()
.overflow_x_scroll()
.hover(|label| {
label.bg(cx.theme().colors().element_hover.opacity(0.5))
})
.map(|this| { .map(|this| {
if use_card_layout { if use_card_layout {
this.text_color(cx.theme().colors().text) this.text_color(cx.theme().colors().text)
@ -1878,28 +1949,25 @@ impl AcpThreadView {
}) })
.child(name) .child(name)
.tooltip(Tooltip::text("Jump to File")) .tooltip(Tooltip::text("Jump to File"))
.cursor(gpui::CursorStyle::PointingHand)
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx); this.open_tool_call_location(entry_ix, 0, window, cx);
})) }))
.into_any_element() .into_any_element()
} else { } else {
h_flex() h_flex()
.relative()
.w_full() .w_full()
.max_w_full() .child(self.render_markdown(
.ml_1p5()
.overflow_hidden()
.child(h_flex().pr_8().child(self.render_markdown(
tool_call.label.clone(), tool_call.label.clone(),
default_markdown_style(false, true, window, cx), default_markdown_style(false, true, window, cx),
))) ))
.child(gradient_overlay(gradient_color))
.into_any() .into_any()
}), })
.when(!has_location, |this| this.child(gradient_overlay)),
) )
.child( .when(is_collapsible || failed_or_canceled, |this| {
this.child(
h_flex() h_flex()
.px_1()
.gap_px() .gap_px()
.when(is_collapsible, |this| { .when(is_collapsible, |this| {
this.child( this.child(
@ -1927,7 +1995,8 @@ impl AcpThreadView {
.size(IconSize::Small), .size(IconSize::Small),
) )
}), }),
), )
}),
) )
.children(tool_output_display) .children(tool_output_display)
} }
@ -1968,7 +2037,7 @@ impl AcpThreadView {
v_flex() v_flex()
.mt_1p5() .mt_1p5()
.ml(px(7.)) .ml(rems(0.4))
.px_3p5() .px_3p5()
.gap_2() .gap_2()
.border_l_1() .border_l_1()
@ -2025,7 +2094,7 @@ impl AcpThreadView {
let button_id = SharedString::from(format!("item-{}", uri)); let button_id = SharedString::from(format!("item-{}", uri));
div() div()
.ml(px(7.)) .ml(rems(0.4))
.pl_2p5() .pl_2p5()
.border_l_1() .border_l_1()
.border_color(self.tool_card_border_color(cx)) .border_color(self.tool_card_border_color(cx))
@ -2213,6 +2282,12 @@ impl AcpThreadView {
started_at.elapsed() started_at.elapsed()
}; };
let header_id =
SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
let header_group = SharedString::from(format!(
"terminal-tool-header-group-{}",
terminal.entity_id()
));
let header_bg = cx let header_bg = cx
.theme() .theme()
.colors() .colors()
@ -2228,10 +2303,7 @@ impl AcpThreadView {
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
let header = h_flex() let header = h_flex()
.id(SharedString::from(format!( .id(header_id)
"terminal-tool-header-{}",
terminal.entity_id()
)))
.flex_none() .flex_none()
.gap_1() .gap_1()
.justify_between() .justify_between()
@ -2295,23 +2367,6 @@ impl AcpThreadView {
), ),
) )
}) })
.when(tool_failed || command_failed, |header| {
header.child(
div()
.id(("terminal-tool-error-code-indicator", terminal.entity_id()))
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.when_some(output.and_then(|o| o.exit_status), |this, status| {
this.tooltip(Tooltip::text(format!(
"Exited with code {}",
status.code().unwrap_or(-1),
)))
}),
)
})
.when(truncated_output, |header| { .when(truncated_output, |header| {
let tooltip = if let Some(output) = output { let tooltip = if let Some(output) = output {
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
@ -2364,6 +2419,7 @@ impl AcpThreadView {
) )
.opened_icon(IconName::ChevronUp) .opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown) .closed_icon(IconName::ChevronDown)
.visible_on_hover(&header_group)
.on_click(cx.listener({ .on_click(cx.listener({
let id = tool_call.id.clone(); let id = tool_call.id.clone();
move |this, _event, _window, _cx| { move |this, _event, _window, _cx| {
@ -2372,8 +2428,26 @@ impl AcpThreadView {
} else { } else {
this.expanded_tool_calls.insert(id.clone()); this.expanded_tool_calls.insert(id.clone());
} }
}})), }
); })),
)
.when(tool_failed || command_failed, |header| {
header.child(
div()
.id(("terminal-tool-error-code-indicator", terminal.entity_id()))
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.when_some(output.and_then(|o| o.exit_status), |this, status| {
this.tooltip(Tooltip::text(format!(
"Exited with code {}",
status.code().unwrap_or(-1),
)))
}),
)
});
let terminal_view = self let terminal_view = self
.entry_view_state .entry_view_state
@ -2383,7 +2457,8 @@ impl AcpThreadView {
let show_output = is_expanded && terminal_view.is_some(); let show_output = is_expanded && terminal_view.is_some();
v_flex() v_flex()
.mb_2() .my_2()
.mx_5()
.border_1() .border_1()
.when(tool_failed || command_failed, |card| card.border_dashed()) .when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color) .border_color(border_color)
@ -2391,9 +2466,10 @@ impl AcpThreadView {
.overflow_hidden() .overflow_hidden()
.child( .child(
v_flex() v_flex()
.group(&header_group)
.py_1p5() .py_1p5()
.pl_2()
.pr_1p5() .pr_1p5()
.pl_2()
.gap_0p5() .gap_0p5()
.bg(header_bg) .bg(header_bg)
.text_xs() .text_xs()
@ -2765,26 +2841,46 @@ impl AcpThreadView {
) )
} }
fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement { fn render_load_error(
let (message, action_slot) = match e { &self,
e: &LoadError,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let (message, action_slot): (SharedString, _) = match e {
LoadError::NotInstalled { LoadError::NotInstalled {
error_message, error_message: _,
install_message, install_message: _,
install_command, install_command,
} => { } => {
let install_command = install_command.clone(); return self.render_not_installed(install_command.clone(), false, window, cx);
let button = Button::new("install", install_message) }
.tooltip(Tooltip::text(install_command.clone())) LoadError::Unsupported {
.style(ButtonStyle::Outlined) error_message: _,
.label_size(LabelSize::Small) upgrade_message: _,
.icon(IconName::Download) upgrade_command,
.icon_size(IconSize::Small) } => {
.icon_color(Color::Muted) return self.render_not_installed(upgrade_command.clone(), true, window, cx);
.icon_position(IconPosition::Start) }
.on_click(cx.listener(move |this, _, window, cx| { LoadError::Exited { .. } => ("Server exited with status {status}".into(), None),
telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id()); LoadError::Other(msg) => (
msg.into(),
Some(self.create_copy_button(msg.to_string()).into_any_element()),
),
};
let task = this Callout::new()
.severity(Severity::Error)
.icon(IconName::XCircleFilled)
.title("Failed to Launch")
.description(message)
.actions_slot(div().children(action_slot))
.into_any_element()
}
fn install_agent(&self, install_command: String, window: &mut Window, cx: &mut Context<Self>) {
telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id());
let task = self
.workspace .workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
let project = workspace.project().read(cx); let project = workspace.project().read(cx);
@ -2822,82 +2918,78 @@ impl AcpThreadView {
} }
}) })
.detach() .detach()
}));
(error_message.clone(), Some(button.into_any_element()))
} }
LoadError::Unsupported {
error_message, fn render_not_installed(
upgrade_message, &self,
upgrade_command, install_command: String,
} => { is_upgrade: bool,
let upgrade_command = upgrade_command.clone(); window: &mut Window,
let button = Button::new("upgrade", upgrade_message) cx: &mut Context<Self>,
.tooltip(Tooltip::text(upgrade_command.clone())) ) -> AnyElement {
.style(ButtonStyle::Outlined) self.install_command_markdown.update(cx, |markdown, cx| {
if !markdown.source().contains(&install_command) {
markdown.replace(format!("```\n{}\n```", install_command), cx);
}
});
let (heading_label, description_label, button_label, or_label) = if is_upgrade {
(
"Upgrade Gemini CLI in Zed",
"Get access to the latest version with support for Zed.",
"Upgrade Gemini CLI",
"Or, to upgrade it manually:",
)
} else {
(
"Get Started with Gemini CLI in Zed",
"Use Google's new coding agent directly in Zed.",
"Install Gemini CLI",
"Or, to install it manually:",
)
};
v_flex()
.w_full()
.p_3p5()
.gap_2p5()
.border_t_1()
.border_color(cx.theme().colors().border)
.bg(linear_gradient(
180.,
linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.),
linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.),
))
.child(
v_flex().gap_0p5().child(Label::new(heading_label)).child(
Label::new(description_label)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
Button::new("install_gemini", button_label)
.full_width()
.size(ButtonSize::Medium)
.style(ButtonStyle::Tinted(TintColor::Accent))
.label_size(LabelSize::Small) .label_size(LabelSize::Small)
.icon(IconName::Download) .icon(IconName::TerminalGhost)
.icon_size(IconSize::Small)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id()); this.install_agent(install_command.clone(), window, cx)
})),
let task = this )
.workspace .child(
.update(cx, |workspace, cx| { Label::new(or_label)
let project = workspace.project().read(cx); .size(LabelSize::Small)
let cwd = project.first_project_directory(cx); .color(Color::Muted),
let shell = project.terminal_settings(&cwd, cx).shell.clone(); )
let spawn_in_terminal = task::SpawnInTerminal { .child(MarkdownElement::new(
id: task::TaskId(upgrade_command.to_string()), self.install_command_markdown.clone(),
full_label: upgrade_command.clone(), default_markdown_style(false, false, window, cx),
label: upgrade_command.clone(), ))
command: Some(upgrade_command.clone()),
args: Vec::new(),
command_label: upgrade_command.clone(),
cwd,
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: true,
reveal: Default::default(),
reveal_target: Default::default(),
hide: Default::default(),
shell,
show_summary: true,
show_command: true,
show_rerun: false,
};
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
})
.ok();
let Some(task) = task else { return };
cx.spawn_in(window, async move |this, cx| {
if let Some(Ok(_)) = task.await {
this.update_in(cx, |this, window, cx| {
this.reset(window, cx);
})
.ok();
}
})
.detach()
}));
(error_message.clone(), Some(button.into_any_element()))
}
LoadError::Exited { .. } => ("Server exited with status {status}".into(), None),
LoadError::Other(msg) => (
msg.into(),
Some(self.create_copy_button(msg.to_string()).into_any_element()),
),
};
Callout::new()
.severity(Severity::Error)
.icon(IconName::XCircleFilled)
.title("Failed to Launch")
.description(message)
.actions_slot(div().children(action_slot))
.into_any_element() .into_any_element()
} }
@ -4152,13 +4244,14 @@ impl AcpThreadView {
) -> impl IntoElement { ) -> impl IntoElement {
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if is_generating { if is_generating {
return h_flex().id("thread-controls-container").ml_1().child( return h_flex().id("thread-controls-container").child(
div() div()
.py_2() .py_2()
.px(rems_from_px(22.)) .px_5()
.child(SpinnerLabel::new().size(LabelSize::Small)), .child(SpinnerLabel::new().size(LabelSize::Small)),
); );
} }
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
@ -4184,12 +4277,10 @@ impl AcpThreadView {
.id("thread-controls-container") .id("thread-controls-container")
.group("thread-controls-container") .group("thread-controls-container")
.w_full() .w_full()
.mr_1() .py_2()
.pt_1() .px_5()
.pb_2()
.px(RESPONSE_PADDING_X)
.gap_px() .gap_px()
.opacity(0.4) .opacity(0.6)
.hover(|style| style.opacity(1.)) .hover(|style| style.opacity(1.))
.flex_wrap() .flex_wrap()
.justify_end(); .justify_end();
@ -4200,21 +4291,24 @@ impl AcpThreadView {
.is_some_and(|thread| thread.read(cx).connection().telemetry().is_some()) .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
{ {
let feedback = self.thread_feedback.feedback; let feedback = self.thread_feedback.feedback;
container = container.child(
container = container
.child(
div().visible_on_hover("thread-controls-container").child( div().visible_on_hover("thread-controls-container").child(
Label::new( Label::new(match feedback {
match feedback {
Some(ThreadFeedback::Positive) => "Thanks for your feedback!", Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.", Some(ThreadFeedback::Negative) => {
None => "Rating the thread sends all of your current conversation to the Zed team.", "We appreciate your feedback and will use it to improve."
} }
) None => {
"Rating the thread sends all of your current conversation to the Zed team."
}
})
.color(Color::Muted) .color(Color::Muted)
.size(LabelSize::XSmall) .size(LabelSize::XSmall)
.truncate(), .truncate(),
), ),
).child( )
h_flex()
.child( .child(
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
@ -4225,11 +4319,7 @@ impl AcpThreadView {
}) })
.tooltip(Tooltip::text("Helpful Response")) .tooltip(Tooltip::text("Helpful Response"))
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click( this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
ThreadFeedback::Positive,
window,
cx,
);
})), })),
) )
.child( .child(
@ -4242,14 +4332,9 @@ impl AcpThreadView {
}) })
.tooltip(Tooltip::text("Not Helpful")) .tooltip(Tooltip::text("Not Helpful"))
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click( this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
ThreadFeedback::Negative,
window,
cx,
);
})), })),
) );
)
} }
container.child(open_as_markdown).child(scroll_to_top) container.child(open_as_markdown).child(scroll_to_top)
@ -4881,7 +4966,7 @@ impl Render for AcpThreadView {
.size_full() .size_full()
.items_center() .items_center()
.justify_end() .justify_end()
.child(self.render_load_error(e, cx)), .child(self.render_load_error(e, window, cx)),
ThreadState::Ready { .. } => v_flex().flex_1().map(|this| { ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
if has_messages { if has_messages {
this.child( this.child(

View file

@ -3,20 +3,23 @@ mod configure_context_server_modal;
mod manage_profiles_modal; mod manage_profiles_modal;
mod tool_picker; mod tool_picker;
use std::{sync::Arc, time::Duration}; use std::{ops::Range, sync::Arc, time::Duration};
use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini}; use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini};
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet}; use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan; use cloud_llm_client::Plan;
use collections::HashMap; use collections::HashMap;
use context_server::ContextServerId; use context_server::ContextServerId;
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest; use extension::ExtensionManifest;
use extension_host::ExtensionStore; use extension_host::ExtensionStore;
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
WeakEntity, percentage,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{
@ -34,7 +37,7 @@ use ui::{
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
}; };
use util::ResultExt as _; use util::ResultExt as _;
use workspace::Workspace; use workspace::{Workspace, create_and_open_local_file};
use zed_actions::ExtensionCategoryFilter; use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal; pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
@ -1058,10 +1061,39 @@ impl AgentConfiguration {
.child( .child(
v_flex() v_flex()
.gap_0p5() .gap_0p5()
.child(
h_flex()
.w_full()
.gap_2()
.justify_between()
.child(Headline::new("External Agents")) .child(Headline::new("External Agents"))
.child(
Button::new("add-agent", "Add Agent")
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(
move |_, window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(
workspace,
cx,
).await
})
.detach_and_log_err(cx);
}
}
),
)
)
.child( .child(
Label::new( Label::new(
"Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.", "Bring the agent of your choice to Zed via our new Agent Client Protocol.",
) )
.color(Color::Muted), .color(Color::Muted),
), ),
@ -1324,3 +1356,109 @@ fn show_unable_to_uninstall_extension_with_context_server(
workspace.toggle_status_toast(status_toast, cx); workspace.toggle_status_toast(status_toast, cx);
} }
async fn open_new_agent_servers_entry_in_settings_editor(
workspace: WeakEntity<Workspace>,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let settings_editor = workspace
.update_in(cx, |_, window, cx| {
create_and_open_local_file(paths::settings_file(), window, cx, || {
settings::initial_user_settings_content().as_ref().into()
})
})?
.await?
.downcast::<Editor>()
.unwrap();
settings_editor
.downgrade()
.update_in(cx, |item, window, cx| {
let text = item.buffer().read(cx).snapshot(cx).text();
let settings = cx.global::<SettingsStore>();
let mut unique_server_name = None;
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
let server_name: Option<SharedString> = (0..u8::MAX)
.map(|i| {
if i == 0 {
"your_agent".into()
} else {
format!("your_agent_{}", i).into()
}
})
.find(|name| !file.custom.contains_key(name));
if let Some(server_name) = server_name {
unique_server_name = Some(server_name.clone());
file.custom.insert(
server_name,
AgentServerSettings {
command: AgentServerCommand {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
},
},
);
}
});
if edits.is_empty() {
return;
}
let ranges = edits
.iter()
.map(|(range, _)| range.clone())
.collect::<Vec<_>>();
item.edit(edits, cx);
if let Some((unique_server_name, buffer)) =
unique_server_name.zip(item.buffer().read(cx).as_singleton())
{
let snapshot = buffer.read(cx).snapshot();
if let Some(range) =
find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
{
item.change_selections(
SelectionEffects::scroll(Autoscroll::newest()),
window,
cx,
|selections| {
selections.select_ranges(vec![range]);
},
);
}
}
})
}
fn find_text_in_buffer(
text: &str,
start: usize,
snapshot: &language::BufferSnapshot,
) -> Option<Range<usize>> {
let chars = text.chars().collect::<Vec<char>>();
let mut offset = start;
let mut char_offset = 0;
for c in snapshot.chars_at(start) {
if char_offset >= chars.len() {
break;
}
offset += 1;
if c == chars[char_offset] {
char_offset += 1;
} else {
char_offset = 0;
}
}
if char_offset == chars.len() {
Some(offset.saturating_sub(chars.len())..offset)
} else {
None
}
}

View file

@ -14,6 +14,7 @@ use zed_actions::agent::ReauthenticateAgent;
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::agent_diff::AgentDiffThread; use crate::agent_diff::AgentDiffThread;
use crate::ui::AcpOnboardingModal;
use crate::{ use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@ -77,7 +78,10 @@ use workspace::{
}; };
use zed_actions::{ use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector}, agent::{
OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
ToggleModelSelector,
},
assistant::{OpenRulesLibrary, ToggleFocus}, assistant::{OpenRulesLibrary, ToggleFocus},
}; };
@ -201,6 +205,9 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| { .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
AgentOnboardingModal::toggle(workspace, window, cx) AgentOnboardingModal::toggle(workspace, window, cx)
}) })
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
AcpOnboardingModal::toggle(workspace, window, cx)
})
.register_action(|_workspace, _: &ResetOnboarding, window, cx| { .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
window.refresh(); window.refresh();
@ -591,17 +598,6 @@ impl AgentPanel {
None None
}; };
// Wait for the Gemini/Native feature flag to be available.
let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
if !client.status().borrow().is_signed_out() {
cx.update(|_, cx| {
cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
Duration::from_secs(2),
)
})?
.await;
}
let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel = cx.new(|cx| { let panel = cx.new(|cx| {
Self::new( Self::new(
@ -1852,19 +1848,6 @@ impl AgentPanel {
menu menu
} }
pub fn set_selected_agent(
&mut self,
agent: AgentType,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.selected_agent != agent {
self.selected_agent = agent.clone();
self.serialize(cx);
}
self.new_agent_thread(agent, window, cx);
}
pub fn selected_agent(&self) -> AgentType { pub fn selected_agent(&self) -> AgentType {
self.selected_agent.clone() self.selected_agent.clone()
} }
@ -1875,6 +1858,11 @@ impl AgentPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if self.selected_agent != agent {
self.selected_agent = agent.clone();
self.serialize(cx);
}
match agent { match agent {
AgentType::Zed => { AgentType::Zed => {
window.dispatch_action( window.dispatch_action(
@ -2555,7 +2543,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx) workspace.panel::<AgentPanel>(cx)
{ {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.set_selected_agent( panel.new_agent_thread(
AgentType::NativeAgent, AgentType::NativeAgent,
window, window,
cx, cx,
@ -2581,7 +2569,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx) workspace.panel::<AgentPanel>(cx)
{ {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.set_selected_agent( panel.new_agent_thread(
AgentType::TextThread, AgentType::TextThread,
window, window,
cx, cx,
@ -2609,7 +2597,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx) workspace.panel::<AgentPanel>(cx)
{ {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.set_selected_agent( panel.new_agent_thread(
AgentType::Gemini, AgentType::Gemini,
window, window,
cx, cx,
@ -2636,7 +2624,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx) workspace.panel::<AgentPanel>(cx)
{ {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.set_selected_agent( panel.new_agent_thread(
AgentType::ClaudeCode, AgentType::ClaudeCode,
window, window,
cx, cx,
@ -2669,7 +2657,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx) workspace.panel::<AgentPanel>(cx)
{ {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.set_selected_agent( panel.new_agent_thread(
AgentType::Custom { AgentType::Custom {
name: agent_name name: agent_name
.clone(), .clone(),
@ -2693,9 +2681,9 @@ impl AgentPanel {
}) })
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| { .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
menu.separator().link( menu.separator().link(
"Add Your Own Agent", "Add Other Agents",
OpenBrowser { OpenBrowser {
url: "https://agentclientprotocol.com/".into(), url: zed_urls::external_agents_docs(cx),
} }
.boxed_clone(), .boxed_clone(),
) )

View file

@ -6,7 +6,8 @@ use feature_flags::ZedProFeatureFlag;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task}; use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
use language_model::{ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry, AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
LanguageModelRegistry,
}; };
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
@ -76,6 +77,7 @@ pub struct LanguageModelPickerDelegate {
all_models: Arc<GroupedModels>, all_models: Arc<GroupedModels>,
filtered_entries: Vec<LanguageModelPickerEntry>, filtered_entries: Vec<LanguageModelPickerEntry>,
selected_index: usize, selected_index: usize,
_authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -96,6 +98,7 @@ impl LanguageModelPickerDelegate {
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
filtered_entries: entries, filtered_entries: entries,
get_active_model: Arc::new(get_active_model), get_active_model: Arc::new(get_active_model),
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
_subscriptions: vec![cx.subscribe_in( _subscriptions: vec![cx.subscribe_in(
&LanguageModelRegistry::global(cx), &LanguageModelRegistry::global(cx),
window, window,
@ -139,6 +142,56 @@ impl LanguageModelPickerDelegate {
.unwrap_or(0) .unwrap_or(0)
} }
/// Authenticates all providers in the [`LanguageModelRegistry`].
///
/// We do this so that we can populate the language selector with all of the
/// models from the configured providers.
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
cx.spawn(async move |_cx| {
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
if let Err(err) = authenticate_task.await {
if matches!(err, AuthenticateError::CredentialsNotFound) {
// Since we're authenticating these providers in the
// background for the purposes of populating the
// language selector, we don't care about providers
// where the credentials are not found.
} else {
// Some providers have noisy failure states that we
// don't want to spam the logs with every time the
// language model selector is initialized.
//
// Ideally these should have more clear failure modes
// that we know are safe to ignore here, like what we do
// with `CredentialsNotFound` above.
match provider_id.0.as_ref() {
"lmstudio" | "ollama" => {
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
//
// These fail noisily, so we don't log them.
}
"copilot_chat" => {
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
}
_ => {
log::error!(
"Failed to authenticate provider: {}: {err}",
provider_name.0
);
}
}
}
}
}
})
}
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> { pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
(self.get_active_model)(cx) (self.get_active_model)(cx)
} }

View file

@ -1,3 +1,4 @@
mod acp_onboarding_modal;
mod agent_notification; mod agent_notification;
mod burn_mode_tooltip; mod burn_mode_tooltip;
mod context_pill; mod context_pill;
@ -6,6 +7,7 @@ mod onboarding_modal;
pub mod preview; pub mod preview;
mod unavailable_editing_tooltip; mod unavailable_editing_tooltip;
pub use acp_onboarding_modal::*;
pub use agent_notification::*; pub use agent_notification::*;
pub use burn_mode_tooltip::*; pub use burn_mode_tooltip::*;
pub use context_pill::*; pub use context_pill::*;

View file

@ -0,0 +1,254 @@
use client::zed_urls;
use gpui::{
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
linear_color_stop, linear_gradient,
};
use ui::{TintColor, Vector, VectorName, prelude::*};
use workspace::{ModalView, Workspace};
use crate::agent_panel::{AgentPanel, AgentType};
macro_rules! acp_onboarding_event {
($name:expr) => {
telemetry::event!($name, source = "ACP Onboarding");
};
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+);
};
}
pub struct AcpOnboardingModal {
focus_handle: FocusHandle,
workspace: Entity<Workspace>,
}
impl AcpOnboardingModal {
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
let workspace_entity = cx.entity();
workspace.toggle_modal(window, cx, |_window, cx| Self {
workspace: workspace_entity,
focus_handle: cx.focus_handle(),
});
}
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.workspace.update(cx, |workspace, cx| {
workspace.focus_panel::<AgentPanel>(window, cx);
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.new_agent_thread(AgentType::Gemini, window, cx);
});
}
});
cx.emit(DismissEvent);
acp_onboarding_event!("Open Panel Clicked");
}
fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url(&zed_urls::external_agents_docs(cx));
cx.notify();
acp_onboarding_event!("Documentation Link Clicked");
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for AcpOnboardingModal {}
impl Focusable for AcpOnboardingModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for AcpOnboardingModal {}
impl Render for AcpOnboardingModal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let illustration_element = |label: bool, opacity: f32| {
h_flex()
.px_1()
.py_0p5()
.gap_1()
.rounded_sm()
.bg(cx.theme().colors().element_active.opacity(0.05))
.border_1()
.border_color(cx.theme().colors().border)
.border_dashed()
.child(
Icon::new(IconName::Stop)
.size(IconSize::Small)
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
)
.map(|this| {
if label {
this.child(
Label::new("Your Agent Here")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else {
this.child(
div().w_16().h_1().rounded_full().bg(cx
.theme()
.colors()
.element_active
.opacity(0.6)),
)
}
})
.opacity(opacity)
};
let illustration = h_flex()
.relative()
.h(rems_from_px(126.))
.bg(cx.theme().colors().editor_background)
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.justify_center()
.gap_8()
.rounded_t_md()
.overflow_hidden()
.child(
div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
),
)
.child(div().absolute().inset_0().size_full().bg(linear_gradient(
0.,
linear_color_stop(
cx.theme().colors().elevated_surface_background.opacity(0.1),
0.9,
),
linear_color_stop(
cx.theme().colors().elevated_surface_background.opacity(0.),
0.,
),
)))
.child(
div()
.absolute()
.inset_0()
.size_full()
.bg(gpui::black().opacity(0.15)),
)
.child(
h_flex()
.gap_4()
.child(
Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
)
.child(
Vector::new(
VectorName::AcpLogoSerif,
rems_from_px(111.),
rems_from_px(41.),
)
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
),
)
.child(
v_flex()
.gap_1p5()
.child(illustration_element(false, 0.15))
.child(illustration_element(true, 0.3))
.child(
h_flex()
.pl_1()
.pr_2()
.py_0p5()
.gap_1()
.rounded_sm()
.bg(cx.theme().colors().element_active.opacity(0.2))
.border_1()
.border_color(cx.theme().colors().border)
.child(
Icon::new(IconName::AiGemini)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)),
)
.child(illustration_element(true, 0.3))
.child(illustration_element(false, 0.15)),
);
let heading = v_flex()
.w_full()
.gap_1()
.child(
Label::new("Now Available")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large));
let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
.icon_size(IconSize::Indicator)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::open_panel));
let docs_button = Button::new("add-other-agents", "Add Other Agents")
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.full_width()
.on_click(cx.listener(Self::view_docs));
let close_button = h_flex().absolute().top_2().right_2().child(
IconButton::new("cancel", IconName::Close).on_click(cx.listener(
|_, _: &ClickEvent, _window, cx| {
acp_onboarding_event!("Canceled", trigger = "X click");
cx.emit(DismissEvent);
},
)),
);
v_flex()
.id("acp-onboarding")
.key_context("AcpOnboardingModal")
.relative()
.w(rems(34.))
.h_full()
.elevation_3(cx)
.track_focus(&self.focus_handle(cx))
.overflow_hidden()
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
acp_onboarding_event!("Canceled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(illustration)
.child(
v_flex()
.p_4()
.gap_2()
.child(heading)
.child(Label::new(copy).color(Color::Muted))
.child(
v_flex()
.w_full()
.mt_2()
.gap_1()
.child(open_panel_button)
.child(docs_button),
),
)
.child(close_button)
}
}

View file

@ -43,3 +43,11 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
server_url = server_url(cx) server_url = server_url(cx)
) )
} }
/// Returns the URL to Zed AI's external agents documentation.
pub fn external_agents_docs(cx: &App) -> String {
format!(
"{server_url}/docs/ai/external-agents",
server_url = server_url(cx)
)
}

View file

@ -19,6 +19,10 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap") load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
}); });
static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
});
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions); static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->"; const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
@ -216,6 +220,7 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
let keymap = match os { let keymap = match os {
"macos" => &KEYMAP_MACOS, "macos" => &KEYMAP_MACOS,
"linux" | "freebsd" => &KEYMAP_LINUX, "linux" | "freebsd" => &KEYMAP_LINUX,
"windows" => &KEYMAP_WINDOWS,
_ => unreachable!("Not a valid OS: {}", os), _ => unreachable!("Not a valid OS: {}", os),
}; };

View file

@ -2588,7 +2588,7 @@ impl Editor {
|| binding || binding
.keystrokes() .keystrokes()
.first() .first()
.is_some_and(|keystroke| keystroke.modifiers.modified()) .is_some_and(|keystroke| keystroke.display_modifiers.modified())
})) }))
} }
@ -7686,16 +7686,16 @@ impl Editor {
.keystroke() .keystroke()
{ {
modifiers_held = modifiers_held modifiers_held = modifiers_held
|| (&accept_keystroke.modifiers == modifiers || (&accept_keystroke.display_modifiers == modifiers
&& accept_keystroke.modifiers.modified()); && accept_keystroke.display_modifiers.modified());
}; };
if let Some(accept_partial_keystroke) = self if let Some(accept_partial_keystroke) = self
.accept_edit_prediction_keybind(true, window, cx) .accept_edit_prediction_keybind(true, window, cx)
.keystroke() .keystroke()
{ {
modifiers_held = modifiers_held modifiers_held = modifiers_held
|| (&accept_partial_keystroke.modifiers == modifiers || (&accept_partial_keystroke.display_modifiers == modifiers
&& accept_partial_keystroke.modifiers.modified()); && accept_partial_keystroke.display_modifiers.modified());
} }
if modifiers_held { if modifiers_held {
@ -9044,7 +9044,7 @@ impl Editor {
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
let modifiers_color = if accept_keystroke.modifiers == window.modifiers() { let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
Color::Accent Color::Accent
} else { } else {
Color::Muted Color::Muted
@ -9056,19 +9056,19 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::XSmall.rems(cx)) .text_size(TextSize::XSmall.rems(cx))
.child(h_flex().children(ui::render_modifiers( .child(h_flex().children(ui::render_modifiers(
&accept_keystroke.modifiers, &accept_keystroke.display_modifiers,
PlatformStyle::platform(), PlatformStyle::platform(),
Some(modifiers_color), Some(modifiers_color),
Some(IconSize::XSmall.rems().into()), Some(IconSize::XSmall.rems().into()),
true, true,
))) )))
.when(is_platform_style_mac, |parent| { .when(is_platform_style_mac, |parent| {
parent.child(accept_keystroke.key.clone()) parent.child(accept_keystroke.display_key.clone())
}) })
.when(!is_platform_style_mac, |parent| { .when(!is_platform_style_mac, |parent| {
parent.child( parent.child(
Key::new( Key::new(
util::capitalize(&accept_keystroke.key), util::capitalize(&accept_keystroke.display_key),
Some(Color::Default), Some(Color::Default),
) )
.size(Some(IconSize::XSmall.rems().into())), .size(Some(IconSize::XSmall.rems().into())),
@ -9171,7 +9171,7 @@ impl Editor {
max_width: Pixels, max_width: Pixels,
cursor_point: Point, cursor_point: Point,
style: &EditorStyle, style: &EditorStyle,
accept_keystroke: Option<&gpui::Keystroke>, accept_keystroke: Option<&gpui::KeybindingKeystroke>,
_window: &Window, _window: &Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> Option<AnyElement> { ) -> Option<AnyElement> {
@ -9249,7 +9249,7 @@ impl Editor {
accept_keystroke.as_ref(), accept_keystroke.as_ref(),
|el, accept_keystroke| { |el, accept_keystroke| {
el.child(h_flex().children(ui::render_modifiers( el.child(h_flex().children(ui::render_modifiers(
&accept_keystroke.modifiers, &accept_keystroke.display_modifiers,
PlatformStyle::platform(), PlatformStyle::platform(),
Some(Color::Default), Some(Color::Default),
Some(IconSize::XSmall.rems().into()), Some(IconSize::XSmall.rems().into()),
@ -9319,7 +9319,7 @@ impl Editor {
.child(completion), .child(completion),
) )
.when_some(accept_keystroke, |el, accept_keystroke| { .when_some(accept_keystroke, |el, accept_keystroke| {
if !accept_keystroke.modifiers.modified() { if !accept_keystroke.display_modifiers.modified() {
return el; return el;
} }
@ -9338,7 +9338,7 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.when(is_platform_style_mac, |parent| parent.gap_1()) .when(is_platform_style_mac, |parent| parent.gap_1())
.child(h_flex().children(ui::render_modifiers( .child(h_flex().children(ui::render_modifiers(
&accept_keystroke.modifiers, &accept_keystroke.display_modifiers,
PlatformStyle::platform(), PlatformStyle::platform(),
Some(if !has_completion { Some(if !has_completion {
Color::Muted Color::Muted

View file

@ -43,10 +43,10 @@ use gpui::{
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
transparent_black, transparent_black,
}; };
@ -7150,7 +7150,7 @@ fn header_jump_data(
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>); pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
impl AcceptEditPredictionBinding { impl AcceptEditPredictionBinding {
pub fn keystroke(&self) -> Option<&Keystroke> { pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
if let Some(binding) = self.0.as_ref() { if let Some(binding) = self.0.as_ref() {
match &binding.keystrokes() { match &binding.keystrokes() {
[keystroke, ..] => Some(keystroke), [keystroke, ..] => Some(keystroke),

View file

@ -98,6 +98,10 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag {
// integration too, and we'd like to turn Gemini/Native on in new builds // integration too, and we'd like to turn Gemini/Native on in new builds
// without enabling Claude Code in old builds. // without enabling Claude Code in old builds.
const NAME: &'static str = "gemini-and-native"; const NAME: &'static str = "gemini-and-native";
fn enabled_for_all() -> bool {
true
}
} }
pub struct ClaudeCodeFeatureFlag; pub struct ClaudeCodeFeatureFlag;
@ -201,7 +205,7 @@ impl FeatureFlagAppExt for App {
fn has_flag<T: FeatureFlag>(&self) -> bool { fn has_flag<T: FeatureFlag>(&self) -> bool {
self.try_global::<FeatureFlags>() self.try_global::<FeatureFlags>()
.map(|flags| flags.has_flag::<T>()) .map(|flags| flags.has_flag::<T>())
.unwrap_or(false) .unwrap_or(T::enabled_for_all())
} }
fn is_staff(&self) -> bool { fn is_staff(&self) -> bool {

View file

@ -4466,7 +4466,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn Language
is_enabled is_enabled
.then(|| { .then(|| {
let ConfiguredModel { provider, model } = let ConfiguredModel { provider, model } =
LanguageModelRegistry::read_global(cx).commit_message_model(cx)?; LanguageModelRegistry::read_global(cx).commit_message_model()?;
provider.is_authenticated(cx).then(|| model) provider.is_authenticated(cx).then(|| model)
}) })

View file

@ -37,10 +37,10 @@ use crate::{
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId, AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext, EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
WindowHandle, WindowId, WindowInvalidator, Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
colors::{Colors, GlobalColors}, colors::{Colors, GlobalColors},
current_platform, hash, init_app_menus, current_platform, hash, init_app_menus,
}; };
@ -263,6 +263,7 @@ pub struct App {
pub(crate) focus_handles: Arc<FocusMap>, pub(crate) focus_handles: Arc<FocusMap>,
pub(crate) keymap: Rc<RefCell<Keymap>>, pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>, pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
pub(crate) global_action_listeners: pub(crate) global_action_listeners:
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>, FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
pending_effects: VecDeque<Effect>, pending_effects: VecDeque<Effect>,
@ -312,6 +313,7 @@ impl App {
let text_system = Arc::new(TextSystem::new(platform.text_system())); let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new(); let entities = EntityMap::new();
let keyboard_layout = platform.keyboard_layout(); let keyboard_layout = platform.keyboard_layout();
let keyboard_mapper = platform.keyboard_mapper();
let app = Rc::new_cyclic(|this| AppCell { let app = Rc::new_cyclic(|this| AppCell {
app: RefCell::new(App { app: RefCell::new(App {
@ -337,6 +339,7 @@ impl App {
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
keymap: Rc::new(RefCell::new(Keymap::default())), keymap: Rc::new(RefCell::new(Keymap::default())),
keyboard_layout, keyboard_layout,
keyboard_mapper,
global_action_listeners: FxHashMap::default(), global_action_listeners: FxHashMap::default(),
pending_effects: VecDeque::new(), pending_effects: VecDeque::new(),
pending_notifications: FxHashSet::default(), pending_notifications: FxHashSet::default(),
@ -376,6 +379,7 @@ impl App {
if let Some(app) = app.upgrade() { if let Some(app) = app.upgrade() {
let cx = &mut app.borrow_mut(); let cx = &mut app.borrow_mut();
cx.keyboard_layout = cx.platform.keyboard_layout(); cx.keyboard_layout = cx.platform.keyboard_layout();
cx.keyboard_mapper = cx.platform.keyboard_mapper();
cx.keyboard_layout_observers cx.keyboard_layout_observers
.clone() .clone()
.retain(&(), move |callback| (callback)(cx)); .retain(&(), move |callback| (callback)(cx));
@ -424,6 +428,11 @@ impl App {
self.keyboard_layout.as_ref() self.keyboard_layout.as_ref()
} }
/// Get the current keyboard mapper.
pub fn keyboard_mapper(&self) -> &Rc<dyn PlatformKeyboardMapper> {
&self.keyboard_mapper
}
/// Invokes a handler when the current keyboard layout changes /// Invokes a handler when the current keyboard layout changes
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
where where

View file

@ -4,7 +4,7 @@ mod context;
pub use binding::*; pub use binding::*;
pub use context::*; pub use context::*;
use crate::{Action, Keystroke, is_no_action}; use crate::{Action, AsKeystroke, Keystroke, is_no_action};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::any::TypeId; use std::any::TypeId;
@ -141,7 +141,7 @@ impl Keymap {
/// only. /// only.
pub fn bindings_for_input( pub fn bindings_for_input(
&self, &self,
input: &[Keystroke], input: &[impl AsKeystroke],
context_stack: &[KeyContext], context_stack: &[KeyContext],
) -> (SmallVec<[KeyBinding; 1]>, bool) { ) -> (SmallVec<[KeyBinding; 1]>, bool) {
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new(); let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
@ -192,7 +192,6 @@ impl Keymap {
(bindings, !pending.is_empty()) (bindings, !pending.is_empty())
} }
/// Check if the given binding is enabled, given a certain key context. /// Check if the given binding is enabled, given a certain key context.
/// Returns the deepest depth at which the binding matches, or None if it doesn't match. /// Returns the deepest depth at which the binding matches, or None if it doesn't match.
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> { fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
@ -639,7 +638,7 @@ mod tests {
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
let actual = keymap let actual = keymap
.bindings_for_action(action) .bindings_for_action(action)
.map(|binding| binding.keystrokes[0].unparse()) .map(|binding| binding.keystrokes[0].inner.unparse())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(actual, expected, "{:?}", action); assert_eq!(actual, expected, "{:?}", action);
} }

View file

@ -1,14 +1,15 @@
use std::rc::Rc; use std::rc::Rc;
use collections::HashMap; use crate::{
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString}; KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
};
use smallvec::SmallVec; use smallvec::SmallVec;
/// A keybinding and its associated metadata, from the keymap. /// A keybinding and its associated metadata, from the keymap.
pub struct KeyBinding { pub struct KeyBinding {
pub(crate) action: Box<dyn Action>, pub(crate) action: Box<dyn Action>,
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>, pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
pub(crate) meta: Option<KeyBindingMetaIndex>, pub(crate) meta: Option<KeyBindingMetaIndex>,
/// The json input string used when building the keybinding, if any /// The json input string used when building the keybinding, if any
@ -32,7 +33,15 @@ impl KeyBinding {
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self { pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
let context_predicate = let context_predicate =
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap() Self::load(
keystrokes,
Box::new(action),
context_predicate,
false,
None,
&DummyKeyboardMapper,
)
.unwrap()
} }
/// Load a keybinding from the given raw data. /// Load a keybinding from the given raw data.
@ -40,24 +49,22 @@ impl KeyBinding {
keystrokes: &str, keystrokes: &str,
action: Box<dyn Action>, action: Box<dyn Action>,
context_predicate: Option<Rc<KeyBindingContextPredicate>>, context_predicate: Option<Rc<KeyBindingContextPredicate>>,
key_equivalents: Option<&HashMap<char, char>>, use_key_equivalents: bool,
action_input: Option<SharedString>, action_input: Option<SharedString>,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> std::result::Result<Self, InvalidKeystrokeError> { ) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
.split_whitespace() .split_whitespace()
.map(Keystroke::parse) .map(|source| {
let keystroke = Keystroke::parse(source)?;
Ok(KeybindingKeystroke::new(
keystroke,
use_key_equivalents,
keyboard_mapper,
))
})
.collect::<std::result::Result<_, _>>()?; .collect::<std::result::Result<_, _>>()?;
if let Some(equivalents) = key_equivalents {
for keystroke in keystrokes.iter_mut() {
if keystroke.key.chars().count() == 1
&& let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
{
keystroke.key = key.to_string();
}
}
}
Ok(Self { Ok(Self {
keystrokes, keystrokes,
action, action,
@ -79,13 +86,13 @@ impl KeyBinding {
} }
/// Check if the given keystrokes match this binding. /// Check if the given keystrokes match this binding.
pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> { pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option<bool> {
if self.keystrokes.len() < typed.len() { if self.keystrokes.len() < typed.len() {
return None; return None;
} }
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) { for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
if !typed.should_match(target) { if !typed.as_keystroke().should_match(target) {
return None; return None;
} }
} }
@ -94,7 +101,7 @@ impl KeyBinding {
} }
/// Get the keystrokes associated with this binding /// Get the keystrokes associated with this binding
pub fn keystrokes(&self) -> &[Keystroke] { pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
self.keystrokes.as_slice() self.keystrokes.as_slice()
} }

View file

@ -231,7 +231,6 @@ pub(crate) trait Platform: 'static {
fn on_quit(&self, callback: Box<dyn FnMut()>); fn on_quit(&self, callback: Box<dyn FnMut()>);
fn on_reopen(&self, callback: Box<dyn FnMut()>); fn on_reopen(&self, callback: Box<dyn FnMut()>);
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap); fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
fn get_menus(&self) -> Option<Vec<OwnedMenu>> { fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
@ -251,7 +250,6 @@ pub(crate) trait Platform: 'static {
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>); fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>); fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>); fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn compositor_name(&self) -> &'static str { fn compositor_name(&self) -> &'static str {
"" ""
@ -272,6 +270,10 @@ pub(crate) trait Platform: 'static {
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>; fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>; fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
fn delete_credentials(&self, url: &str) -> Task<Result<()>>; fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
} }
/// A handle to a platform's display, e.g. a monitor or laptop screen. /// A handle to a platform's display, e.g. a monitor or laptop screen.

View file

@ -1,3 +1,7 @@
use collections::HashMap;
use crate::{KeybindingKeystroke, Keystroke};
/// A trait for platform-specific keyboard layouts /// A trait for platform-specific keyboard layouts
pub trait PlatformKeyboardLayout { pub trait PlatformKeyboardLayout {
/// Get the keyboard layout ID, which should be unique to the layout /// Get the keyboard layout ID, which should be unique to the layout
@ -5,3 +9,33 @@ pub trait PlatformKeyboardLayout {
/// Get the keyboard layout display name /// Get the keyboard layout display name
fn name(&self) -> &str; fn name(&self) -> &str;
} }
/// A trait for platform-specific keyboard mappings
pub trait PlatformKeyboardMapper {
/// Map a key equivalent to its platform-specific representation
fn map_key_equivalent(
&self,
keystroke: Keystroke,
use_key_equivalents: bool,
) -> KeybindingKeystroke;
/// Get the key equivalents for the current keyboard layout,
/// only used on macOS
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>>;
}
/// A dummy implementation of the platform keyboard mapper
pub struct DummyKeyboardMapper;
impl PlatformKeyboardMapper for DummyKeyboardMapper {
fn map_key_equivalent(
&self,
keystroke: Keystroke,
_use_key_equivalents: bool,
) -> KeybindingKeystroke {
KeybindingKeystroke::from_keystroke(keystroke)
}
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
None
}
}

View file

@ -5,6 +5,14 @@ use std::{
fmt::{Display, Write}, fmt::{Display, Write},
}; };
use crate::PlatformKeyboardMapper;
/// This is a helper trait so that we can simplify the implementation of some functions
pub trait AsKeystroke {
/// Returns the GPUI representation of the keystroke.
fn as_keystroke(&self) -> &Keystroke;
}
/// A keystroke and associated metadata generated by the platform /// A keystroke and associated metadata generated by the platform
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
pub struct Keystroke { pub struct Keystroke {
@ -24,6 +32,17 @@ pub struct Keystroke {
pub key_char: Option<String>, pub key_char: Option<String>,
} }
/// Represents a keystroke that can be used in keybindings and displayed to the user.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct KeybindingKeystroke {
/// The GPUI representation of the keystroke.
pub inner: Keystroke,
/// The modifiers to display.
pub display_modifiers: Modifiers,
/// The key to display.
pub display_key: String,
}
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use /// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
/// markdown to display it. /// markdown to display it.
#[derive(Debug)] #[derive(Debug)]
@ -58,7 +77,7 @@ impl Keystroke {
/// ///
/// This method assumes that `self` was typed and `target' is in the keymap, and checks /// This method assumes that `self` was typed and `target' is in the keymap, and checks
/// both possibilities for self against the target. /// both possibilities for self against the target.
pub fn should_match(&self, target: &Keystroke) -> bool { pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
if let Some(key_char) = self if let Some(key_char) = self
.key_char .key_char
@ -71,7 +90,7 @@ impl Keystroke {
..Default::default() ..Default::default()
}; };
if &target.key == key_char && target.modifiers == ime_modifiers { if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
return true; return true;
} }
} }
@ -83,12 +102,12 @@ impl Keystroke {
.filter(|key_char| key_char != &&self.key) .filter(|key_char| key_char != &&self.key)
{ {
// On Windows, if key_char is set, then the typed keystroke produced the key_char // On Windows, if key_char is set, then the typed keystroke produced the key_char
if &target.key == key_char && target.modifiers == Modifiers::none() { if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
return true; return true;
} }
} }
target.modifiers == self.modifiers && target.key == self.key target.inner.modifiers == self.modifiers && target.inner.key == self.key
} }
/// key syntax is: /// key syntax is:
@ -200,31 +219,7 @@ impl Keystroke {
/// Produces a representation of this key that Parse can understand. /// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String { pub fn unparse(&self) -> String {
let mut str = String::new(); unparse(&self.modifiers, &self.key)
if self.modifiers.function {
str.push_str("fn-");
}
if self.modifiers.control {
str.push_str("ctrl-");
}
if self.modifiers.alt {
str.push_str("alt-");
}
if self.modifiers.platform {
#[cfg(target_os = "macos")]
str.push_str("cmd-");
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
str.push_str("super-");
#[cfg(target_os = "windows")]
str.push_str("win-");
}
if self.modifiers.shift {
str.push_str("shift-");
}
str.push_str(&self.key);
str
} }
/// Returns true if this keystroke left /// Returns true if this keystroke left
@ -266,6 +261,32 @@ impl Keystroke {
} }
} }
impl KeybindingKeystroke {
/// Create a new keybinding keystroke from the given keystroke
pub fn new(
inner: Keystroke,
use_key_equivalents: bool,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> Self {
keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
}
pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self {
let key = keystroke.key.clone();
let modifiers = keystroke.modifiers;
KeybindingKeystroke {
inner: keystroke,
display_modifiers: modifiers,
display_key: key,
}
}
/// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String {
unparse(&self.display_modifiers, &self.display_key)
}
}
fn is_printable_key(key: &str) -> bool { fn is_printable_key(key: &str) -> bool {
!matches!( !matches!(
key, key,
@ -322,65 +343,15 @@ fn is_printable_key(key: &str) -> bool {
impl std::fmt::Display for Keystroke { impl std::fmt::Display for Keystroke {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.modifiers.control { display_modifiers(&self.modifiers, f)?;
#[cfg(target_os = "macos")] display_key(&self.key, f)
f.write_char('^')?;
#[cfg(not(target_os = "macos"))]
write!(f, "ctrl-")?;
} }
if self.modifiers.alt {
#[cfg(target_os = "macos")]
f.write_char('⌥')?;
#[cfg(not(target_os = "macos"))]
write!(f, "alt-")?;
} }
if self.modifiers.platform {
#[cfg(target_os = "macos")]
f.write_char('⌘')?;
#[cfg(any(target_os = "linux", target_os = "freebsd"))] impl std::fmt::Display for KeybindingKeystroke {
f.write_char('❖')?; fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
display_modifiers(&self.display_modifiers, f)?;
#[cfg(target_os = "windows")] display_key(&self.display_key, f)
f.write_char('⊞')?;
}
if self.modifiers.shift {
#[cfg(target_os = "macos")]
f.write_char('⇧')?;
#[cfg(not(target_os = "macos"))]
write!(f, "shift-")?;
}
let key = match self.key.as_str() {
#[cfg(target_os = "macos")]
"backspace" => '⌫',
#[cfg(target_os = "macos")]
"up" => '↑',
#[cfg(target_os = "macos")]
"down" => '↓',
#[cfg(target_os = "macos")]
"left" => '←',
#[cfg(target_os = "macos")]
"right" => '→',
#[cfg(target_os = "macos")]
"tab" => '⇥',
#[cfg(target_os = "macos")]
"escape" => '⎋',
#[cfg(target_os = "macos")]
"shift" => '⇧',
#[cfg(target_os = "macos")]
"control" => '⌃',
#[cfg(target_os = "macos")]
"alt" => '⌥',
#[cfg(target_os = "macos")]
"platform" => '⌘',
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
key => return f.write_str(key),
};
f.write_char(key)
} }
} }
@ -600,3 +571,110 @@ pub struct Capslock {
#[serde(default)] #[serde(default)]
pub on: bool, pub on: bool,
} }
impl AsKeystroke for Keystroke {
fn as_keystroke(&self) -> &Keystroke {
self
}
}
impl AsKeystroke for KeybindingKeystroke {
fn as_keystroke(&self) -> &Keystroke {
&self.inner
}
}
fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if modifiers.control {
#[cfg(target_os = "macos")]
f.write_char('^')?;
#[cfg(not(target_os = "macos"))]
write!(f, "ctrl-")?;
}
if modifiers.alt {
#[cfg(target_os = "macos")]
f.write_char('⌥')?;
#[cfg(not(target_os = "macos"))]
write!(f, "alt-")?;
}
if modifiers.platform {
#[cfg(target_os = "macos")]
f.write_char('⌘')?;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
f.write_char('❖')?;
#[cfg(target_os = "windows")]
f.write_char('⊞')?;
}
if modifiers.shift {
#[cfg(target_os = "macos")]
f.write_char('⇧')?;
#[cfg(not(target_os = "macos"))]
write!(f, "shift-")?;
}
Ok(())
}
fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let key = match key {
#[cfg(target_os = "macos")]
"backspace" => '⌫',
#[cfg(target_os = "macos")]
"up" => '↑',
#[cfg(target_os = "macos")]
"down" => '↓',
#[cfg(target_os = "macos")]
"left" => '←',
#[cfg(target_os = "macos")]
"right" => '→',
#[cfg(target_os = "macos")]
"tab" => '⇥',
#[cfg(target_os = "macos")]
"escape" => '⎋',
#[cfg(target_os = "macos")]
"shift" => '⇧',
#[cfg(target_os = "macos")]
"control" => '⌃',
#[cfg(target_os = "macos")]
"alt" => '⌥',
#[cfg(target_os = "macos")]
"platform" => '⌘',
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
key => return f.write_str(key),
};
f.write_char(key)
}
#[inline]
fn unparse(modifiers: &Modifiers, key: &str) -> String {
let mut result = String::new();
if modifiers.function {
result.push_str("fn-");
}
if modifiers.control {
result.push_str("ctrl-");
}
if modifiers.alt {
result.push_str("alt-");
}
if modifiers.platform {
#[cfg(target_os = "macos")]
result.push_str("cmd-");
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
result.push_str("super-");
#[cfg(target_os = "windows")]
result.push_str("win-");
}
if modifiers.shift {
result.push_str("shift-");
}
result.push_str(&key);
result
}

View file

@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
use crate::{ use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions, ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
Point, Result, Task, WindowAppearance, WindowParams, px, PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
}; };
#[cfg(any(feature = "wayland", feature = "x11"))] #[cfg(any(feature = "wayland", feature = "x11"))]
@ -144,6 +144,10 @@ impl<P: LinuxClient + 'static> Platform for P {
self.keyboard_layout() self.keyboard_layout()
} }
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
Rc::new(crate::DummyKeyboardMapper)
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) { fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
} }

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
use super::{ use super::{
BoolExt, MacKeyboardLayout, BoolExt, MacKeyboardLayout, MacKeyboardMapper,
attributed_string::{NSAttributedString, NSMutableAttributedString}, attributed_string::{NSAttributedString, NSMutableAttributedString},
events::key_to_native, events::key_to_native,
renderer, renderer,
@ -8,8 +8,9 @@ use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString, Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher, CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform, MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash, PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams,
hash,
}; };
use anyhow::{Context as _, anyhow}; use anyhow::{Context as _, anyhow};
use block::ConcreteBlock; use block::ConcreteBlock;
@ -171,6 +172,7 @@ pub(crate) struct MacPlatformState {
finish_launching: Option<Box<dyn FnOnce()>>, finish_launching: Option<Box<dyn FnOnce()>>,
dock_menu: Option<id>, dock_menu: Option<id>,
menus: Option<Vec<OwnedMenu>>, menus: Option<Vec<OwnedMenu>>,
keyboard_mapper: Rc<MacKeyboardMapper>,
} }
impl Default for MacPlatform { impl Default for MacPlatform {
@ -189,6 +191,9 @@ impl MacPlatform {
#[cfg(not(feature = "font-kit"))] #[cfg(not(feature = "font-kit"))]
let text_system = Arc::new(crate::NoopTextSystem::new()); let text_system = Arc::new(crate::NoopTextSystem::new());
let keyboard_layout = MacKeyboardLayout::new();
let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
Self(Mutex::new(MacPlatformState { Self(Mutex::new(MacPlatformState {
headless, headless,
text_system, text_system,
@ -209,6 +214,7 @@ impl MacPlatform {
dock_menu: None, dock_menu: None,
on_keyboard_layout_change: None, on_keyboard_layout_change: None,
menus: None, menus: None,
keyboard_mapper,
})) }))
} }
@ -348,19 +354,19 @@ impl MacPlatform {
let mut mask = NSEventModifierFlags::empty(); let mut mask = NSEventModifierFlags::empty();
for (modifier, flag) in &[ for (modifier, flag) in &[
( (
keystroke.modifiers.platform, keystroke.display_modifiers.platform,
NSEventModifierFlags::NSCommandKeyMask, NSEventModifierFlags::NSCommandKeyMask,
), ),
( (
keystroke.modifiers.control, keystroke.display_modifiers.control,
NSEventModifierFlags::NSControlKeyMask, NSEventModifierFlags::NSControlKeyMask,
), ),
( (
keystroke.modifiers.alt, keystroke.display_modifiers.alt,
NSEventModifierFlags::NSAlternateKeyMask, NSEventModifierFlags::NSAlternateKeyMask,
), ),
( (
keystroke.modifiers.shift, keystroke.display_modifiers.shift,
NSEventModifierFlags::NSShiftKeyMask, NSEventModifierFlags::NSShiftKeyMask,
), ),
] { ] {
@ -373,7 +379,7 @@ impl MacPlatform {
.initWithTitle_action_keyEquivalent_( .initWithTitle_action_keyEquivalent_(
ns_string(name), ns_string(name),
selector, selector,
ns_string(key_to_native(&keystroke.key).as_ref()), ns_string(key_to_native(&keystroke.display_key).as_ref()),
) )
.autorelease(); .autorelease();
if Self::os_version() >= SemanticVersion::new(12, 0, 0) { if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
@ -882,6 +888,10 @@ impl Platform for MacPlatform {
Box::new(MacKeyboardLayout::new()) Box::new(MacKeyboardLayout::new())
} }
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
self.0.lock().keyboard_mapper.clone()
}
fn app_path(&self) -> Result<PathBuf> { fn app_path(&self) -> Result<PathBuf> {
unsafe { unsafe {
let bundle: id = NSBundle::mainBundle(); let bundle: id = NSBundle::mainBundle();
@ -1393,6 +1403,8 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) { extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
let platform = unsafe { get_mac_platform(this) }; let platform = unsafe { get_mac_platform(this) };
let mut lock = platform.0.lock(); let mut lock = platform.0.lock();
let keyboard_layout = MacKeyboardLayout::new();
lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
if let Some(mut callback) = lock.on_keyboard_layout_change.take() { if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
drop(lock); drop(lock);
callback(); callback();

View file

@ -1,8 +1,9 @@
use crate::{ use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout, DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
}; };
use anyhow::Result; use anyhow::Result;
use collections::VecDeque; use collections::VecDeque;
@ -237,6 +238,10 @@ impl Platform for TestPlatform {
Box::new(TestKeyboardLayout) Box::new(TestKeyboardLayout)
} }
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
Rc::new(DummyKeyboardMapper)
}
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {} fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) { fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {

View file

@ -1,22 +1,31 @@
use anyhow::Result; use anyhow::Result;
use collections::HashMap;
use windows::Win32::UI::{ use windows::Win32::UI::{
Input::KeyboardAndMouse::{ Input::KeyboardAndMouse::{
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0, GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU, VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102, VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
}, },
WindowsAndMessaging::KL_NAMELENGTH, WindowsAndMessaging::KL_NAMELENGTH,
}; };
use windows_core::HSTRING; use windows_core::HSTRING;
use crate::{Modifiers, PlatformKeyboardLayout}; use crate::{
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
};
pub(crate) struct WindowsKeyboardLayout { pub(crate) struct WindowsKeyboardLayout {
id: String, id: String,
name: String, name: String,
} }
pub(crate) struct WindowsKeyboardMapper {
key_to_vkey: HashMap<String, (u16, bool)>,
vkey_to_key: HashMap<u16, String>,
vkey_to_shifted: HashMap<u16, String>,
}
impl PlatformKeyboardLayout for WindowsKeyboardLayout { impl PlatformKeyboardLayout for WindowsKeyboardLayout {
fn id(&self) -> &str { fn id(&self) -> &str {
&self.id &self.id
@ -27,6 +36,65 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout {
} }
} }
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
fn map_key_equivalent(
&self,
mut keystroke: Keystroke,
use_key_equivalents: bool,
) -> KeybindingKeystroke {
let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
else {
return KeybindingKeystroke::from_keystroke(keystroke);
};
if shifted_key && keystroke.modifiers.shift {
log::warn!(
"Keystroke '{}' has both shift and a shifted key, this is likely a bug",
keystroke.key
);
}
let shift = shifted_key || keystroke.modifiers.shift;
keystroke.modifiers.shift = false;
let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
log::error!(
"Failed to map key equivalent '{:?}' to a valid key",
keystroke
);
return KeybindingKeystroke::from_keystroke(keystroke);
};
keystroke.key = if shift {
let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
log::error!(
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
keystroke,
vkey
);
return KeybindingKeystroke::from_keystroke(keystroke);
};
shifted_key
} else {
key.clone()
};
let modifiers = Modifiers {
shift,
..keystroke.modifiers
};
KeybindingKeystroke {
inner: keystroke,
display_modifiers: modifiers,
display_key: key,
}
}
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
None
}
}
impl WindowsKeyboardLayout { impl WindowsKeyboardLayout {
pub(crate) fn new() -> Result<Self> { pub(crate) fn new() -> Result<Self> {
let mut buffer = [0u16; KL_NAMELENGTH as usize]; let mut buffer = [0u16; KL_NAMELENGTH as usize];
@ -48,6 +116,41 @@ impl WindowsKeyboardLayout {
} }
} }
impl WindowsKeyboardMapper {
pub(crate) fn new() -> Self {
let mut key_to_vkey = HashMap::default();
let mut vkey_to_key = HashMap::default();
let mut vkey_to_shifted = HashMap::default();
for vkey in CANDIDATE_VKEYS {
if let Some(key) = get_key_from_vkey(*vkey) {
key_to_vkey.insert(key.clone(), (vkey.0, false));
vkey_to_key.insert(vkey.0, key);
}
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
if scan_code == 0 {
continue;
}
if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
vkey_to_shifted.insert(vkey.0, shifted_key);
}
}
Self {
key_to_vkey,
vkey_to_key,
vkey_to_shifted,
}
}
fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
if use_key_equivalents {
get_vkey_from_key_with_us_layout(key)
} else {
self.key_to_vkey.get(key).cloned()
}
}
}
pub(crate) fn get_keystroke_key( pub(crate) fn get_keystroke_key(
vkey: VIRTUAL_KEY, vkey: VIRTUAL_KEY,
scan_code: u32, scan_code: u32,
@ -140,3 +243,134 @@ pub(crate) fn generate_key_char(
_ => None, _ => None,
} }
} }
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
match key {
// ` => VK_OEM_3
"`" => Some((VK_OEM_3.0, false)),
"~" => Some((VK_OEM_3.0, true)),
"1" => Some((VK_1.0, false)),
"!" => Some((VK_1.0, true)),
"2" => Some((VK_2.0, false)),
"@" => Some((VK_2.0, true)),
"3" => Some((VK_3.0, false)),
"#" => Some((VK_3.0, true)),
"4" => Some((VK_4.0, false)),
"$" => Some((VK_4.0, true)),
"5" => Some((VK_5.0, false)),
"%" => Some((VK_5.0, true)),
"6" => Some((VK_6.0, false)),
"^" => Some((VK_6.0, true)),
"7" => Some((VK_7.0, false)),
"&" => Some((VK_7.0, true)),
"8" => Some((VK_8.0, false)),
"*" => Some((VK_8.0, true)),
"9" => Some((VK_9.0, false)),
"(" => Some((VK_9.0, true)),
"0" => Some((VK_0.0, false)),
")" => Some((VK_0.0, true)),
"-" => Some((VK_OEM_MINUS.0, false)),
"_" => Some((VK_OEM_MINUS.0, true)),
"=" => Some((VK_OEM_PLUS.0, false)),
"+" => Some((VK_OEM_PLUS.0, true)),
"[" => Some((VK_OEM_4.0, false)),
"{" => Some((VK_OEM_4.0, true)),
"]" => Some((VK_OEM_6.0, false)),
"}" => Some((VK_OEM_6.0, true)),
"\\" => Some((VK_OEM_5.0, false)),
"|" => Some((VK_OEM_5.0, true)),
";" => Some((VK_OEM_1.0, false)),
":" => Some((VK_OEM_1.0, true)),
"'" => Some((VK_OEM_7.0, false)),
"\"" => Some((VK_OEM_7.0, true)),
"," => Some((VK_OEM_COMMA.0, false)),
"<" => Some((VK_OEM_COMMA.0, true)),
"." => Some((VK_OEM_PERIOD.0, false)),
">" => Some((VK_OEM_PERIOD.0, true)),
"/" => Some((VK_OEM_2.0, false)),
"?" => Some((VK_OEM_2.0, true)),
_ => None,
}
}
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
VK_OEM_3,
VK_OEM_MINUS,
VK_OEM_PLUS,
VK_OEM_4,
VK_OEM_5,
VK_OEM_6,
VK_OEM_1,
VK_OEM_7,
VK_OEM_COMMA,
VK_OEM_PERIOD,
VK_OEM_2,
VK_OEM_102,
VK_OEM_8,
VK_ABNT_C1,
VK_0,
VK_1,
VK_2,
VK_3,
VK_4,
VK_5,
VK_6,
VK_7,
VK_8,
VK_9,
];
#[cfg(test)]
mod tests {
use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
#[test]
fn test_keyboard_mapper() {
let mapper = WindowsKeyboardMapper::new();
// Normal case
let keystroke = Keystroke {
modifiers: Modifiers::control(),
key: "a".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(mapped.inner, keystroke);
assert_eq!(mapped.display_key, "a");
assert_eq!(mapped.display_modifiers, Modifiers::control());
// Shifted case, ctrl-$
let keystroke = Keystroke {
modifiers: Modifiers::control(),
key: "$".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(mapped.inner, keystroke);
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
// Shifted case, but shift is true
let keystroke = Keystroke {
modifiers: Modifiers::control_shift(),
key: "$".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke, true);
assert_eq!(mapped.inner.modifiers, Modifiers::control());
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
// Windows style
let keystroke = Keystroke {
modifiers: Modifiers::control_shift(),
key: "4".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke, true);
assert_eq!(mapped.inner.modifiers, Modifiers::control());
assert_eq!(mapped.inner.key, "$");
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
}
}

View file

@ -351,6 +351,10 @@ impl Platform for WindowsPlatform {
) )
} }
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
Rc::new(WindowsKeyboardMapper::new())
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) { fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback); self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
} }

View file

@ -215,6 +215,7 @@ pub enum IconName {
Tab, Tab,
Terminal, Terminal,
TerminalAlt, TerminalAlt,
TerminalGhost,
TextSnippet, TextSnippet,
TextThread, TextThread,
Thread, Thread,

View file

@ -4,12 +4,16 @@ use crate::{
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelRequest, LanguageModelToolChoice,
}; };
use anyhow::anyhow;
use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, App, AsyncApp, Entity, Task, Window}; use gpui::{AnyView, App, AsyncApp, Entity, Task, Window};
use http_client::Result; use http_client::Result;
use parking_lot::Mutex; use parking_lot::Mutex;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::sync::Arc; use std::sync::{
Arc,
atomic::{AtomicBool, Ordering::SeqCst},
};
#[derive(Clone)] #[derive(Clone)]
pub struct FakeLanguageModelProvider { pub struct FakeLanguageModelProvider {
@ -106,6 +110,7 @@ pub struct FakeLanguageModel {
>, >,
)>, )>,
>, >,
forbid_requests: AtomicBool,
} }
impl Default for FakeLanguageModel { impl Default for FakeLanguageModel {
@ -114,11 +119,20 @@ impl Default for FakeLanguageModel {
provider_id: LanguageModelProviderId::from("fake".to_string()), provider_id: LanguageModelProviderId::from("fake".to_string()),
provider_name: LanguageModelProviderName::from("Fake".to_string()), provider_name: LanguageModelProviderName::from("Fake".to_string()),
current_completion_txs: Mutex::new(Vec::new()), current_completion_txs: Mutex::new(Vec::new()),
forbid_requests: AtomicBool::new(false),
} }
} }
} }
impl FakeLanguageModel { impl FakeLanguageModel {
pub fn allow_requests(&self) {
self.forbid_requests.store(false, SeqCst);
}
pub fn forbid_requests(&self) {
self.forbid_requests.store(true, SeqCst);
}
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> { pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
self.current_completion_txs self.current_completion_txs
.lock() .lock()
@ -251,10 +265,19 @@ impl LanguageModel for FakeLanguageModel {
LanguageModelCompletionError, LanguageModelCompletionError,
>, >,
> { > {
if self.forbid_requests.load(SeqCst) {
async move {
Err(LanguageModelCompletionError::Other(anyhow!(
"requests are forbidden"
)))
}
.boxed()
} else {
let (tx, rx) = mpsc::unbounded(); let (tx, rx) = mpsc::unbounded();
self.current_completion_txs.lock().push((request, tx)); self.current_completion_txs.lock().push((request, tx));
async move { Ok(rx.boxed()) }.boxed() async move { Ok(rx.boxed()) }.boxed()
} }
}
fn as_fake(&self) -> &Self { fn as_fake(&self) -> &Self {
self self

View file

@ -6,6 +6,7 @@ use collections::BTreeMap;
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*}; use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
use std::{str::FromStr, sync::Arc}; use std::{str::FromStr, sync::Arc};
use thiserror::Error; use thiserror::Error;
use util::maybe;
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
let registry = cx.new(|_cx| LanguageModelRegistry::default()); let registry = cx.new(|_cx| LanguageModelRegistry::default());
@ -41,9 +42,7 @@ impl std::fmt::Debug for ConfigurationError {
#[derive(Default)] #[derive(Default)]
pub struct LanguageModelRegistry { pub struct LanguageModelRegistry {
default_model: Option<ConfiguredModel>, default_model: Option<ConfiguredModel>,
/// This model is automatically configured by a user's environment after default_fast_model: Option<ConfiguredModel>,
/// authenticating all providers. It's only used when default_model is not available.
environment_fallback_model: Option<ConfiguredModel>,
inline_assistant_model: Option<ConfiguredModel>, inline_assistant_model: Option<ConfiguredModel>,
commit_message_model: Option<ConfiguredModel>, commit_message_model: Option<ConfiguredModel>,
thread_summary_model: Option<ConfiguredModel>, thread_summary_model: Option<ConfiguredModel>,
@ -99,6 +98,9 @@ impl ConfiguredModel {
pub enum Event { pub enum Event {
DefaultModelChanged, DefaultModelChanged,
InlineAssistantModelChanged,
CommitMessageModelChanged,
ThreadSummaryModelChanged,
ProviderStateChanged(LanguageModelProviderId), ProviderStateChanged(LanguageModelProviderId),
AddedProvider(LanguageModelProviderId), AddedProvider(LanguageModelProviderId),
RemovedProvider(LanguageModelProviderId), RemovedProvider(LanguageModelProviderId),
@ -224,7 +226,7 @@ impl LanguageModelRegistry {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let configured_model = model.and_then(|model| self.select_model(model, cx)); let configured_model = model.and_then(|model| self.select_model(model, cx));
self.set_inline_assistant_model(configured_model); self.set_inline_assistant_model(configured_model, cx);
} }
pub fn select_commit_message_model( pub fn select_commit_message_model(
@ -233,7 +235,7 @@ impl LanguageModelRegistry {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let configured_model = model.and_then(|model| self.select_model(model, cx)); let configured_model = model.and_then(|model| self.select_model(model, cx));
self.set_commit_message_model(configured_model); self.set_commit_message_model(configured_model, cx);
} }
pub fn select_thread_summary_model( pub fn select_thread_summary_model(
@ -242,7 +244,7 @@ impl LanguageModelRegistry {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let configured_model = model.and_then(|model| self.select_model(model, cx)); let configured_model = model.and_then(|model| self.select_model(model, cx));
self.set_thread_summary_model(configured_model); self.set_thread_summary_model(configured_model, cx);
} }
/// Selects and sets the inline alternatives for language models based on /// Selects and sets the inline alternatives for language models based on
@ -276,60 +278,68 @@ impl LanguageModelRegistry {
} }
pub fn set_default_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) { pub fn set_default_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
match (self.default_model(), model.as_ref()) { match (self.default_model.as_ref(), model.as_ref()) {
(Some(old), Some(new)) if old.is_same_as(new) => {} (Some(old), Some(new)) if old.is_same_as(new) => {}
(None, None) => {} (None, None) => {}
_ => cx.emit(Event::DefaultModelChanged), _ => cx.emit(Event::DefaultModelChanged),
} }
self.default_fast_model = maybe!({
let provider = &model.as_ref()?.provider;
let fast_model = provider.default_fast_model(cx)?;
Some(ConfiguredModel {
provider: provider.clone(),
model: fast_model,
})
});
self.default_model = model; self.default_model = model;
} }
pub fn set_environment_fallback_model( pub fn set_inline_assistant_model(
&mut self, &mut self,
model: Option<ConfiguredModel>, model: Option<ConfiguredModel>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if self.default_model.is_none() { match (self.inline_assistant_model.as_ref(), model.as_ref()) {
match (self.environment_fallback_model.as_ref(), model.as_ref()) {
(Some(old), Some(new)) if old.is_same_as(new) => {} (Some(old), Some(new)) if old.is_same_as(new) => {}
(None, None) => {} (None, None) => {}
_ => cx.emit(Event::DefaultModelChanged), _ => cx.emit(Event::InlineAssistantModelChanged),
} }
}
self.environment_fallback_model = model;
}
pub fn set_inline_assistant_model(&mut self, model: Option<ConfiguredModel>) {
self.inline_assistant_model = model; self.inline_assistant_model = model;
} }
pub fn set_commit_message_model(&mut self, model: Option<ConfiguredModel>) { pub fn set_commit_message_model(
&mut self,
model: Option<ConfiguredModel>,
cx: &mut Context<Self>,
) {
match (self.commit_message_model.as_ref(), model.as_ref()) {
(Some(old), Some(new)) if old.is_same_as(new) => {}
(None, None) => {}
_ => cx.emit(Event::CommitMessageModelChanged),
}
self.commit_message_model = model; self.commit_message_model = model;
} }
pub fn set_thread_summary_model(&mut self, model: Option<ConfiguredModel>) { pub fn set_thread_summary_model(
&mut self,
model: Option<ConfiguredModel>,
cx: &mut Context<Self>,
) {
match (self.thread_summary_model.as_ref(), model.as_ref()) {
(Some(old), Some(new)) if old.is_same_as(new) => {}
(None, None) => {}
_ => cx.emit(Event::ThreadSummaryModelChanged),
}
self.thread_summary_model = model; self.thread_summary_model = model;
} }
#[track_caller]
pub fn default_model(&self) -> Option<ConfiguredModel> { pub fn default_model(&self) -> Option<ConfiguredModel> {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
return None; return None;
} }
self.default_model self.default_model.clone()
.clone()
.or_else(|| self.environment_fallback_model.clone())
}
pub fn default_fast_model(&self, cx: &App) -> Option<ConfiguredModel> {
let provider = self.default_model()?.provider;
let fast_model = provider.default_fast_model(cx)?;
Some(ConfiguredModel {
provider,
model: fast_model,
})
} }
pub fn inline_assistant_model(&self) -> Option<ConfiguredModel> { pub fn inline_assistant_model(&self) -> Option<ConfiguredModel> {
@ -343,7 +353,7 @@ impl LanguageModelRegistry {
.or_else(|| self.default_model.clone()) .or_else(|| self.default_model.clone())
} }
pub fn commit_message_model(&self, cx: &App) -> Option<ConfiguredModel> { pub fn commit_message_model(&self) -> Option<ConfiguredModel> {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
return None; return None;
@ -351,11 +361,11 @@ impl LanguageModelRegistry {
self.commit_message_model self.commit_message_model
.clone() .clone()
.or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_fast_model.clone())
.or_else(|| self.default_model.clone()) .or_else(|| self.default_model.clone())
} }
pub fn thread_summary_model(&self, cx: &App) -> Option<ConfiguredModel> { pub fn thread_summary_model(&self) -> Option<ConfiguredModel> {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
return None; return None;
@ -363,7 +373,7 @@ impl LanguageModelRegistry {
self.thread_summary_model self.thread_summary_model
.clone() .clone()
.or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_fast_model.clone())
.or_else(|| self.default_model.clone()) .or_else(|| self.default_model.clone())
} }
@ -400,34 +410,4 @@ mod tests {
let providers = registry.read(cx).providers(); let providers = registry.read(cx).providers();
assert!(providers.is_empty()); assert!(providers.is_empty());
} }
#[gpui::test]
async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) {
let registry = cx.new(|_| LanguageModelRegistry::default());
let provider = FakeLanguageModelProvider::default();
registry.update(cx, |registry, cx| {
registry.register_provider(provider.clone(), cx);
});
cx.update(|cx| provider.authenticate(cx)).await.unwrap();
registry.update(cx, |registry, cx| {
let provider = registry.provider(&provider.id()).unwrap();
registry.set_environment_fallback_model(
Some(ConfiguredModel {
provider: provider.clone(),
model: provider.default_model(cx).unwrap(),
}),
cx,
);
let default_model = registry.default_model().unwrap();
let fallback_model = registry.environment_fallback_model.clone().unwrap();
assert_eq!(default_model.model.id(), fallback_model.model.id());
assert_eq!(default_model.provider.id(), fallback_model.provider.id());
});
}
} }

View file

@ -44,7 +44,6 @@ ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] }
open_router = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] }
partial-json-fixer.workspace = true partial-json-fixer.workspace = true
project.workspace = true
release_channel.workspace = true release_channel.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true serde.workspace = true

View file

@ -3,12 +3,8 @@ use std::sync::Arc;
use ::settings::{Settings, SettingsStore}; use ::settings::{Settings, SettingsStore};
use client::{Client, UserStore}; use client::{Client, UserStore};
use collections::HashSet; use collections::HashSet;
use futures::future; use gpui::{App, Context, Entity};
use gpui::{App, AppContext as _, Context, Entity}; use language_model::{LanguageModelProviderId, LanguageModelRegistry};
use language_model::{
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
};
use project::DisableAiSettings;
use provider::deepseek::DeepSeekLanguageModelProvider; use provider::deepseek::DeepSeekLanguageModelProvider;
pub mod provider; pub mod provider;
@ -17,7 +13,7 @@ pub mod ui;
use crate::provider::anthropic::AnthropicLanguageModelProvider; use crate::provider::anthropic::AnthropicLanguageModelProvider;
use crate::provider::bedrock::BedrockLanguageModelProvider; use crate::provider::bedrock::BedrockLanguageModelProvider;
use crate::provider::cloud::{self, CloudLanguageModelProvider}; use crate::provider::cloud::CloudLanguageModelProvider;
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider; use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
use crate::provider::google::GoogleLanguageModelProvider; use crate::provider::google::GoogleLanguageModelProvider;
use crate::provider::lmstudio::LmStudioLanguageModelProvider; use crate::provider::lmstudio::LmStudioLanguageModelProvider;
@ -52,13 +48,6 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
cx, cx,
); );
}); });
let mut already_authenticated = false;
if !DisableAiSettings::get_global(cx).disable_ai {
authenticate_all_providers(registry.clone(), cx);
already_authenticated = true;
}
cx.observe_global::<SettingsStore>(move |cx| { cx.observe_global::<SettingsStore>(move |cx| {
let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx) let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx)
.openai_compatible .openai_compatible
@ -76,12 +65,6 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
); );
}); });
openai_compatible_providers = openai_compatible_providers_new; openai_compatible_providers = openai_compatible_providers_new;
already_authenticated = false;
}
if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated {
authenticate_all_providers(registry.clone(), cx);
already_authenticated = true;
} }
}) })
.detach(); .detach();
@ -168,83 +151,3 @@ fn register_language_model_providers(
registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx); registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx);
registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
} }
/// Authenticates all providers in the [`LanguageModelRegistry`].
///
/// We do this so that we can populate the language selector with all of the
/// models from the configured providers.
///
/// This function won't do anything if AI is disabled.
fn authenticate_all_providers(registry: Entity<LanguageModelRegistry>, cx: &mut App) {
let providers_to_authenticate = registry
.read(cx)
.providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
let mut tasks = Vec::with_capacity(providers_to_authenticate.len());
for (provider_id, provider_name, authenticate_task) in providers_to_authenticate {
tasks.push(cx.background_spawn(async move {
if let Err(err) = authenticate_task.await {
if matches!(err, AuthenticateError::CredentialsNotFound) {
// Since we're authenticating these providers in the
// background for the purposes of populating the
// language selector, we don't care about providers
// where the credentials are not found.
} else {
// Some providers have noisy failure states that we
// don't want to spam the logs with every time the
// language model selector is initialized.
//
// Ideally these should have more clear failure modes
// that we know are safe to ignore here, like what we do
// with `CredentialsNotFound` above.
match provider_id.0.as_ref() {
"lmstudio" | "ollama" => {
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
//
// These fail noisily, so we don't log them.
}
"copilot_chat" => {
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
}
_ => {
log::error!(
"Failed to authenticate provider: {}: {err}",
provider_name.0
);
}
}
}
}
}));
}
let all_authenticated_future = future::join_all(tasks);
cx.spawn(async move |cx| {
all_authenticated_future.await;
registry
.update(cx, |registry, cx| {
let cloud_provider = registry.provider(&cloud::PROVIDER_ID);
let fallback_model = cloud_provider
.iter()
.chain(registry.providers().iter())
.find(|provider| provider.is_authenticated(cx))
.and_then(|provider| {
Some(ConfiguredModel {
provider: provider.clone(),
model: provider
.default_model(cx)
.or_else(|| provider.recommended_models(cx).first().cloned())?,
})
});
registry.set_environment_fallback_model(fallback_model, cx);
})
.ok();
})
.detach();
}

View file

@ -44,8 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i
use crate::provider::google::{GoogleEventMapper, into_google}; use crate::provider::google::{GoogleEventMapper, into_google};
use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai}; use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
#[derive(Default, Clone, Debug, PartialEq)] #[derive(Default, Clone, Debug, PartialEq)]
pub struct ZedDotDevSettings { pub struct ZedDotDevSettings {
@ -146,7 +146,7 @@ impl State {
default_fast_model: None, default_fast_model: None,
recommended_models: Vec::new(), recommended_models: Vec::new(),
_fetch_models_task: cx.spawn(async move |this, cx| { _fetch_models_task: cx.spawn(async move |this, cx| {
maybe!(async { maybe!(async move {
let (client, llm_api_token) = this let (client, llm_api_token) = this
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;

View file

@ -4,7 +4,6 @@ use gpui::{
}; };
use itertools::Itertools; use itertools::Itertools;
use serde_json::json; use serde_json::json;
use settings::get_key_equivalents;
use ui::{Button, ButtonStyle}; use ui::{Button, ButtonStyle};
use ui::{ use ui::{
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon, ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
@ -169,7 +168,8 @@ impl Item for KeyContextView {
impl Render for KeyContextView { impl Render for KeyContextView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
use itertools::Itertools; use itertools::Itertools;
let key_equivalents = get_key_equivalents(cx.keyboard_layout().id());
let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
v_flex() v_flex()
.id("key-context-view") .id("key-context-view")
.overflow_scroll() .overflow_scroll()

View file

@ -1323,7 +1323,7 @@ fn render_copy_code_block_button(
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code")) .tooltip(Tooltip::text("Copy"))
.on_click({ .on_click({
let markdown = markdown; let markdown = markdown;
move |_event, _window, cx| { move |_event, _window, cx| {

View file

@ -4089,6 +4089,7 @@ impl ProjectPanel {
.when(!is_sticky, |this| { .when(!is_sticky, |this| {
this this
.when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
.when(settings.drag_and_drop, |this| this
.on_drag_move::<ExternalPaths>(cx.listener( .on_drag_move::<ExternalPaths>(cx.listener(
move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| { move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
let is_current_target = this.drag_target_entry.as_ref() let is_current_target = this.drag_target_entry.as_ref()
@ -4222,7 +4223,7 @@ impl ProjectPanel {
} }
this.drag_onto(selections, entry_id, kind.is_file(), window, cx); this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
}), }),
) ))
}) })
.on_mouse_down( .on_mouse_down(
MouseButton::Left, MouseButton::Left,
@ -4433,6 +4434,7 @@ impl ProjectPanel {
div() div()
.when(!is_sticky, |div| { .when(!is_sticky, |div| {
div div
.when(settings.drag_and_drop, |div| div
.on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
this.hover_scroll_task.take(); this.hover_scroll_task.take();
this.drag_target_entry = None; this.drag_target_entry = None;
@ -4464,7 +4466,7 @@ impl ProjectPanel {
} }
}, },
)) )))
}) })
.child( .child(
Label::new(DELIMITER.clone()) Label::new(DELIMITER.clone())
@ -4484,6 +4486,7 @@ impl ProjectPanel {
.when(index != components_len - 1, |div|{ .when(index != components_len - 1, |div|{
let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
div div
.when(settings.drag_and_drop, |div| div
.on_drag_move(cx.listener( .on_drag_move(cx.listener(
move |this, event: &DragMoveEvent<DraggedSelection>, _, _| { move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
if event.bounds.contains(&event.event.position) { if event.bounds.contains(&event.event.position) {
@ -4521,7 +4524,7 @@ impl ProjectPanel {
target.index == index target.index == index
), |this| { ), |this| {
this.bg(item_colors.drag_over) this.bg(item_colors.drag_over)
}) }))
}) })
}) })
.on_click(cx.listener(move |this, _, _, cx| { .on_click(cx.listener(move |this, _, _, cx| {
@ -5029,7 +5032,8 @@ impl ProjectPanel {
sticky_parents.reverse(); sticky_parents.reverse();
let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status; let panel_settings = ProjectPanelSettings::get_global(cx);
let git_status_enabled = panel_settings.git_status;
let root_name = OsStr::new(worktree.root_name()); let root_name = OsStr::new(worktree.root_name());
let git_summaries_by_id = if git_status_enabled { let git_summaries_by_id = if git_status_enabled {
@ -5113,11 +5117,11 @@ impl Render for ProjectPanel {
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 has_worktree = !self.visible_entries.is_empty(); let has_worktree = !self.visible_entries.is_empty();
let project = self.project.read(cx); let project = self.project.read(cx);
let indent_size = ProjectPanelSettings::get_global(cx).indent_size; let panel_settings = ProjectPanelSettings::get_global(cx);
let show_indent_guides = let indent_size = panel_settings.indent_size;
ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always; let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
let show_sticky_entries = { let show_sticky_entries = {
if ProjectPanelSettings::get_global(cx).sticky_scroll { if panel_settings.sticky_scroll {
let is_scrollable = self.scroll_handle.is_scrollable(); let is_scrollable = self.scroll_handle.is_scrollable();
let is_scrolled = self.scroll_handle.offset().y < px(0.); let is_scrolled = self.scroll_handle.offset().y < px(0.);
is_scrollable && is_scrolled is_scrollable && is_scrolled
@ -5205,8 +5209,10 @@ impl Render for ProjectPanel {
h_flex() h_flex()
.id("project-panel") .id("project-panel")
.group("project-panel") .group("project-panel")
.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>)) .when(panel_settings.drag_and_drop, |this| {
this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
.on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>)) .on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
})
.size_full() .size_full()
.relative() .relative()
.on_modifiers_changed(cx.listener( .on_modifiers_changed(cx.listener(
@ -5544,6 +5550,7 @@ impl Render for ProjectPanel {
})), })),
) )
.when(is_local, |div| { .when(is_local, |div| {
div.when(panel_settings.drag_and_drop, |div| {
div.drag_over::<ExternalPaths>(|style, _, _, cx| { div.drag_over::<ExternalPaths>(|style, _, _, cx| {
style.bg(cx.theme().colors().drop_target_background) style.bg(cx.theme().colors().drop_target_background)
}) })
@ -5569,6 +5576,7 @@ impl Render for ProjectPanel {
}, },
)) ))
}) })
})
} }
} }
} }

View file

@ -47,6 +47,7 @@ pub struct ProjectPanelSettings {
pub scrollbar: ScrollbarSettings, pub scrollbar: ScrollbarSettings,
pub show_diagnostics: ShowDiagnostics, pub show_diagnostics: ShowDiagnostics,
pub hide_root: bool, pub hide_root: bool,
pub drag_and_drop: bool,
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@ -160,6 +161,10 @@ pub struct ProjectPanelSettingsContent {
/// ///
/// Default: true /// Default: true
pub sticky_scroll: Option<bool>, pub sticky_scroll: Option<bool>,
/// Whether to enable drag-and-drop operations in the project panel.
///
/// Default: true
pub drag_and_drop: Option<bool>,
} }
impl Settings for ProjectPanelSettings { impl Settings for ProjectPanelSettings {

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,8 @@ use collections::{BTreeMap, HashMap, IndexMap};
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE, Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString, KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke,
NoAction, SharedString,
}; };
use schemars::{JsonSchema, json_schema}; use schemars::{JsonSchema, json_schema};
use serde::Deserialize; use serde::Deserialize;
@ -211,9 +212,6 @@ impl KeymapFile {
} }
pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult { pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
let key_equivalents =
crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id());
if content.is_empty() { if content.is_empty() {
return KeymapFileLoadResult::Success { return KeymapFileLoadResult::Success {
key_bindings: Vec::new(), key_bindings: Vec::new(),
@ -255,12 +253,6 @@ impl KeymapFile {
} }
}; };
let key_equivalents = if *use_key_equivalents {
key_equivalents.as_ref()
} else {
None
};
let mut section_errors = String::new(); let mut section_errors = String::new();
if !unrecognized_fields.is_empty() { if !unrecognized_fields.is_empty() {
@ -278,7 +270,7 @@ impl KeymapFile {
keystrokes, keystrokes,
action, action,
context_predicate.clone(), context_predicate.clone(),
key_equivalents, *use_key_equivalents,
cx, cx,
); );
match result { match result {
@ -336,7 +328,7 @@ impl KeymapFile {
keystrokes: &str, keystrokes: &str,
action: &KeymapAction, action: &KeymapAction,
context: Option<Rc<KeyBindingContextPredicate>>, context: Option<Rc<KeyBindingContextPredicate>>,
key_equivalents: Option<&HashMap<char, char>>, use_key_equivalents: bool,
cx: &App, cx: &App,
) -> std::result::Result<KeyBinding, String> { ) -> std::result::Result<KeyBinding, String> {
let (build_result, action_input_string) = match &action.0 { let (build_result, action_input_string) = match &action.0 {
@ -404,8 +396,9 @@ impl KeymapFile {
keystrokes, keystrokes,
action, action,
context, context,
key_equivalents, use_key_equivalents,
action_input_string.map(SharedString::from), action_input_string.map(SharedString::from),
cx.keyboard_mapper().as_ref(),
) { ) {
Ok(key_binding) => key_binding, Ok(key_binding) => key_binding,
Err(InvalidKeystrokeError { keystroke }) => { Err(InvalidKeystrokeError { keystroke }) => {
@ -607,6 +600,7 @@ impl KeymapFile {
mut operation: KeybindUpdateOperation<'a>, mut operation: KeybindUpdateOperation<'a>,
mut keymap_contents: String, mut keymap_contents: String,
tab_size: usize, tab_size: usize,
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
) -> Result<String> { ) -> Result<String> {
match operation { match operation {
// if trying to replace a keybinding that is not user-defined, treat it as an add operation // if trying to replace a keybinding that is not user-defined, treat it as an add operation
@ -646,7 +640,7 @@ impl KeymapFile {
.action_value() .action_value()
.context("Failed to generate target action JSON value")?; .context("Failed to generate target action JSON value")?;
let Some((index, keystrokes_str)) = let Some((index, keystrokes_str)) =
find_binding(&keymap, &target, &target_action_value) find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
else { else {
anyhow::bail!("Failed to find keybinding to remove"); anyhow::bail!("Failed to find keybinding to remove");
}; };
@ -681,7 +675,7 @@ impl KeymapFile {
.context("Failed to generate source action JSON value")?; .context("Failed to generate source action JSON value")?;
if let Some((index, keystrokes_str)) = if let Some((index, keystrokes_str)) =
find_binding(&keymap, &target, &target_action_value) find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
{ {
if target.context == source.context { if target.context == source.context {
// if we are only changing the keybinding (common case) // if we are only changing the keybinding (common case)
@ -781,7 +775,7 @@ impl KeymapFile {
} }
let use_key_equivalents = from.and_then(|from| { let use_key_equivalents = from.and_then(|from| {
let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?; let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?;
let (index, _) = find_binding(&keymap, &from, &action_value)?; let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?;
Some(keymap.0[index].use_key_equivalents) Some(keymap.0[index].use_key_equivalents)
}).unwrap_or(false); }).unwrap_or(false);
if use_key_equivalents { if use_key_equivalents {
@ -808,6 +802,7 @@ impl KeymapFile {
keymap: &'b KeymapFile, keymap: &'b KeymapFile,
target: &KeybindUpdateTarget<'a>, target: &KeybindUpdateTarget<'a>,
target_action_value: &Value, target_action_value: &Value,
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
) -> Option<(usize, &'b str)> { ) -> Option<(usize, &'b str)> {
let target_context_parsed = let target_context_parsed =
KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok(); KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
@ -823,8 +818,11 @@ impl KeymapFile {
for (keystrokes_str, action) in bindings { for (keystrokes_str, action) in bindings {
let Ok(keystrokes) = keystrokes_str let Ok(keystrokes) = keystrokes_str
.split_whitespace() .split_whitespace()
.map(Keystroke::parse) .map(|source| {
.collect::<Result<Vec<_>, _>>() let keystroke = Keystroke::parse(source)?;
Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper))
})
.collect::<Result<Vec<_>, InvalidKeystrokeError>>()
else { else {
continue; continue;
}; };
@ -832,7 +830,7 @@ impl KeymapFile {
|| !keystrokes || !keystrokes
.iter() .iter()
.zip(target.keystrokes) .zip(target.keystrokes)
.all(|(a, b)| a.should_match(b)) .all(|(a, b)| a.inner.should_match(b))
{ {
continue; continue;
} }
@ -847,7 +845,7 @@ impl KeymapFile {
} }
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub enum KeybindUpdateOperation<'a> { pub enum KeybindUpdateOperation<'a> {
Replace { Replace {
/// Describes the keybind to create /// Describes the keybind to create
@ -916,7 +914,7 @@ impl<'a> KeybindUpdateOperation<'a> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct KeybindUpdateTarget<'a> { pub struct KeybindUpdateTarget<'a> {
pub context: Option<&'a str>, pub context: Option<&'a str>,
pub keystrokes: &'a [Keystroke], pub keystrokes: &'a [KeybindingKeystroke],
pub action_name: &'a str, pub action_name: &'a str,
pub action_arguments: Option<&'a str>, pub action_arguments: Option<&'a str>,
} }
@ -941,6 +939,9 @@ impl<'a> KeybindUpdateTarget<'a> {
fn keystrokes_unparsed(&self) -> String { fn keystrokes_unparsed(&self) -> String {
let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8); let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
for keystroke in self.keystrokes { for keystroke in self.keystrokes {
// The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()`
// here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$`
// by default on Windows.
keystrokes.push_str(&keystroke.unparse()); keystrokes.push_str(&keystroke.unparse());
keystrokes.push(' '); keystrokes.push(' ');
} }
@ -959,7 +960,7 @@ impl<'a> KeybindUpdateTarget<'a> {
} }
} }
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum KeybindSource { pub enum KeybindSource {
User, User,
Vim, Vim,
@ -1020,7 +1021,7 @@ impl From<KeybindSource> for KeyBindingMetaIndex {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use gpui::Keystroke; use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke};
use unindent::Unindent; use unindent::Unindent;
use crate::{ use crate::{
@ -1049,16 +1050,27 @@ mod tests {
operation: KeybindUpdateOperation, operation: KeybindUpdateOperation,
expected: impl ToString, expected: impl ToString,
) { ) {
let result = KeymapFile::update_keybinding(operation, input.to_string(), 4) let result = KeymapFile::update_keybinding(
operation,
input.to_string(),
4,
&gpui::DummyKeyboardMapper,
)
.expect("Update succeeded"); .expect("Update succeeded");
pretty_assertions::assert_eq!(expected.to_string(), result); pretty_assertions::assert_eq!(expected.to_string(), result);
} }
#[track_caller] #[track_caller]
fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> { fn parse_keystrokes(keystrokes: &str) -> Vec<KeybindingKeystroke> {
keystrokes keystrokes
.split(' ') .split(' ')
.map(|s| Keystroke::parse(s).expect("Keystrokes valid")) .map(|s| {
KeybindingKeystroke::new(
Keystroke::parse(s).expect("Keystrokes valid"),
false,
&DummyKeyboardMapper,
)
})
.collect() .collect()
} }

View file

@ -1,6 +1,5 @@
mod base_keymap_setting; mod base_keymap_setting;
mod editable_setting_control; mod editable_setting_control;
mod key_equivalents;
mod keymap_file; mod keymap_file;
mod settings_file; mod settings_file;
mod settings_json; mod settings_json;
@ -14,7 +13,6 @@ use util::asset_str;
pub use base_keymap_setting::*; pub use base_keymap_setting::*;
pub use editable_setting_control::*; pub use editable_setting_control::*;
pub use key_equivalents::*;
pub use keymap_file::{ pub use keymap_file::{
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation, KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult, KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
@ -89,7 +87,10 @@ pub fn default_settings() -> Cow<'static, str> {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json"; pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json";
#[cfg(not(target_os = "macos"))] #[cfg(target_os = "windows")]
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json";
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json"; pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json";
pub fn default_keymap() -> Cow<'static, str> { pub fn default_keymap() -> Cow<'static, str> {

View file

@ -14,9 +14,9 @@ use gpui::{
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
EventEmitter, FocusHandle, Focusable, Global, IsZero, EventEmitter, FocusHandle, Focusable, Global, IsZero,
KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point,
StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,
div, TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,
}; };
use language::{Language, LanguageConfig, ToOffset as _}; use language::{Language, LanguageConfig, ToOffset as _};
use notifications::status_toast::{StatusToast, ToastIcon}; use notifications::status_toast::{StatusToast, ToastIcon};
@ -174,7 +174,7 @@ impl FilterState {
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
struct ActionMapping { struct ActionMapping {
keystrokes: Vec<Keystroke>, keystrokes: Vec<KeybindingKeystroke>,
context: Option<SharedString>, context: Option<SharedString>,
} }
@ -236,7 +236,7 @@ struct ConflictState {
} }
type ConflictKeybindMapping = HashMap< type ConflictKeybindMapping = HashMap<
Vec<Keystroke>, Vec<KeybindingKeystroke>,
Vec<( Vec<(
Option<gpui::KeyBindingContextPredicate>, Option<gpui::KeyBindingContextPredicate>,
Vec<ConflictOrigin>, Vec<ConflictOrigin>,
@ -414,12 +414,14 @@ impl Focusable for KeymapEditor {
} }
} }
/// Helper function to check if two keystroke sequences match exactly /// Helper function to check if two keystroke sequences match exactly
fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool { fn keystrokes_match_exactly(
keystrokes1: &[KeybindingKeystroke],
keystrokes2: &[KeybindingKeystroke],
) -> bool {
keystrokes1.len() == keystrokes2.len() keystrokes1.len() == keystrokes2.len()
&& keystrokes1 && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {
.iter() k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers
.zip(keystrokes2) })
.all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers)
} }
impl KeymapEditor { impl KeymapEditor {
@ -509,7 +511,7 @@ impl KeymapEditor {
self.filter_editor.read(cx).text(cx) self.filter_editor.read(cx).text(cx)
} }
fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> { fn current_keystroke_query(&self, cx: &App) -> Vec<KeybindingKeystroke> {
match self.search_mode { match self.search_mode {
SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(), SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
SearchMode::Normal => Default::default(), SearchMode::Normal => Default::default(),
@ -530,7 +532,7 @@ impl KeymapEditor {
let keystroke_query = keystroke_query let keystroke_query = keystroke_query
.into_iter() .into_iter()
.map(|keystroke| keystroke.unparse()) .map(|keystroke| keystroke.inner.unparse())
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(" "); .join(" ");
@ -554,7 +556,7 @@ impl KeymapEditor {
async fn update_matches( async fn update_matches(
this: WeakEntity<Self>, this: WeakEntity<Self>,
action_query: String, action_query: String,
keystroke_query: Vec<Keystroke>, keystroke_query: Vec<KeybindingKeystroke>,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let action_query = command_palette::normalize_action_query(&action_query); let action_query = command_palette::normalize_action_query(&action_query);
@ -603,12 +605,14 @@ impl KeymapEditor {
{ {
let query = &keystroke_query[query_cursor]; let query = &keystroke_query[query_cursor];
let keystroke = &keystrokes[keystroke_cursor]; let keystroke = &keystrokes[keystroke_cursor];
let matches = let matches = query
query.modifiers.is_subset_of(&keystroke.modifiers) .inner
&& ((query.key.is_empty() .modifiers
|| query.key == keystroke.key) .is_subset_of(&keystroke.inner.modifiers)
&& query.key_char.as_ref().is_none_or( && ((query.inner.key.is_empty()
|q_kc| q_kc == &keystroke.key, || query.inner.key == keystroke.inner.key)
&& query.inner.key_char.as_ref().is_none_or(
|q_kc| q_kc == &keystroke.inner.key,
)); ));
if matches { if matches {
found_count += 1; found_count += 1;
@ -678,7 +682,7 @@ impl KeymapEditor {
.map(KeybindSource::from_meta) .map(KeybindSource::from_meta)
.unwrap_or(KeybindSource::Unknown); .unwrap_or(KeybindSource::Unknown);
let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx); let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx);
let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx) let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
.vim_mode(source == KeybindSource::Vim); .vim_mode(source == KeybindSource::Vim);
@ -1202,7 +1206,10 @@ impl KeymapEditor {
.read(cx) .read(cx)
.get_scrollbar_offset(Axis::Vertical), .get_scrollbar_offset(Axis::Vertical),
)); ));
cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await) let keyboard_mapper = cx.keyboard_mapper().clone();
cx.spawn(async move |_, _| {
remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await
})
.detach_and_notify_err(window, cx); .detach_and_notify_err(window, cx);
} }
@ -1422,7 +1429,7 @@ impl ProcessedBinding {
.map(|keybind| keybind.get_action_mapping()) .map(|keybind| keybind.get_action_mapping())
} }
fn keystrokes(&self) -> Option<&[Keystroke]> { fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> {
self.ui_key_binding() self.ui_key_binding()
.map(|binding| binding.keystrokes.as_slice()) .map(|binding| binding.keystrokes.as_slice())
} }
@ -2220,7 +2227,7 @@ impl KeybindingEditorModal {
Ok(action_arguments) Ok(action_arguments)
} }
fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> { fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<KeybindingKeystroke>> {
let new_keystrokes = self let new_keystrokes = self
.keybind_editor .keybind_editor
.read_with(cx, |editor, _| editor.keystrokes().to_vec()); .read_with(cx, |editor, _| editor.keystrokes().to_vec());
@ -2316,6 +2323,7 @@ impl KeybindingEditorModal {
}).unwrap_or(Ok(()))?; }).unwrap_or(Ok(()))?;
let create = self.creating; let create = self.creating;
let keyboard_mapper = cx.keyboard_mapper().clone();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let action_name = existing_keybind.action().name; let action_name = existing_keybind.action().name;
@ -2328,6 +2336,7 @@ impl KeybindingEditorModal {
new_action_args.as_deref(), new_action_args.as_deref(),
&fs, &fs,
tab_size, tab_size,
keyboard_mapper.as_ref(),
) )
.await .await
{ {
@ -2445,11 +2454,21 @@ impl KeybindingEditorModal {
} }
} }
fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke { fn remove_key_char(
Keystroke { KeybindingKeystroke {
modifiers, inner,
key, display_modifiers,
..Default::default() display_key,
}: KeybindingKeystroke,
) -> KeybindingKeystroke {
KeybindingKeystroke {
inner: Keystroke {
modifiers: inner.modifiers,
key: inner.key,
key_char: None,
},
display_modifiers,
display_key,
} }
} }
@ -2992,6 +3011,7 @@ async fn save_keybinding_update(
new_args: Option<&str>, new_args: Option<&str>,
fs: &Arc<dyn Fs>, fs: &Arc<dyn Fs>,
tab_size: usize, tab_size: usize,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let keymap_contents = settings::KeymapFile::load_keymap_file(fs) let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
.await .await
@ -3034,8 +3054,12 @@ async fn save_keybinding_update(
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
let updated_keymap_contents = let updated_keymap_contents = settings::KeymapFile::update_keybinding(
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) operation,
keymap_contents,
tab_size,
keyboard_mapper,
)
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
fs.write( fs.write(
paths::keymap_file().as_path(), paths::keymap_file().as_path(),
@ -3057,6 +3081,7 @@ async fn remove_keybinding(
existing: ProcessedBinding, existing: ProcessedBinding,
fs: &Arc<dyn Fs>, fs: &Arc<dyn Fs>,
tab_size: usize, tab_size: usize,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let Some(keystrokes) = existing.keystrokes() else { let Some(keystrokes) = existing.keystrokes() else {
anyhow::bail!("Cannot remove a keybinding that does not exist"); anyhow::bail!("Cannot remove a keybinding that does not exist");
@ -3080,8 +3105,12 @@ async fn remove_keybinding(
}; };
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
let updated_keymap_contents = let updated_keymap_contents = settings::KeymapFile::update_keybinding(
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) operation,
keymap_contents,
tab_size,
keyboard_mapper,
)
.context("Failed to update keybinding")?; .context("Failed to update keybinding")?;
fs.write( fs.write(
paths::keymap_file().as_path(), paths::keymap_file().as_path(),

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
}; };
use ui::{ use ui::{
ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult {
} }
pub struct KeystrokeInput { pub struct KeystrokeInput {
keystrokes: Vec<Keystroke>, keystrokes: Vec<KeybindingKeystroke>,
placeholder_keystrokes: Option<Vec<Keystroke>>, placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
outer_focus_handle: FocusHandle, outer_focus_handle: FocusHandle,
inner_focus_handle: FocusHandle, inner_focus_handle: FocusHandle,
intercept_subscription: Option<Subscription>, intercept_subscription: Option<Subscription>,
@ -70,7 +70,7 @@ impl KeystrokeInput {
const KEYSTROKE_COUNT_MAX: usize = 3; const KEYSTROKE_COUNT_MAX: usize = 3;
pub fn new( pub fn new(
placeholder_keystrokes: Option<Vec<Keystroke>>, placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
@ -97,7 +97,7 @@ impl KeystrokeInput {
} }
} }
pub fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) { pub fn set_keystrokes(&mut self, keystrokes: Vec<KeybindingKeystroke>, cx: &mut Context<Self>) {
self.keystrokes = keystrokes; self.keystrokes = keystrokes;
self.keystrokes_changed(cx); self.keystrokes_changed(cx);
} }
@ -106,7 +106,7 @@ impl KeystrokeInput {
self.search = search; self.search = search;
} }
pub fn keystrokes(&self) -> &[Keystroke] { pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
if let Some(placeholders) = self.placeholder_keystrokes.as_ref() if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
&& self.keystrokes.is_empty() && self.keystrokes.is_empty()
{ {
@ -116,18 +116,22 @@ impl KeystrokeInput {
&& self && self
.keystrokes .keystrokes
.last() .last()
.is_some_and(|last| last.key.is_empty()) .is_some_and(|last| last.display_key.is_empty())
{ {
return &self.keystrokes[..self.keystrokes.len() - 1]; return &self.keystrokes[..self.keystrokes.len() - 1];
} }
&self.keystrokes &self.keystrokes
} }
fn dummy(modifiers: Modifiers) -> Keystroke { fn dummy(modifiers: Modifiers) -> KeybindingKeystroke {
Keystroke { KeybindingKeystroke {
inner: Keystroke {
modifiers, modifiers,
key: "".to_string(), key: "".to_string(),
key_char: None, key_char: None,
},
display_modifiers: modifiers,
display_key: "".to_string(),
} }
} }
@ -254,7 +258,7 @@ impl KeystrokeInput {
self.keystrokes_changed(cx); self.keystrokes_changed(cx);
if let Some(last) = self.keystrokes.last_mut() if let Some(last) = self.keystrokes.last_mut()
&& last.key.is_empty() && last.display_key.is_empty()
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
{ {
if !self.search && !event.modifiers.modified() { if !self.search && !event.modifiers.modified() {
@ -263,13 +267,15 @@ impl KeystrokeInput {
} }
if self.search { if self.search {
if self.previous_modifiers.modified() { if self.previous_modifiers.modified() {
last.modifiers |= event.modifiers; last.display_modifiers |= event.modifiers;
last.inner.modifiers |= event.modifiers;
} else { } else {
self.keystrokes.push(Self::dummy(event.modifiers)); self.keystrokes.push(Self::dummy(event.modifiers));
} }
self.previous_modifiers |= event.modifiers; self.previous_modifiers |= event.modifiers;
} else { } else {
last.modifiers = event.modifiers; last.display_modifiers = event.modifiers;
last.inner.modifiers = event.modifiers;
return; return;
} }
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
@ -297,14 +303,17 @@ impl KeystrokeInput {
return; return;
} }
let mut keystroke = keystroke.clone(); let mut keystroke =
KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref());
if let Some(last) = self.keystrokes.last() if let Some(last) = self.keystrokes.last()
&& last.key.is_empty() && last.display_key.is_empty()
&& (!self.search || self.previous_modifiers.modified()) && (!self.search || self.previous_modifiers.modified())
{ {
let key = keystroke.key.clone(); let display_key = keystroke.display_key.clone();
let inner_key = keystroke.inner.key.clone();
keystroke = last.clone(); keystroke = last.clone();
keystroke.key = key; keystroke.display_key = display_key;
keystroke.inner.key = inner_key;
self.keystrokes.pop(); self.keystrokes.pop();
} }
@ -324,11 +333,14 @@ impl KeystrokeInput {
self.keystrokes_changed(cx); self.keystrokes_changed(cx);
if self.search { if self.search {
self.previous_modifiers = keystroke.modifiers; self.previous_modifiers = keystroke.display_modifiers;
return; return;
} }
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() { if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX
self.keystrokes.push(Self::dummy(keystroke.modifiers)); && keystroke.display_modifiers.modified()
{
self.keystrokes
.push(Self::dummy(keystroke.display_modifiers));
} }
} }
@ -364,7 +376,7 @@ impl KeystrokeInput {
&self.keystrokes &self.keystrokes
}; };
keystrokes.iter().map(move |keystroke| { keystrokes.iter().map(move |keystroke| {
h_flex().children(ui::render_keystroke( h_flex().children(ui::render_keybinding_keystroke(
keystroke, keystroke,
Some(Color::Default), Some(Color::Default),
Some(rems(0.875).into()), Some(rems(0.875).into()),
@ -809,9 +821,13 @@ mod tests {
/// Verifies that the keystrokes match the expected strings /// Verifies that the keystrokes match the expected strings
#[track_caller] #[track_caller]
pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
let actual = self let actual: Vec<Keystroke> = self.input.read_with(&self.cx, |input, _| {
.input input
.read_with(&self.cx, |input, _| input.keystrokes.clone()); .keystrokes
.iter()
.map(|keystroke| keystroke.inner.clone())
.collect()
});
Self::expect_keystrokes_equal(&actual, expected); Self::expect_keystrokes_equal(&actual, expected);
self self
} }
@ -939,7 +955,7 @@ mod tests {
} }
struct KeystrokeUpdateTracker { struct KeystrokeUpdateTracker {
initial_keystrokes: Vec<Keystroke>, initial_keystrokes: Vec<KeybindingKeystroke>,
_subscription: Subscription, _subscription: Subscription,
input: Entity<KeystrokeInput>, input: Entity<KeystrokeInput>,
received_keystrokes_updated: bool, received_keystrokes_updated: bool,
@ -983,8 +999,8 @@ mod tests {
); );
} }
fn keystrokes_str(ks: &[Keystroke]) -> String { fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String {
ks.iter().map(|ks| ks.unparse()).join(" ") ks.iter().map(|ks| ks.inner.unparse()).join(" ")
} }
} }
} }

View file

@ -119,7 +119,7 @@ impl Render for OnboardingBanner {
h_flex() h_flex()
.h_full() .h_full()
.gap_1() .gap_1()
.child(Icon::new(self.details.icon_name).size(IconSize::Small)) .child(Icon::new(self.details.icon_name).size(IconSize::XSmall))
.child( .child(
h_flex() h_flex()
.gap_0p5() .gap_0p5()

View file

@ -275,11 +275,11 @@ impl TitleBar {
let banner = cx.new(|cx| { let banner = cx.new(|cx| {
OnboardingBanner::new( OnboardingBanner::new(
"Debugger Onboarding", "ACP Onboarding",
IconName::Debug, IconName::Sparkle,
"The Debugger", "Bring Your Own Agent",
None, Some("Introducing:".into()),
zed_actions::debugger::OpenOnboardingModal.boxed_clone(), zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(),
cx, cx,
) )
}); });

View file

@ -13,6 +13,9 @@ use crate::prelude::*;
)] )]
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
pub enum VectorName { pub enum VectorName {
AcpGrid,
AcpLogo,
AcpLogoSerif,
AiGrid, AiGrid,
DebuggerGrid, DebuggerGrid,
Grid, Grid,

View file

@ -1,8 +1,8 @@
use crate::PlatformStyle; use crate::PlatformStyle;
use crate::{Icon, IconName, IconSize, h_flex, prelude::*}; use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
use gpui::{ use gpui::{
Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window, Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke,
relative, Modifiers, Window, relative,
}; };
use itertools::Itertools; use itertools::Itertools;
@ -13,7 +13,7 @@ pub struct KeyBinding {
/// More than one keystroke produces a chord. /// More than one keystroke produces a chord.
/// ///
/// This should always contain at least one keystroke. /// This should always contain at least one keystroke.
pub keystrokes: Vec<Keystroke>, pub keystrokes: Vec<KeybindingKeystroke>,
/// The [`PlatformStyle`] to use when displaying this keybinding. /// The [`PlatformStyle`] to use when displaying this keybinding.
platform_style: PlatformStyle, platform_style: PlatformStyle,
@ -59,7 +59,7 @@ impl KeyBinding {
cx.try_global::<VimStyle>().is_some_and(|g| g.0) cx.try_global::<VimStyle>().is_some_and(|g| g.0)
} }
pub fn new(keystrokes: Vec<Keystroke>, cx: &App) -> Self { pub fn new(keystrokes: Vec<KeybindingKeystroke>, cx: &App) -> Self {
Self { Self {
keystrokes, keystrokes,
platform_style: PlatformStyle::platform(), platform_style: PlatformStyle::platform(),
@ -99,16 +99,16 @@ impl KeyBinding {
} }
fn render_key( fn render_key(
keystroke: &Keystroke, key: &str,
color: Option<Color>, color: Option<Color>,
platform_style: PlatformStyle, platform_style: PlatformStyle,
size: impl Into<Option<AbsoluteLength>>, size: impl Into<Option<AbsoluteLength>>,
) -> AnyElement { ) -> AnyElement {
let key_icon = icon_for_key(keystroke, platform_style); let key_icon = icon_for_key(key, platform_style);
match key_icon { match key_icon {
Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
None => { None => {
let key = util::capitalize(&keystroke.key); let key = util::capitalize(key);
Key::new(&key, color).size(size).into_any_element() Key::new(&key, color).size(size).into_any_element()
} }
} }
@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding {
"KEY_BINDING-{}", "KEY_BINDING-{}",
self.keystrokes self.keystrokes
.iter() .iter()
.map(|k| k.key.to_string()) .map(|k| k.display_key.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" ") .join(" ")
) )
@ -137,7 +137,7 @@ impl RenderOnce for KeyBinding {
.py_0p5() .py_0p5()
.rounded_xs() .rounded_xs()
.text_color(cx.theme().colors().text_muted) .text_color(cx.theme().colors().text_muted)
.children(render_keystroke( .children(render_keybinding_keystroke(
keystroke, keystroke,
color, color,
self.size, self.size,
@ -148,8 +148,8 @@ impl RenderOnce for KeyBinding {
} }
} }
pub fn render_keystroke( pub fn render_keybinding_keystroke(
keystroke: &Keystroke, keystroke: &KeybindingKeystroke,
color: Option<Color>, color: Option<Color>,
size: impl Into<Option<AbsoluteLength>>, size: impl Into<Option<AbsoluteLength>>,
platform_style: PlatformStyle, platform_style: PlatformStyle,
@ -163,26 +163,39 @@ pub fn render_keystroke(
let size = size.into(); let size = size.into();
if use_text { if use_text {
let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color) let element = Key::new(
keystroke_text(
&keystroke.display_modifiers,
&keystroke.display_key,
platform_style,
vim_mode,
),
color,
)
.size(size) .size(size)
.into_any_element(); .into_any_element();
vec![element] vec![element]
} else { } else {
let mut elements = Vec::new(); let mut elements = Vec::new();
elements.extend(render_modifiers( elements.extend(render_modifiers(
&keystroke.modifiers, &keystroke.display_modifiers,
platform_style, platform_style,
color, color,
size, size,
true, true,
)); ));
elements.push(render_key(keystroke, color, platform_style, size)); elements.push(render_key(
&keystroke.display_key,
color,
platform_style,
size,
));
elements elements
} }
} }
fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> { fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option<IconName> {
match keystroke.key.as_str() { match key {
"left" => Some(IconName::ArrowLeft), "left" => Some(IconName::ArrowLeft),
"right" => Some(IconName::ArrowRight), "right" => Some(IconName::ArrowRight),
"up" => Some(IconName::ArrowUp), "up" => Some(IconName::ArrowUp),
@ -379,7 +392,7 @@ impl KeyIcon {
/// Returns a textual representation of the key binding for the given [`Action`]. /// Returns a textual representation of the key binding for the given [`Action`].
pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> { pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
let key_binding = window.highest_precedence_binding_for_action(action)?; let key_binding = window.highest_precedence_binding_for_action(action)?;
Some(text_for_keystrokes(key_binding.keystrokes(), cx)) Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx))
} }
pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
@ -387,22 +400,50 @@ pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
let vim_enabled = cx.try_global::<VimStyle>().is_some(); let vim_enabled = cx.try_global::<VimStyle>().is_some();
keystrokes keystrokes
.iter() .iter()
.map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled)) .map(|keystroke| {
keystroke_text(
&keystroke.modifiers,
&keystroke.key,
platform_style,
vim_enabled,
)
})
.join(" ") .join(" ")
} }
pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String { pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String {
let platform_style = PlatformStyle::platform(); let platform_style = PlatformStyle::platform();
let vim_enabled = cx.try_global::<VimStyle>().is_some(); let vim_enabled = cx.try_global::<VimStyle>().is_some();
keystroke_text(keystroke, platform_style, vim_enabled) keystrokes
.iter()
.map(|keystroke| {
keystroke_text(
&keystroke.display_modifiers,
&keystroke.display_key,
platform_style,
vim_enabled,
)
})
.join(" ")
}
pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String {
let platform_style = PlatformStyle::platform();
let vim_enabled = cx.try_global::<VimStyle>().is_some();
keystroke_text(modifiers, key, platform_style, vim_enabled)
} }
/// Returns a textual representation of the given [`Keystroke`]. /// Returns a textual representation of the given [`Keystroke`].
fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String { fn keystroke_text(
modifiers: &Modifiers,
key: &str,
platform_style: PlatformStyle,
vim_mode: bool,
) -> String {
let mut text = String::new(); let mut text = String::new();
let delimiter = '-'; let delimiter = '-';
if keystroke.modifiers.function { if modifiers.function {
match vim_mode { match vim_mode {
false => text.push_str("Fn"), false => text.push_str("Fn"),
true => text.push_str("fn"), true => text.push_str("fn"),
@ -411,7 +452,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
text.push(delimiter); text.push(delimiter);
} }
if keystroke.modifiers.control { if modifiers.control {
match (platform_style, vim_mode) { match (platform_style, vim_mode) {
(PlatformStyle::Mac, false) => text.push_str("Control"), (PlatformStyle::Mac, false) => text.push_str("Control"),
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
@ -421,7 +462,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
text.push(delimiter); text.push(delimiter);
} }
if keystroke.modifiers.platform { if modifiers.platform {
match (platform_style, vim_mode) { match (platform_style, vim_mode) {
(PlatformStyle::Mac, false) => text.push_str("Command"), (PlatformStyle::Mac, false) => text.push_str("Command"),
(PlatformStyle::Mac, true) => text.push_str("cmd"), (PlatformStyle::Mac, true) => text.push_str("cmd"),
@ -434,7 +475,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
text.push(delimiter); text.push(delimiter);
} }
if keystroke.modifiers.alt { if modifiers.alt {
match (platform_style, vim_mode) { match (platform_style, vim_mode) {
(PlatformStyle::Mac, false) => text.push_str("Option"), (PlatformStyle::Mac, false) => text.push_str("Option"),
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
@ -444,7 +485,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
text.push(delimiter); text.push(delimiter);
} }
if keystroke.modifiers.shift { if modifiers.shift {
match (platform_style, vim_mode) { match (platform_style, vim_mode) {
(_, false) => text.push_str("Shift"), (_, false) => text.push_str("Shift"),
(_, true) => text.push_str("shift"), (_, true) => text.push_str("shift"),
@ -453,9 +494,9 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
} }
if vim_mode { if vim_mode {
text.push_str(&keystroke.key) text.push_str(key)
} else { } else {
let key = match keystroke.key.as_str() { let key = match key {
"pageup" => "PageUp", "pageup" => "PageUp",
"pagedown" => "PageDown", "pagedown" => "PageDown",
key => &util::capitalize(key), key => &util::capitalize(key),
@ -562,9 +603,11 @@ mod tests {
#[test] #[test]
fn test_text_for_keystroke() { fn test_text_for_keystroke() {
let keystroke = Keystroke::parse("cmd-c").unwrap();
assert_eq!( assert_eq!(
keystroke_text( keystroke_text(
&Keystroke::parse("cmd-c").unwrap(), &keystroke.modifiers,
&keystroke.key,
PlatformStyle::Mac, PlatformStyle::Mac,
false false
), ),
@ -572,7 +615,8 @@ mod tests {
); );
assert_eq!( assert_eq!(
keystroke_text( keystroke_text(
&Keystroke::parse("cmd-c").unwrap(), &keystroke.modifiers,
&keystroke.key,
PlatformStyle::Linux, PlatformStyle::Linux,
false false
), ),
@ -580,16 +624,19 @@ mod tests {
); );
assert_eq!( assert_eq!(
keystroke_text( keystroke_text(
&Keystroke::parse("cmd-c").unwrap(), &keystroke.modifiers,
&keystroke.key,
PlatformStyle::Windows, PlatformStyle::Windows,
false false
), ),
"Win-C".to_string() "Win-C".to_string()
); );
let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap();
assert_eq!( assert_eq!(
keystroke_text( keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(), &keystroke.modifiers,
&keystroke.key,
PlatformStyle::Mac, PlatformStyle::Mac,
false false
), ),
@ -597,7 +644,8 @@ mod tests {
); );
assert_eq!( assert_eq!(
keystroke_text( keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(), &keystroke.modifiers,
&keystroke.key,
PlatformStyle::Linux, PlatformStyle::Linux,
false false
), ),
@ -605,16 +653,19 @@ mod tests {
); );
assert_eq!( assert_eq!(
keystroke_text( keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(), &keystroke.modifiers,
&keystroke.key,
PlatformStyle::Windows, PlatformStyle::Windows,
false false
), ),
"Ctrl-Alt-Delete".to_string() "Ctrl-Alt-Delete".to_string()
); );
let keystroke = Keystroke::parse("shift-pageup").unwrap();
assert_eq!( assert_eq!(
keystroke_text( keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(), &keystroke.modifiers,
&keystroke.key,
PlatformStyle::Mac, PlatformStyle::Mac,
false false
), ),
@ -622,7 +673,8 @@ mod tests {
); );
assert_eq!( assert_eq!(
keystroke_text( keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(), &keystroke.modifiers,
&keystroke.key,
PlatformStyle::Linux, PlatformStyle::Linux,
false, false,
), ),
@ -630,7 +682,8 @@ mod tests {
); );
assert_eq!( assert_eq!(
keystroke_text( keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(), &keystroke.modifiers,
&keystroke.key,
PlatformStyle::Windows, PlatformStyle::Windows,
false false
), ),

View file

@ -203,7 +203,10 @@ impl Vim {
// hook into the existing to clear out any vim search state on cmd+f or edit -> find. // hook into the existing to clear out any vim search state on cmd+f or edit -> find.
fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) { fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) {
// Preserve the current mode when resetting search state
let current_mode = self.mode;
self.search = Default::default(); self.search = Default::default();
self.search.prior_mode = current_mode;
cx.propagate(); cx.propagate();
} }

View file

@ -599,6 +599,13 @@ impl Domain for WorkspaceDb {
ssh_projects ON ssh_projects ON
workspaces.ssh_project_id = ssh_projects.id; workspaces.ssh_project_id = ssh_projects.id;
DELETE FROM workspaces_2
WHERE workspace_id NOT IN (
SELECT MAX(workspace_id)
FROM workspaces_2
GROUP BY ssh_connection_id, paths
);
DROP TABLE ssh_projects; DROP TABLE ssh_projects;
DROP TABLE workspaces; DROP TABLE workspaces;
ALTER TABLE workspaces_2 RENAME TO workspaces; ALTER TABLE workspaces_2 RENAME TO workspaces;

View file

@ -1308,11 +1308,11 @@ pub fn handle_keymap_file_changes(
}) })
.detach(); .detach();
let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); let mut current_layout_id = cx.keyboard_layout().id().to_string();
cx.on_keyboard_layout_change(move |cx| { cx.on_keyboard_layout_change(move |cx| {
let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); let next_layout_id = cx.keyboard_layout().id();
if next_mapping != current_mapping { if next_layout_id != current_layout_id {
current_mapping = next_mapping; current_layout_id = next_layout_id.to_string();
keyboard_layout_tx.unbounded_send(()).ok(); keyboard_layout_tx.unbounded_send(()).ok();
} }
}) })
@ -4729,7 +4729,7 @@ mod tests {
// and key strokes contain the given key // and key strokes contain the given key
bindings bindings
.into_iter() .into_iter()
.any(|binding| binding.keystrokes().iter().any(|k| k.key == key)), .any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)),
"On {} Failed to find {} with key binding {}", "On {} Failed to find {} with key binding {}",
line, line,
action.name(), action.name(),

View file

@ -72,7 +72,10 @@ impl QuickActionBar {
Tooltip::with_meta( Tooltip::with_meta(
tooltip_text, tooltip_text,
Some(open_action_for_tooltip), Some(open_action_for_tooltip),
format!("{} to open in a split", text_for_keystroke(&alt_click, cx)), format!(
"{} to open in a split",
text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx)
),
window, window,
cx, cx,
) )

View file

@ -284,6 +284,8 @@ pub mod agent {
OpenSettings, OpenSettings,
/// Opens the agent onboarding modal. /// Opens the agent onboarding modal.
OpenOnboardingModal, OpenOnboardingModal,
/// Opens the ACP onboarding modal.
OpenAcpOnboardingModal,
/// Resets the agent onboarding state. /// Resets the agent onboarding state.
ResetOnboarding, ResetOnboarding,
/// Starts a chat conversation with the agent. /// Starts a chat conversation with the agent.

View file

@ -3243,6 +3243,7 @@ Run the `theme selector: toggle` action in the command palette to see a current
"indent_size": 20, "indent_size": 20,
"auto_reveal_entries": true, "auto_reveal_entries": true,
"auto_fold_dirs": true, "auto_fold_dirs": true,
"drag_and_drop": true,
"scrollbar": { "scrollbar": {
"show": null "show": null
}, },

View file

@ -45,9 +45,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
// Whether to show the task line in the output of the spawned task, defaults to `true`. // Whether to show the task line in the output of the spawned task, defaults to `true`.
"show_summary": true, "show_summary": true,
// Whether to show the command line in the output of the spawned task, defaults to `true`. // Whether to show the command line in the output of the spawned task, defaults to `true`.
"show_output": true, "show_output": true
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once. // Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
"tags": [] // "tags": []
} }
] ]
``` ```

View file

@ -431,6 +431,7 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k
"auto_reveal_entries": true, // Show file in panel when activating its buffer "auto_reveal_entries": true, // Show file in panel when activating its buffer
"auto_fold_dirs": true, // Fold dirs with single subdir "auto_fold_dirs": true, // Fold dirs with single subdir
"sticky_scroll": true, // Stick parent directories at top of the project panel. "sticky_scroll": true, // Stick parent directories at top of the project panel.
"drag_and_drop": true, // Whether drag and drop is enabled
"scrollbar": { // Project panel scrollbar settings "scrollbar": { // Project panel scrollbar settings
"show": null // Show/hide: (auto, system, always, never) "show": null // Show/hide: (auto, system, always, never)
}, },