Compare commits
25 commits
ui-scrollb
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bd4e943597 | ||
![]() |
c5d3c7d790 | ||
![]() |
fff0ecead1 | ||
![]() |
b1b60bb7fe | ||
![]() |
0e575b2809 | ||
![]() |
65c6c709fd | ||
![]() |
858ab9cc23 | ||
![]() |
2c64b05ea4 | ||
![]() |
b7dad2cf71 | ||
![]() |
76dbcde628 | ||
![]() |
aa0f7a2d09 | ||
![]() |
372b3c7af6 | ||
![]() |
10a1140d49 | ||
![]() |
e96b68bc15 | ||
![]() |
b249593abe | ||
![]() |
c14d84cfdb | ||
![]() |
428fc6d483 | ||
![]() |
64b14ef848 | ||
![]() |
bf5ed6d1c9 | ||
![]() |
bb5cfe118f | ||
![]() |
633ce23ae9 | ||
![]() |
d43df9e841 | ||
![]() |
f8667a8379 | ||
![]() |
1460573dd4 | ||
![]() |
65de969cc8 |
94 changed files with 6793 additions and 3822 deletions
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
|
@ -14,7 +14,7 @@ body:
|
|||
### Description
|
||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
|
||||
- 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.
|
||||
-->
|
||||
|
||||
|
|
4
assets/icons/terminal_ghost.svg
Normal file
4
assets/icons/terminal_ghost.svg
Normal 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
1257
assets/images/acp_grid.svg
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 176 KiB |
1
assets/images/acp_logo.svg
Normal file
1
assets/images/acp_logo.svg
Normal 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 |
2
assets/images/acp_logo_serif.svg
Normal file
2
assets/images/acp_logo_serif.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 14 KiB |
|
@ -40,7 +40,7 @@
|
|||
"shift-f11": "debugger::StepOut",
|
||||
"f11": "zed::ToggleFullScreen",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
|
@ -120,7 +120,7 @@
|
|||
"alt-g m": "git::OpenModifiedFiles",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"ctrl-alt-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint"
|
||||
}
|
||||
|
|
1260
assets/keymaps/default-windows.json
Normal file
1260
assets/keymaps/default-windows.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -38,6 +38,7 @@
|
|||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
|
|
|
@ -428,11 +428,13 @@
|
|||
"g h": "vim::StartOfLine",
|
||||
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
||||
"g e": "vim::EndOfDocument",
|
||||
"g .": "vim::HelixGotoLastModification", // go to last modification
|
||||
"g r": "editor::FindAllReferences", // zed specific
|
||||
"g t": "vim::WindowTop",
|
||||
"g c": "vim::WindowMiddle",
|
||||
"g b": "vim::WindowBottom",
|
||||
|
||||
"shift-r": "editor::Paste",
|
||||
"x": "editor::SelectLine",
|
||||
"shift-x": "editor::SelectLine",
|
||||
"%": "editor::SelectAll",
|
||||
|
|
|
@ -653,6 +653,8 @@
|
|||
// "never"
|
||||
"show": "always"
|
||||
},
|
||||
// Whether to enable drag-and-drop operations in the project panel.
|
||||
"drag_and_drop": true,
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
"hide_root": false
|
||||
},
|
||||
|
|
|
@ -43,8 +43,8 @@
|
|||
// "args": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
"shell": "system"
|
||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||
"tags": []
|
||||
// "tags": []
|
||||
}
|
||||
]
|
||||
|
|
|
@ -21,12 +21,12 @@ use ui::prelude::*;
|
|||
use util::ResultExt as _;
|
||||
use workspace::{Item, Workspace};
|
||||
|
||||
actions!(acp, [OpenDebugTools]);
|
||||
actions!(dev, [OpenAcpLogs]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(
|
||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| {
|
||||
workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
|
||||
let acp_tools =
|
||||
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
|
||||
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
|
||||
|
|
|
@ -664,7 +664,7 @@ impl Thread {
|
|||
}
|
||||
|
||||
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.clone()
|
||||
|
@ -2097,7 +2097,7 @@ impl Thread {
|
|||
}
|
||||
|
||||
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");
|
||||
return;
|
||||
};
|
||||
|
@ -2416,7 +2416,7 @@ impl Thread {
|
|||
}
|
||||
|
||||
let Some(ConfiguredModel { model, provider }) =
|
||||
LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
|
||||
LanguageModelRegistry::read_global(cx).thread_summary_model()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
@ -5410,10 +5410,13 @@ fn main() {{
|
|||
}),
|
||||
cx,
|
||||
);
|
||||
registry.set_thread_summary_model(Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}));
|
||||
registry.set_thread_summary_model(
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
@ -228,7 +228,7 @@ impl NativeAgent {
|
|||
) -> Entity<AcpThread> {
|
||||
let connection = Rc::new(NativeAgentConnection(cx.entity()));
|
||||
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.set_summarization_model(summarization_model, cx);
|
||||
|
@ -524,7 +524,7 @@ impl NativeAgent {
|
|||
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
let default_model = registry.default_model().map(|m| m.model);
|
||||
let summarization_model = registry.thread_summary_model(cx).map(|m| m.model);
|
||||
let summarization_model = registry.thread_summary_model().map(|m| m.model);
|
||||
|
||||
for session in self.sessions.values_mut() {
|
||||
session.thread.update(cx, |thread, cx| {
|
||||
|
|
|
@ -72,6 +72,7 @@ async fn test_echo(cx: &mut TestAppContext) {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_thinking(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
@ -471,7 +472,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
|||
tool_name: ToolRequiringPermission::name().into(),
|
||||
is_error: true,
|
||||
content: "Permission to run tool denied by user".into(),
|
||||
output: None
|
||||
output: Some("Permission to run tool denied by user".into())
|
||||
})
|
||||
]
|
||||
);
|
||||
|
@ -1347,6 +1348,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
@ -1820,11 +1822,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
|||
let clock = Arc::new(clock::FakeSystemClock::new());
|
||||
let client = Client::new(clock, http_client, cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
Project::init_settings(cx);
|
||||
agent_settings::init(cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store, client.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
agent_settings::init(cx);
|
||||
});
|
||||
cx.executor().forbid_parking();
|
||||
|
||||
|
|
|
@ -732,7 +732,17 @@ impl Thread {
|
|||
stream.update_tool_call_fields(
|
||||
&tool_use.id,
|
||||
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,
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -1557,7 +1567,7 @@ impl Thread {
|
|||
tool_name: tool_use.name,
|
||||
is_error: true,
|
||||
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> {
|
||||
let event = self.0.next().await;
|
||||
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(
|
||||
|
|
|
@ -273,6 +273,13 @@ impl AgentTool for EditFileTool {
|
|||
|
||||
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
||||
event_stream.update_diff(diff.clone());
|
||||
let _finalize_diff = util::defer({
|
||||
let diff = diff.downgrade();
|
||||
let mut cx = cx.clone();
|
||||
move || {
|
||||
diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
|
||||
}
|
||||
});
|
||||
|
||||
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let old_text = cx
|
||||
|
@ -389,8 +396,6 @@ impl AgentTool for EditFileTool {
|
|||
})
|
||||
.await;
|
||||
|
||||
diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
|
||||
|
||||
let input_path = input.path.display();
|
||||
if unified_diff.is_empty() {
|
||||
anyhow::ensure!(
|
||||
|
@ -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) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
|
|
@ -162,12 +162,34 @@ impl AgentConnection for AcpConnection {
|
|||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||
let mcp_servers = context_server_store
|
||||
.configured_server_ids()
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let configuration = context_server_store.configuration_for_server(id)?;
|
||||
let command = configuration.command();
|
||||
Some(acp::McpServer {
|
||||
name: id.0.to_string(),
|
||||
command: command.path.clone(),
|
||||
args: command.args.clone(),
|
||||
env: if let Some(env) = command.env.as_ref() {
|
||||
env.iter()
|
||||
.map(|(name, value)| acp::EnvVariable {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let response = conn
|
||||
.new_session(acp::NewSessionRequest {
|
||||
mcp_servers: vec![],
|
||||
cwd,
|
||||
})
|
||||
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
|
|
|
@ -1,524 +0,0 @@
|
|||
// Translates old acp agents into the new schema
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol as acp;
|
||||
use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
use project::Project;
|
||||
use std::{any::Any, cell::RefCell, path::Path, rc::Rc};
|
||||
use ui::App;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::AgentServerCommand;
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct OldAcpClientDelegate {
|
||||
thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
cx: AsyncApp,
|
||||
next_tool_call_id: Rc<RefCell<u64>>,
|
||||
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
|
||||
}
|
||||
|
||||
impl OldAcpClientDelegate {
|
||||
fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
cx,
|
||||
next_tool_call_id: Rc::new(RefCell::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_old::Client for OldAcpClientDelegate {
|
||||
async fn stream_assistant_message_chunk(
|
||||
&self,
|
||||
params: acp_old::StreamAssistantMessageChunkParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread
|
||||
.borrow()
|
||||
.update(cx, |thread, cx| match params.chunk {
|
||||
acp_old::AssistantMessageChunk::Text { text } => {
|
||||
thread.push_assistant_content_block(text.into(), false, cx)
|
||||
}
|
||||
acp_old::AssistantMessageChunk::Thought { thought } => {
|
||||
thread.push_assistant_content_block(thought.into(), true, cx)
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn request_tool_call_confirmation(
|
||||
&self,
|
||||
request: acp_old::RequestToolCallConfirmationParams,
|
||||
) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
|
||||
self.next_tool_call_id.replace(old_acp_id);
|
||||
|
||||
let tool_call = into_new_tool_call(
|
||||
acp::ToolCallId(old_acp_id.to_string().into()),
|
||||
request.tool_call,
|
||||
);
|
||||
|
||||
let mut options = match request.confirmation {
|
||||
acp_old::ToolCallConfirmation::Edit { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow Edits".to_string(),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", root_command),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Mcp {
|
||||
server_name,
|
||||
tool_name,
|
||||
..
|
||||
} => vec![
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", server_name),
|
||||
),
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", tool_name),
|
||||
),
|
||||
],
|
||||
acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow".to_string(),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Other { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow".to_string(),
|
||||
)],
|
||||
};
|
||||
|
||||
options.extend([
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::Allow,
|
||||
acp::PermissionOptionKind::AllowOnce,
|
||||
"Allow".to_string(),
|
||||
),
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::Reject,
|
||||
acp::PermissionOptionKind::RejectOnce,
|
||||
"Reject".to_string(),
|
||||
),
|
||||
]);
|
||||
|
||||
let mut outcomes = Vec::with_capacity(options.len());
|
||||
let mut acp_options = Vec::with_capacity(options.len());
|
||||
|
||||
for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
|
||||
outcomes.push(outcome);
|
||||
acp_options.push(acp::PermissionOption {
|
||||
id: acp::PermissionOptionId(index.to_string().into()),
|
||||
name: label,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
|
||||
let response = cx
|
||||
.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(tool_call.into(), acp_options, cx)
|
||||
})
|
||||
})??
|
||||
.context("Failed to update thread")?
|
||||
.await;
|
||||
|
||||
let outcome = match response {
|
||||
Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
|
||||
Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
|
||||
};
|
||||
|
||||
Ok(acp_old::RequestToolCallConfirmationResponse {
|
||||
id: acp_old::ToolCallId(old_acp_id),
|
||||
outcome,
|
||||
})
|
||||
}
|
||||
|
||||
async fn push_tool_call(
|
||||
&self,
|
||||
request: acp_old::PushToolCallParams,
|
||||
) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
|
||||
self.next_tool_call_id.replace(old_acp_id);
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.upsert_tool_call(
|
||||
into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})??
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp_old::PushToolCallResponse {
|
||||
id: acp_old::ToolCallId(old_acp_id),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_tool_call(
|
||||
&self,
|
||||
request: acp_old::UpdateToolCallParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.update_tool_call(
|
||||
acp::ToolCallUpdate {
|
||||
id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(into_new_tool_call_status(request.status)),
|
||||
content: Some(
|
||||
request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_content)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.update_plan(
|
||||
acp::Plan {
|
||||
entries: request
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(into_new_plan_entry)
|
||||
.collect(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
|
||||
) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
|
||||
let content = self
|
||||
.cx
|
||||
.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.read_text_file(path, line, limit, false, cx)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
Ok(acp_old::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
&self,
|
||||
acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
self.cx
|
||||
.update(|cx| {
|
||||
self.thread
|
||||
.borrow()
|
||||
.update(cx, |thread, cx| thread.write_text_file(path, content, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
|
||||
acp::ToolCall {
|
||||
id,
|
||||
title: request.label,
|
||||
kind: acp_kind_from_old_icon(request.icon),
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
content: request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_content)
|
||||
.collect(),
|
||||
locations: request
|
||||
.locations
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_location)
|
||||
.collect(),
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
|
||||
match icon {
|
||||
acp_old::Icon::FileSearch => acp::ToolKind::Search,
|
||||
acp_old::Icon::Folder => acp::ToolKind::Search,
|
||||
acp_old::Icon::Globe => acp::ToolKind::Search,
|
||||
acp_old::Icon::Hammer => acp::ToolKind::Other,
|
||||
acp_old::Icon::LightBulb => acp::ToolKind::Think,
|
||||
acp_old::Icon::Pencil => acp::ToolKind::Edit,
|
||||
acp_old::Icon::Regex => acp::ToolKind::Search,
|
||||
acp_old::Icon::Terminal => acp::ToolKind::Execute,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
|
||||
match status {
|
||||
acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
|
||||
acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
|
||||
acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
|
||||
match content {
|
||||
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
|
||||
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
|
||||
diff: into_new_diff(diff),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
|
||||
acp::Diff {
|
||||
path: diff.path,
|
||||
old_text: diff.old_text,
|
||||
new_text: diff.new_text,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
|
||||
acp::ToolCallLocation {
|
||||
path: location.path,
|
||||
line: location.line,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
|
||||
acp::PlanEntry {
|
||||
content: entry.content,
|
||||
priority: into_new_plan_priority(entry.priority),
|
||||
status: into_new_plan_status(entry.status),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
|
||||
match priority {
|
||||
acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
|
||||
acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
|
||||
acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
|
||||
match status {
|
||||
acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
|
||||
acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
|
||||
acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpConnection {
|
||||
pub name: &'static str,
|
||||
pub connection: acp_old::AgentConnection,
|
||||
pub _child_status: Task<Result<()>>,
|
||||
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
}
|
||||
|
||||
impl AcpConnection {
|
||||
pub fn stdio(
|
||||
name: &'static str,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Self>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().unwrap();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
log::trace!("Spawned (pid: {})", child.id());
|
||||
|
||||
let foreground_executor = cx.foreground_executor().clone();
|
||||
|
||||
let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
|
||||
|
||||
let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
|
||||
OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
|
||||
stdin,
|
||||
stdout,
|
||||
move |fut| foreground_executor.spawn(fut).detach(),
|
||||
);
|
||||
|
||||
let io_task = cx.background_spawn(async move {
|
||||
io_fut.await.log_err();
|
||||
});
|
||||
|
||||
let child_status = cx.background_spawn(async move {
|
||||
let result = match child.status().await {
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
Ok(result) if result.success() => Ok(()),
|
||||
Ok(result) => Err(anyhow!(result)),
|
||||
};
|
||||
drop(io_task);
|
||||
result
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
connection,
|
||||
_child_status: child_status,
|
||||
current_thread: thread_rc,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
_cwd: &Path,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let task = self.connection.request_any(
|
||||
acp_old::InitializeParams {
|
||||
protocol_version: acp_old::ProtocolVersion::latest(),
|
||||
}
|
||||
.into_any(),
|
||||
);
|
||||
let current_thread = self.current_thread.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let result = task.await?;
|
||||
let result = acp_old::InitializeParams::response_from_any(result)?;
|
||||
|
||||
if !result.is_authenticated {
|
||||
anyhow::bail!(AuthRequired::new())
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
let session_id = acp::SessionId("acp-old-no-id".into());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
AcpThread::new(self.name, self.clone(), project, action_log, session_id)
|
||||
});
|
||||
current_thread.replace(thread.downgrade());
|
||||
thread
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::AuthenticateParams.into_any());
|
||||
cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let chunks = params
|
||||
.prompt
|
||||
.into_iter()
|
||||
.filter_map(|block| match block {
|
||||
acp::ContentBlock::Text(text) => {
|
||||
Some(acp_old::UserMessageChunk::Text { text: text.text })
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
|
||||
path: link.uri.into(),
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::SendUserMessageParams { chunks }.into_any());
|
||||
cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
anyhow::Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: false,
|
||||
audio: false,
|
||||
embedded_context: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::CancelSendMessageParams.into_any());
|
||||
cx.foreground_executor()
|
||||
.spawn(async move {
|
||||
task.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
|
@ -1,376 +0,0 @@
|
|||
use acp_tools::AcpConnectionRegistry;
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use futures::AsyncBufReadExt as _;
|
||||
use futures::channel::oneshot;
|
||||
use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, cell::RefCell};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
|
||||
use crate::{AgentServerCommand, acp::UnsupportedVersion};
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError};
|
||||
|
||||
pub struct AcpConnection {
|
||||
server_name: &'static str,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
_io_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
pub struct AcpSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
suppress_abort_err: bool,
|
||||
}
|
||||
|
||||
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
|
||||
|
||||
impl AcpConnection {
|
||||
pub async fn stdio(
|
||||
server_name: &'static str,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.envs(command.env.iter().flatten())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdout = child.stdout.take().context("Failed to take stdout")?;
|
||||
let stdin = child.stdin.take().context("Failed to take stdin")?;
|
||||
let stderr = child.stderr.take().context("Failed to take stderr")?;
|
||||
log::trace!("Spawned (pid: {})", child.id());
|
||||
|
||||
let sessions = Rc::new(RefCell::new(HashMap::default()));
|
||||
|
||||
let client = ClientDelegate {
|
||||
sessions: sessions.clone(),
|
||||
cx: cx.clone(),
|
||||
};
|
||||
let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
|
||||
let foreground_executor = cx.foreground_executor().clone();
|
||||
move |fut| {
|
||||
foreground_executor.spawn(fut).detach();
|
||||
}
|
||||
});
|
||||
|
||||
let io_task = cx.background_spawn(io_task);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut stderr = BufReader::new(stderr);
|
||||
let mut line = String::new();
|
||||
while let Ok(n) = stderr.read_line(&mut line).await
|
||||
&& n > 0
|
||||
{
|
||||
log::warn!("agent stderr: {}", &line);
|
||||
line.clear();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn({
|
||||
let sessions = sessions.clone();
|
||||
async move |cx| {
|
||||
let status = child.status().await?;
|
||||
|
||||
for session in sessions.borrow().values() {
|
||||
session
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.emit_load_error(LoadError::Exited { status }, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let connection = Rc::new(connection);
|
||||
|
||||
cx.update(|cx| {
|
||||
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
|
||||
registry.set_active_connection(server_name, &connection, cx)
|
||||
});
|
||||
})?;
|
||||
|
||||
let response = connection
|
||||
.initialize(acp::InitializeRequest {
|
||||
protocol_version: acp::VERSION,
|
||||
client_capabilities: acp::ClientCapabilities {
|
||||
fs: acp::FileSystemCapability {
|
||||
read_text_file: true,
|
||||
write_text_file: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
|
||||
return Err(UnsupportedVersion.into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
auth_methods: response.auth_methods,
|
||||
connection,
|
||||
server_name,
|
||||
sessions,
|
||||
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
|
||||
_io_task: io_task,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
cwd: &Path,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
cx.spawn(async move |cx| {
|
||||
let response = conn
|
||||
.new_session(acp::NewSessionRequest {
|
||||
mcp_servers: vec![],
|
||||
cwd,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
let mut error = AuthRequired::new();
|
||||
|
||||
if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
|
||||
error = error.with_description(err.message);
|
||||
}
|
||||
|
||||
anyhow!(error)
|
||||
} else {
|
||||
anyhow!(err)
|
||||
}
|
||||
})?;
|
||||
|
||||
let session_id = response.session_id;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||
let thread = cx.new(|_cx| {
|
||||
AcpThread::new(
|
||||
self.server_name,
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let session = AcpSession {
|
||||
thread: thread.downgrade(),
|
||||
suppress_abort_err: false,
|
||||
};
|
||||
sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
Ok(thread)
|
||||
})
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||
&self.auth_methods
|
||||
}
|
||||
|
||||
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
|
||||
let conn = self.connection.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = conn
|
||||
.authenticate(acp::AuthenticateRequest {
|
||||
method_id: method_id.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let session_id = params.session_id.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = conn.prompt(params).await;
|
||||
|
||||
let mut suppress_abort_err = false;
|
||||
|
||||
if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
|
||||
suppress_abort_err = session.suppress_abort_err;
|
||||
session.suppress_abort_err = false;
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) => {
|
||||
if err.code != ErrorCode::INTERNAL_ERROR.code {
|
||||
anyhow::bail!(err)
|
||||
}
|
||||
|
||||
let Some(data) = &err.data else {
|
||||
anyhow::bail!(err)
|
||||
};
|
||||
|
||||
// Temporary workaround until the following PR is generally available:
|
||||
// https://github.com/google-gemini/gemini-cli/pull/6656
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ErrorDetails {
|
||||
details: Box<str>,
|
||||
}
|
||||
|
||||
match serde_json::from_value(data.clone()) {
|
||||
Ok(ErrorDetails { details }) => {
|
||||
if suppress_abort_err && details.contains("This operation was aborted")
|
||||
{
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(details))
|
||||
}
|
||||
}
|
||||
Err(_) => Err(anyhow!(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
self.prompt_capabilities
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
||||
session.suppress_abort_err = true;
|
||||
}
|
||||
let conn = self.connection.clone();
|
||||
let params = acp::CancelNotification {
|
||||
session_id: session_id.clone(),
|
||||
};
|
||||
cx.foreground_executor()
|
||||
.spawn(async move { conn.cancel(params).await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegate {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
cx: AsyncApp,
|
||||
}
|
||||
|
||||
impl acp::Client for ClientDelegate {
|
||||
async fn request_permission(
|
||||
&self,
|
||||
arguments: acp::RequestPermissionRequest,
|
||||
) -> Result<acp::RequestPermissionResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let rx = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
|
||||
})?;
|
||||
|
||||
let result = rx?.await;
|
||||
|
||||
let outcome = match result {
|
||||
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
|
||||
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
|
||||
};
|
||||
|
||||
Ok(acp::RequestPermissionResponse { outcome })
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
&self,
|
||||
arguments: acp::WriteTextFileRequest,
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.write_text_file(arguments.path, arguments.content, cx)
|
||||
})?;
|
||||
|
||||
task.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
arguments: acp::ReadTextFileRequest,
|
||||
) -> Result<acp::ReadTextFileResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
|
||||
})?;
|
||||
|
||||
let content = task.await?;
|
||||
|
||||
Ok(acp::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn session_notification(
|
||||
&self,
|
||||
notification: acp::SessionNotification,
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let sessions = self.sessions.borrow();
|
||||
let session = sessions
|
||||
.get(¬ification.session_id)
|
||||
.context("Failed to get session")?;
|
||||
|
||||
session.thread.update(cx, |thread, cx| {
|
||||
thread.handle_session_update(notification.update, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use agent2::HistoryStore;
|
|||
use collections::HashMap;
|
||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||
use gpui::{
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
|
||||
TextStyleRefinement, WeakEntity, Window,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
|
@ -154,10 +154,22 @@ impl EntryViewState {
|
|||
});
|
||||
}
|
||||
}
|
||||
AgentThreadEntry::AssistantMessage(_) => {
|
||||
if index == self.entries.len() {
|
||||
self.entries.push(Entry::empty())
|
||||
}
|
||||
AgentThreadEntry::AssistantMessage(message) => {
|
||||
let entry = if let Some(Entry::AssistantMessage(entry)) =
|
||||
self.entries.get_mut(index)
|
||||
{
|
||||
entry
|
||||
} else {
|
||||
self.set_entry(
|
||||
index,
|
||||
Entry::AssistantMessage(AssistantMessageEntry::default()),
|
||||
);
|
||||
let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
|
||||
unreachable!()
|
||||
};
|
||||
entry
|
||||
};
|
||||
entry.sync(message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -177,7 +189,7 @@ impl EntryViewState {
|
|||
pub fn settings_changed(&mut self, cx: &mut App) {
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
Entry::UserMessage { .. } => {}
|
||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||
Entry::Content(response_views) => {
|
||||
for view in response_views.values() {
|
||||
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
|
||||
|
@ -208,9 +220,29 @@ pub enum ViewEvent {
|
|||
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)]
|
||||
pub enum Entry {
|
||||
UserMessage(Entity<MessageEditor>),
|
||||
AssistantMessage(AssistantMessageEntry),
|
||||
Content(HashMap<EntityId, AnyEntity>),
|
||||
}
|
||||
|
||||
|
@ -218,7 +250,7 @@ impl Entry {
|
|||
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
||||
match self {
|
||||
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())
|
||||
}
|
||||
|
||||
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>> {
|
||||
match self {
|
||||
Self::Content(map) => Some(map),
|
||||
|
@ -254,7 +296,7 @@ impl Entry {
|
|||
pub fn has_content(&self) -> bool {
|
||||
match self {
|
||||
Self::Content(map) => !map.is_empty(),
|
||||
Self::UserMessage(_) => false,
|
||||
Self::UserMessage(_) | Self::AssistantMessage(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,20 +3,23 @@ mod configure_context_server_modal;
|
|||
mod manage_profiles_modal;
|
||||
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 anyhow::Result;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use cloud_llm_client::Plan;
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
|
||||
use extension::ExtensionManifest;
|
||||
use extension_host::ExtensionStore;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||
Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
|
||||
WeakEntity, percentage,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
|
@ -34,7 +37,7 @@ use ui::{
|
|||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
use workspace::{Workspace, create_and_open_local_file};
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
|
||||
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||
|
@ -1058,10 +1061,39 @@ impl AgentConfiguration {
|
|||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("External Agents"))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Headline::new("External Agents"))
|
||||
.child(
|
||||
Button::new("add-agent", "Add Agent")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(
|
||||
move |_, window, cx| {
|
||||
if let Some(workspace) = window.root().flatten() {
|
||||
let workspace = workspace.downgrade();
|
||||
window
|
||||
.spawn(cx, async |cx| {
|
||||
open_new_agent_servers_entry_in_settings_editor(
|
||||
workspace,
|
||||
cx,
|
||||
).await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
.child(
|
||||
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),
|
||||
),
|
||||
|
@ -1324,3 +1356,109 @@ fn show_unable_to_uninstall_extension_with_context_server(
|
|||
|
||||
workspace.toggle_status_toast(status_toast, cx);
|
||||
}
|
||||
|
||||
async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
workspace: WeakEntity<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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ use zed_actions::agent::ReauthenticateAgent;
|
|||
|
||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::ui::AcpOnboardingModal;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||
|
@ -77,7 +78,10 @@ use workspace::{
|
|||
};
|
||||
use zed_actions::{
|
||||
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
||||
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
|
||||
agent::{
|
||||
OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
|
||||
ToggleModelSelector,
|
||||
},
|
||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||
};
|
||||
|
||||
|
@ -201,6 +205,9 @@ pub fn init(cx: &mut App) {
|
|||
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
|
||||
AgentOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
|
||||
AcpOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
||||
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
||||
window.refresh();
|
||||
|
@ -591,17 +598,6 @@ impl AgentPanel {
|
|||
None
|
||||
};
|
||||
|
||||
// Wait for the Gemini/Native feature flag to be available.
|
||||
let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
|
||||
if !client.status().borrow().is_signed_out() {
|
||||
cx.update(|_, cx| {
|
||||
cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
}
|
||||
|
||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||
let panel = cx.new(|cx| {
|
||||
Self::new(
|
||||
|
@ -1852,19 +1848,6 @@ impl AgentPanel {
|
|||
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 {
|
||||
self.selected_agent.clone()
|
||||
}
|
||||
|
@ -1875,6 +1858,11 @@ impl AgentPanel {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_agent != agent {
|
||||
self.selected_agent = agent.clone();
|
||||
self.serialize(cx);
|
||||
}
|
||||
|
||||
match agent {
|
||||
AgentType::Zed => {
|
||||
window.dispatch_action(
|
||||
|
@ -2555,7 +2543,7 @@ impl AgentPanel {
|
|||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
panel.new_agent_thread(
|
||||
AgentType::NativeAgent,
|
||||
window,
|
||||
cx,
|
||||
|
@ -2581,7 +2569,7 @@ impl AgentPanel {
|
|||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
panel.new_agent_thread(
|
||||
AgentType::TextThread,
|
||||
window,
|
||||
cx,
|
||||
|
@ -2609,7 +2597,7 @@ impl AgentPanel {
|
|||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
panel.new_agent_thread(
|
||||
AgentType::Gemini,
|
||||
window,
|
||||
cx,
|
||||
|
@ -2636,7 +2624,7 @@ impl AgentPanel {
|
|||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
panel.new_agent_thread(
|
||||
AgentType::ClaudeCode,
|
||||
window,
|
||||
cx,
|
||||
|
@ -2669,7 +2657,7 @@ impl AgentPanel {
|
|||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
panel.new_agent_thread(
|
||||
AgentType::Custom {
|
||||
name: agent_name
|
||||
.clone(),
|
||||
|
@ -2693,9 +2681,9 @@ impl AgentPanel {
|
|||
})
|
||||
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
|
||||
menu.separator().link(
|
||||
"Add Your Own Agent",
|
||||
"Add Other Agents",
|
||||
OpenBrowser {
|
||||
url: "https://agentclientprotocol.com/".into(),
|
||||
url: zed_urls::external_agents_docs(cx),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
|
|
|
@ -6,7 +6,8 @@ use feature_flags::ZedProFeatureFlag;
|
|||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||
LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
@ -76,6 +77,7 @@ pub struct LanguageModelPickerDelegate {
|
|||
all_models: Arc<GroupedModels>,
|
||||
filtered_entries: Vec<LanguageModelPickerEntry>,
|
||||
selected_index: usize,
|
||||
_authenticate_all_providers_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
|
@ -96,6 +98,7 @@ impl LanguageModelPickerDelegate {
|
|||
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
|
||||
filtered_entries: entries,
|
||||
get_active_model: Arc::new(get_active_model),
|
||||
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
|
||||
_subscriptions: vec![cx.subscribe_in(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
window,
|
||||
|
@ -139,6 +142,56 @@ impl LanguageModelPickerDelegate {
|
|||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Authenticates all providers in the [`LanguageModelRegistry`].
|
||||
///
|
||||
/// We do this so that we can populate the language selector with all of the
|
||||
/// models from the configured providers.
|
||||
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<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> {
|
||||
(self.get_active_model)(cx)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
mod acp_onboarding_modal;
|
||||
mod agent_notification;
|
||||
mod burn_mode_tooltip;
|
||||
mod context_pill;
|
||||
|
@ -6,6 +7,7 @@ mod onboarding_modal;
|
|||
pub mod preview;
|
||||
mod unavailable_editing_tooltip;
|
||||
|
||||
pub use acp_onboarding_modal::*;
|
||||
pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use context_pill::*;
|
||||
|
|
254
crates/agent_ui/src/ui/acp_onboarding_modal.rs
Normal file
254
crates/agent_ui/src/ui/acp_onboarding_modal.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -43,3 +43,11 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
|
|||
server_url = server_url(cx)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the URL to Zed AI's external agents documentation.
|
||||
pub fn external_agents_docs(cx: &App) -> String {
|
||||
format!(
|
||||
"{server_url}/docs/ai/external-agents",
|
||||
server_url = server_url(cx)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use anyhow::Result;
|
||||
use db::{
|
||||
define_connection, query,
|
||||
sqlez::{bindable::Column, statement::Statement},
|
||||
query,
|
||||
sqlez::{
|
||||
bindable::Column, domain::Domain, statement::Statement,
|
||||
thread_safe_connection::ThreadSafeConnection,
|
||||
},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -50,8 +53,11 @@ impl Column for SerializedCommandInvocation {
|
|||
}
|
||||
}
|
||||
|
||||
define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
|
||||
&[sql!(
|
||||
pub struct CommandPaletteDB(ThreadSafeConnection);
|
||||
|
||||
impl Domain for CommandPaletteDB {
|
||||
const NAME: &str = stringify!(CommandPaletteDB);
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE IF NOT EXISTS command_invocations(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
command_name TEXT NOT NULL,
|
||||
|
@ -59,7 +65,9 @@ define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()>
|
|||
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
);
|
||||
}
|
||||
|
||||
db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
|
||||
|
||||
impl CommandPaletteDB {
|
||||
pub async fn write_command_invocation(
|
||||
|
|
|
@ -110,11 +110,14 @@ pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection {
|
|||
}
|
||||
|
||||
/// Implements a basic DB wrapper for a given domain
|
||||
///
|
||||
/// Arguments:
|
||||
/// - static variable name for connection
|
||||
/// - type of connection wrapper
|
||||
/// - dependencies, whose migrations should be run prior to this domain's migrations
|
||||
#[macro_export]
|
||||
macro_rules! define_connection {
|
||||
(pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
macro_rules! static_connection {
|
||||
($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => {
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
|
||||
|
@ -123,16 +126,6 @@ macro_rules! define_connection {
|
|||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
impl $t {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn open_test_db(name: &'static str) -> Self {
|
||||
|
@ -142,7 +135,8 @@ macro_rules! define_connection {
|
|||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
$t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id))))
|
||||
#[allow(unused_parens)]
|
||||
$t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id))))
|
||||
});
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
|
@ -153,46 +147,10 @@ macro_rules! define_connection {
|
|||
} else {
|
||||
$crate::RELEASE_CHANNEL.dev_name()
|
||||
};
|
||||
$t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope)))
|
||||
#[allow(unused_parens)]
|
||||
$t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope)))
|
||||
});
|
||||
};
|
||||
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
$t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id))))
|
||||
});
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
let db_dir = $crate::database_dir();
|
||||
let scope = if false $(|| stringify!($global) == "global")? {
|
||||
"global"
|
||||
} else {
|
||||
$crate::RELEASE_CHANNEL.dev_name()
|
||||
};
|
||||
$t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope)))
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
|
||||
|
@ -219,17 +177,12 @@ mod tests {
|
|||
enum BadDB {}
|
||||
|
||||
impl Domain for BadDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[
|
||||
sql!(CREATE TABLE test(value);),
|
||||
// failure because test already exists
|
||||
sql!(CREATE TABLE test(value);),
|
||||
]
|
||||
}
|
||||
const NAME: &str = "db_tests";
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql!(CREATE TABLE test(value);),
|
||||
// failure because test already exists
|
||||
sql!(CREATE TABLE test(value);),
|
||||
];
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
|
@ -251,25 +204,15 @@ mod tests {
|
|||
enum CorruptedDB {}
|
||||
|
||||
impl Domain for CorruptedDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
const NAME: &str = "db_tests";
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
||||
}
|
||||
|
||||
enum GoodDB {}
|
||||
|
||||
impl Domain for GoodDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
const NAME: &str = "db_tests"; //Notice same name
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)];
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
|
@ -305,25 +248,16 @@ mod tests {
|
|||
enum CorruptedDB {}
|
||||
|
||||
impl Domain for CorruptedDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
const NAME: &str = "db_tests";
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
||||
}
|
||||
|
||||
enum GoodDB {}
|
||||
|
||||
impl Domain for GoodDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
const NAME: &str = "db_tests"; //Notice same name
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
|
|
|
@ -2,16 +2,26 @@ use gpui::App;
|
|||
use sqlez_macros::sql;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{define_connection, query, write_and_log};
|
||||
use crate::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
write_and_log,
|
||||
};
|
||||
|
||||
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
|
||||
&[sql!(
|
||||
pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl Domain for KeyValueStore {
|
||||
const NAME: &str = stringify!(KeyValueStore);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE IF NOT EXISTS kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
);
|
||||
}
|
||||
|
||||
crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
|
||||
|
||||
pub trait Dismissable {
|
||||
const KEY: &'static str;
|
||||
|
@ -91,15 +101,19 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> =
|
||||
&[sql!(
|
||||
pub struct GlobalKeyValueStore(ThreadSafeConnection);
|
||||
|
||||
impl Domain for GlobalKeyValueStore {
|
||||
const NAME: &str = stringify!(GlobalKeyValueStore);
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE IF NOT EXISTS kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
global
|
||||
);
|
||||
}
|
||||
|
||||
crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global);
|
||||
|
||||
impl GlobalKeyValueStore {
|
||||
query! {
|
||||
|
|
|
@ -19,6 +19,10 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
|
|||
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);
|
||||
|
||||
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
||||
|
@ -216,6 +220,7 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
|
|||
let keymap = match os {
|
||||
"macos" => &KEYMAP_MACOS,
|
||||
"linux" | "freebsd" => &KEYMAP_LINUX,
|
||||
"windows" => &KEYMAP_WINDOWS,
|
||||
_ => unreachable!("Not a valid OS: {}", os),
|
||||
};
|
||||
|
||||
|
|
|
@ -2588,7 +2588,7 @@ impl Editor {
|
|||
|| binding
|
||||
.keystrokes()
|
||||
.first()
|
||||
.is_some_and(|keystroke| keystroke.modifiers.modified())
|
||||
.is_some_and(|keystroke| keystroke.display_modifiers.modified())
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -7686,16 +7686,16 @@ impl Editor {
|
|||
.keystroke()
|
||||
{
|
||||
modifiers_held = modifiers_held
|
||||
|| (&accept_keystroke.modifiers == modifiers
|
||||
&& accept_keystroke.modifiers.modified());
|
||||
|| (&accept_keystroke.display_modifiers == modifiers
|
||||
&& accept_keystroke.display_modifiers.modified());
|
||||
};
|
||||
if let Some(accept_partial_keystroke) = self
|
||||
.accept_edit_prediction_keybind(true, window, cx)
|
||||
.keystroke()
|
||||
{
|
||||
modifiers_held = modifiers_held
|
||||
|| (&accept_partial_keystroke.modifiers == modifiers
|
||||
&& accept_partial_keystroke.modifiers.modified());
|
||||
|| (&accept_partial_keystroke.display_modifiers == modifiers
|
||||
&& accept_partial_keystroke.display_modifiers.modified());
|
||||
}
|
||||
|
||||
if modifiers_held {
|
||||
|
@ -9044,7 +9044,7 @@ impl Editor {
|
|||
|
||||
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
|
||||
|
||||
let modifiers_color = if accept_keystroke.modifiers == window.modifiers() {
|
||||
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
|
@ -9056,19 +9056,19 @@ impl Editor {
|
|||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_size(TextSize::XSmall.rems(cx))
|
||||
.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.modifiers,
|
||||
&accept_keystroke.display_modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(modifiers_color),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
true,
|
||||
)))
|
||||
.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| {
|
||||
parent.child(
|
||||
Key::new(
|
||||
util::capitalize(&accept_keystroke.key),
|
||||
util::capitalize(&accept_keystroke.display_key),
|
||||
Some(Color::Default),
|
||||
)
|
||||
.size(Some(IconSize::XSmall.rems().into())),
|
||||
|
@ -9171,7 +9171,7 @@ impl Editor {
|
|||
max_width: Pixels,
|
||||
cursor_point: Point,
|
||||
style: &EditorStyle,
|
||||
accept_keystroke: Option<&gpui::Keystroke>,
|
||||
accept_keystroke: Option<&gpui::KeybindingKeystroke>,
|
||||
_window: &Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
|
@ -9249,7 +9249,7 @@ impl Editor {
|
|||
accept_keystroke.as_ref(),
|
||||
|el, accept_keystroke| {
|
||||
el.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.modifiers,
|
||||
&accept_keystroke.display_modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(Color::Default),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
|
@ -9319,7 +9319,7 @@ impl Editor {
|
|||
.child(completion),
|
||||
)
|
||||
.when_some(accept_keystroke, |el, accept_keystroke| {
|
||||
if !accept_keystroke.modifiers.modified() {
|
||||
if !accept_keystroke.display_modifiers.modified() {
|
||||
return el;
|
||||
}
|
||||
|
||||
|
@ -9338,7 +9338,7 @@ impl Editor {
|
|||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.when(is_platform_style_mac, |parent| parent.gap_1())
|
||||
.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.modifiers,
|
||||
&accept_keystroke.display_modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(if !has_completion {
|
||||
Color::Muted
|
||||
|
|
|
@ -43,10 +43,10 @@ use gpui::{
|
|||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
||||
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
||||
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||
KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
||||
ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
|
||||
Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
||||
transparent_black,
|
||||
};
|
||||
|
@ -7150,7 +7150,7 @@ fn header_jump_data(
|
|||
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
|
||||
|
||||
impl AcceptEditPredictionBinding {
|
||||
pub fn keystroke(&self) -> Option<&Keystroke> {
|
||||
pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
|
||||
if let Some(binding) = self.0.as_ref() {
|
||||
match &binding.keystrokes() {
|
||||
[keystroke, ..] => Some(keystroke),
|
||||
|
|
|
@ -1404,7 +1404,7 @@ impl ProjectItem for Editor {
|
|||
}
|
||||
|
||||
fn for_broken_project_item(
|
||||
abs_path: PathBuf,
|
||||
abs_path: &Path,
|
||||
is_local: bool,
|
||||
e: &anyhow::Error,
|
||||
window: &mut Window,
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
use anyhow::Result;
|
||||
use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
|
||||
use db::sqlez::statement::Statement;
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
domain::Domain,
|
||||
statement::Statement,
|
||||
},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use fs::MTime;
|
||||
use itertools::Itertools as _;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{define_connection, query};
|
||||
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
|
@ -83,7 +87,11 @@ impl Column for SerializedEditor {
|
|||
}
|
||||
}
|
||||
|
||||
define_connection!(
|
||||
pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl Domain for EditorDb {
|
||||
const NAME: &str = stringify!(EditorDb);
|
||||
|
||||
// Current schema shape using pseudo-rust syntax:
|
||||
// editors(
|
||||
// item_id: usize,
|
||||
|
@ -113,7 +121,8 @@ define_connection!(
|
|||
// start: usize,
|
||||
// end: usize,
|
||||
// )
|
||||
pub static ref DB: EditorDb<WorkspaceDb> = &[
|
||||
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql! (
|
||||
CREATE TABLE editors(
|
||||
item_id INTEGER NOT NULL,
|
||||
|
@ -189,7 +198,9 @@ define_connection!(
|
|||
) STRICT;
|
||||
),
|
||||
];
|
||||
);
|
||||
}
|
||||
|
||||
db::static_connection!(DB, EditorDb, [WorkspaceDb]);
|
||||
|
||||
// https://www.sqlite.org/limits.html
|
||||
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
|
||||
|
|
|
@ -98,6 +98,10 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag {
|
|||
// integration too, and we'd like to turn Gemini/Native on in new builds
|
||||
// without enabling Claude Code in old builds.
|
||||
const NAME: &'static str = "gemini-and-native";
|
||||
|
||||
fn enabled_for_all() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClaudeCodeFeatureFlag;
|
||||
|
@ -201,7 +205,7 @@ impl FeatureFlagAppExt for App {
|
|||
fn has_flag<T: FeatureFlag>(&self) -> bool {
|
||||
self.try_global::<FeatureFlags>()
|
||||
.map(|flags| flags.has_flag::<T>())
|
||||
.unwrap_or(false)
|
||||
.unwrap_or(T::enabled_for_all())
|
||||
}
|
||||
|
||||
fn is_staff(&self) -> bool {
|
||||
|
|
|
@ -4466,7 +4466,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn Language
|
|||
is_enabled
|
||||
.then(|| {
|
||||
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)
|
||||
})
|
||||
|
|
|
@ -37,10 +37,10 @@ use crate::{
|
|||
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
||||
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
|
||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
||||
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
|
||||
WindowHandle, WindowId, WindowInvalidator,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
|
||||
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
|
||||
Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
|
||||
Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||
colors::{Colors, GlobalColors},
|
||||
current_platform, hash, init_app_menus,
|
||||
};
|
||||
|
@ -263,6 +263,7 @@ pub struct App {
|
|||
pub(crate) focus_handles: Arc<FocusMap>,
|
||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
||||
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
|
||||
pub(crate) global_action_listeners:
|
||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
|
@ -312,6 +313,7 @@ impl App {
|
|||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
let entities = EntityMap::new();
|
||||
let keyboard_layout = platform.keyboard_layout();
|
||||
let keyboard_mapper = platform.keyboard_mapper();
|
||||
|
||||
let app = Rc::new_cyclic(|this| AppCell {
|
||||
app: RefCell::new(App {
|
||||
|
@ -337,6 +339,7 @@ impl App {
|
|||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||
keyboard_layout,
|
||||
keyboard_mapper,
|
||||
global_action_listeners: FxHashMap::default(),
|
||||
pending_effects: VecDeque::new(),
|
||||
pending_notifications: FxHashSet::default(),
|
||||
|
@ -376,6 +379,7 @@ impl App {
|
|||
if let Some(app) = app.upgrade() {
|
||||
let cx = &mut app.borrow_mut();
|
||||
cx.keyboard_layout = cx.platform.keyboard_layout();
|
||||
cx.keyboard_mapper = cx.platform.keyboard_mapper();
|
||||
cx.keyboard_layout_observers
|
||||
.clone()
|
||||
.retain(&(), move |callback| (callback)(cx));
|
||||
|
@ -424,6 +428,11 @@ impl App {
|
|||
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
|
||||
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
||||
where
|
||||
|
|
|
@ -4,7 +4,7 @@ mod context;
|
|||
pub use binding::*;
|
||||
pub use context::*;
|
||||
|
||||
use crate::{Action, Keystroke, is_no_action};
|
||||
use crate::{Action, AsKeystroke, Keystroke, is_no_action};
|
||||
use collections::{HashMap, HashSet};
|
||||
use smallvec::SmallVec;
|
||||
use std::any::TypeId;
|
||||
|
@ -141,7 +141,7 @@ impl Keymap {
|
|||
/// only.
|
||||
pub fn bindings_for_input(
|
||||
&self,
|
||||
input: &[Keystroke],
|
||||
input: &[impl AsKeystroke],
|
||||
context_stack: &[KeyContext],
|
||||
) -> (SmallVec<[KeyBinding; 1]>, bool) {
|
||||
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
|
||||
|
@ -192,7 +192,6 @@ impl Keymap {
|
|||
|
||||
(bindings, !pending.is_empty())
|
||||
}
|
||||
|
||||
/// Check if the given binding is enabled, given a certain key context.
|
||||
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
||||
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
||||
|
@ -639,7 +638,7 @@ mod tests {
|
|||
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
||||
let actual = keymap
|
||||
.bindings_for_action(action)
|
||||
.map(|binding| binding.keystrokes[0].unparse())
|
||||
.map(|binding| binding.keystrokes[0].inner.unparse())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected, "{:?}", action);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
|
||||
use crate::{
|
||||
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
|
||||
KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A keybinding and its associated metadata, from the keymap.
|
||||
pub struct KeyBinding {
|
||||
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) meta: Option<KeyBindingMetaIndex>,
|
||||
/// 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 {
|
||||
let context_predicate =
|
||||
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.
|
||||
|
@ -40,24 +49,22 @@ impl KeyBinding {
|
|||
keystrokes: &str,
|
||||
action: Box<dyn Action>,
|
||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
use_key_equivalents: bool,
|
||||
action_input: Option<SharedString>,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||
let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
|
||||
.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<_, _>>()?;
|
||||
|
||||
if let Some(equivalents) = key_equivalents {
|
||||
for keystroke in keystrokes.iter_mut() {
|
||||
if keystroke.key.chars().count() == 1
|
||||
&& let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
|
||||
{
|
||||
keystroke.key = key.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
keystrokes,
|
||||
action,
|
||||
|
@ -79,13 +86,13 @@ impl KeyBinding {
|
|||
}
|
||||
|
||||
/// 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() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
|
||||
if !typed.should_match(target) {
|
||||
if !typed.as_keystroke().should_match(target) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +101,7 @@ impl KeyBinding {
|
|||
}
|
||||
|
||||
/// Get the keystrokes associated with this binding
|
||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
||||
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
||||
self.keystrokes.as_slice()
|
||||
}
|
||||
|
||||
|
|
|
@ -231,7 +231,6 @@ pub(crate) trait Platform: 'static {
|
|||
|
||||
fn on_quit(&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 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_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
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 {
|
||||
""
|
||||
|
@ -272,6 +270,10 @@ pub(crate) trait Platform: 'static {
|
|||
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 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.
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
use collections::HashMap;
|
||||
|
||||
use crate::{KeybindingKeystroke, Keystroke};
|
||||
|
||||
/// A trait for platform-specific keyboard layouts
|
||||
pub trait PlatformKeyboardLayout {
|
||||
/// Get the keyboard layout ID, which should be unique to the layout
|
||||
|
@ -5,3 +9,33 @@ pub trait PlatformKeyboardLayout {
|
|||
/// Get the keyboard layout display name
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
/// A trait for platform-specific keyboard mappings
|
||||
pub trait PlatformKeyboardMapper {
|
||||
/// Map a key equivalent to its platform-specific representation
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
keystroke: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke;
|
||||
/// Get the key equivalents for the current keyboard layout,
|
||||
/// only used on macOS
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,14 @@ use std::{
|
|||
fmt::{Display, Write},
|
||||
};
|
||||
|
||||
use crate::PlatformKeyboardMapper;
|
||||
|
||||
/// This is a helper trait so that we can simplify the implementation of some functions
|
||||
pub trait AsKeystroke {
|
||||
/// Returns the GPUI representation of the keystroke.
|
||||
fn as_keystroke(&self) -> &Keystroke;
|
||||
}
|
||||
|
||||
/// A keystroke and associated metadata generated by the platform
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||
pub struct Keystroke {
|
||||
|
@ -24,6 +32,17 @@ pub struct Keystroke {
|
|||
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
|
||||
/// markdown to display it.
|
||||
#[derive(Debug)]
|
||||
|
@ -58,7 +77,7 @@ impl Keystroke {
|
|||
///
|
||||
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
||||
/// both possibilities for self against the target.
|
||||
pub fn should_match(&self, target: &Keystroke) -> bool {
|
||||
pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(key_char) = self
|
||||
.key_char
|
||||
|
@ -71,7 +90,7 @@ impl Keystroke {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
if &target.key == key_char && target.modifiers == ime_modifiers {
|
||||
if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -83,12 +102,12 @@ impl Keystroke {
|
|||
.filter(|key_char| key_char != &&self.key)
|
||||
{
|
||||
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
||||
if &target.key == key_char && target.modifiers == Modifiers::none() {
|
||||
if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
target.modifiers == self.modifiers && target.key == self.key
|
||||
target.inner.modifiers == self.modifiers && target.inner.key == self.key
|
||||
}
|
||||
|
||||
/// key syntax is:
|
||||
|
@ -200,31 +219,7 @@ impl Keystroke {
|
|||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
let mut str = String::new();
|
||||
if self.modifiers.function {
|
||||
str.push_str("fn-");
|
||||
}
|
||||
if self.modifiers.control {
|
||||
str.push_str("ctrl-");
|
||||
}
|
||||
if self.modifiers.alt {
|
||||
str.push_str("alt-");
|
||||
}
|
||||
if self.modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
str.push_str("cmd-");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
str.push_str("super-");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
str.push_str("win-");
|
||||
}
|
||||
if self.modifiers.shift {
|
||||
str.push_str("shift-");
|
||||
}
|
||||
str.push_str(&self.key);
|
||||
str
|
||||
unparse(&self.modifiers, &self.key)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
!matches!(
|
||||
key,
|
||||
|
@ -322,65 +343,15 @@ fn is_printable_key(key: &str) -> bool {
|
|||
|
||||
impl std::fmt::Display for Keystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.modifiers.control {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('^')?;
|
||||
display_modifiers(&self.modifiers, f)?;
|
||||
display_key(&self.key, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "ctrl-")?;
|
||||
}
|
||||
if self.modifiers.alt {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌥')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "alt-")?;
|
||||
}
|
||||
if self.modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌘')?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
f.write_char('❖')?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
f.write_char('⊞')?;
|
||||
}
|
||||
if self.modifiers.shift {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⇧')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "shift-")?;
|
||||
}
|
||||
let key = match self.key.as_str() {
|
||||
#[cfg(target_os = "macos")]
|
||||
"backspace" => '⌫',
|
||||
#[cfg(target_os = "macos")]
|
||||
"up" => '↑',
|
||||
#[cfg(target_os = "macos")]
|
||||
"down" => '↓',
|
||||
#[cfg(target_os = "macos")]
|
||||
"left" => '←',
|
||||
#[cfg(target_os = "macos")]
|
||||
"right" => '→',
|
||||
#[cfg(target_os = "macos")]
|
||||
"tab" => '⇥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"escape" => '⎋',
|
||||
#[cfg(target_os = "macos")]
|
||||
"shift" => '⇧',
|
||||
#[cfg(target_os = "macos")]
|
||||
"control" => '⌃',
|
||||
#[cfg(target_os = "macos")]
|
||||
"alt" => '⌥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"platform" => '⌘',
|
||||
|
||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||
key => return f.write_str(key),
|
||||
};
|
||||
f.write_char(key)
|
||||
impl std::fmt::Display for KeybindingKeystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
display_modifiers(&self.display_modifiers, f)?;
|
||||
display_key(&self.display_key, f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -600,3 +571,110 @@ pub struct Capslock {
|
|||
#[serde(default)]
|
||||
pub on: bool,
|
||||
}
|
||||
|
||||
impl AsKeystroke for Keystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsKeystroke for KeybindingKeystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if modifiers.control {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('^')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "ctrl-")?;
|
||||
}
|
||||
if modifiers.alt {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌥')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "alt-")?;
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌘')?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
f.write_char('❖')?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
f.write_char('⊞')?;
|
||||
}
|
||||
if modifiers.shift {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⇧')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "shift-")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let key = match key {
|
||||
#[cfg(target_os = "macos")]
|
||||
"backspace" => '⌫',
|
||||
#[cfg(target_os = "macos")]
|
||||
"up" => '↑',
|
||||
#[cfg(target_os = "macos")]
|
||||
"down" => '↓',
|
||||
#[cfg(target_os = "macos")]
|
||||
"left" => '←',
|
||||
#[cfg(target_os = "macos")]
|
||||
"right" => '→',
|
||||
#[cfg(target_os = "macos")]
|
||||
"tab" => '⇥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"escape" => '⎋',
|
||||
#[cfg(target_os = "macos")]
|
||||
"shift" => '⇧',
|
||||
#[cfg(target_os = "macos")]
|
||||
"control" => '⌃',
|
||||
#[cfg(target_os = "macos")]
|
||||
"alt" => '⌥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"platform" => '⌘',
|
||||
|
||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||
key => return f.write_str(key),
|
||||
};
|
||||
f.write_char(key)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn unparse(modifiers: &Modifiers, key: &str) -> String {
|
||||
let mut result = String::new();
|
||||
if modifiers.function {
|
||||
result.push_str("fn-");
|
||||
}
|
||||
if modifiers.control {
|
||||
result.push_str("ctrl-");
|
||||
}
|
||||
if modifiers.alt {
|
||||
result.push_str("alt-");
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
result.push_str("cmd-");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
result.push_str("super-");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
result.push_str("win-");
|
||||
}
|
||||
if modifiers.shift {
|
||||
result.push_str("shift-");
|
||||
}
|
||||
result.push_str(&key);
|
||||
result
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
|||
use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
|
||||
Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
|
@ -144,6 +144,10 @@ impl<P: LinuxClient + 'static> Platform for P {
|
|||
self.keyboard_layout()
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(crate::DummyKeyboardMapper)
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
use super::{
|
||||
BoolExt, MacKeyboardLayout,
|
||||
BoolExt, MacKeyboardLayout, MacKeyboardMapper,
|
||||
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
||||
events::key_to_native,
|
||||
renderer,
|
||||
|
@ -8,8 +8,9 @@ use crate::{
|
|||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
|
||||
SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams,
|
||||
hash,
|
||||
};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use block::ConcreteBlock;
|
||||
|
@ -171,6 +172,7 @@ pub(crate) struct MacPlatformState {
|
|||
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||
dock_menu: Option<id>,
|
||||
menus: Option<Vec<OwnedMenu>>,
|
||||
keyboard_mapper: Rc<MacKeyboardMapper>,
|
||||
}
|
||||
|
||||
impl Default for MacPlatform {
|
||||
|
@ -189,6 +191,9 @@ impl MacPlatform {
|
|||
#[cfg(not(feature = "font-kit"))]
|
||||
let text_system = Arc::new(crate::NoopTextSystem::new());
|
||||
|
||||
let keyboard_layout = MacKeyboardLayout::new();
|
||||
let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||
|
||||
Self(Mutex::new(MacPlatformState {
|
||||
headless,
|
||||
text_system,
|
||||
|
@ -209,6 +214,7 @@ impl MacPlatform {
|
|||
dock_menu: None,
|
||||
on_keyboard_layout_change: None,
|
||||
menus: None,
|
||||
keyboard_mapper,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -348,19 +354,19 @@ impl MacPlatform {
|
|||
let mut mask = NSEventModifierFlags::empty();
|
||||
for (modifier, flag) in &[
|
||||
(
|
||||
keystroke.modifiers.platform,
|
||||
keystroke.display_modifiers.platform,
|
||||
NSEventModifierFlags::NSCommandKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.control,
|
||||
keystroke.display_modifiers.control,
|
||||
NSEventModifierFlags::NSControlKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.alt,
|
||||
keystroke.display_modifiers.alt,
|
||||
NSEventModifierFlags::NSAlternateKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.shift,
|
||||
keystroke.display_modifiers.shift,
|
||||
NSEventModifierFlags::NSShiftKeyMask,
|
||||
),
|
||||
] {
|
||||
|
@ -373,7 +379,7 @@ impl MacPlatform {
|
|||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector,
|
||||
ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||
ns_string(key_to_native(&keystroke.display_key).as_ref()),
|
||||
)
|
||||
.autorelease();
|
||||
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
|
||||
|
@ -882,6 +888,10 @@ impl Platform for MacPlatform {
|
|||
Box::new(MacKeyboardLayout::new())
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
self.0.lock().keyboard_mapper.clone()
|
||||
}
|
||||
|
||||
fn app_path(&self) -> Result<PathBuf> {
|
||||
unsafe {
|
||||
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) {
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
let mut lock = platform.0.lock();
|
||||
let keyboard_layout = MacKeyboardLayout::new();
|
||||
lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
|
||||
drop(lock);
|
||||
callback();
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
||||
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
||||
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::VecDeque;
|
||||
|
@ -237,6 +238,10 @@ impl Platform for TestPlatform {
|
|||
Box::new(TestKeyboardLayout)
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(DummyKeyboardMapper)
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||
|
|
|
@ -1,22 +1,31 @@
|
|||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use windows::Win32::UI::{
|
||||
Input::KeyboardAndMouse::{
|
||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
|
||||
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
|
||||
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
|
||||
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
|
||||
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
|
||||
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
|
||||
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||
},
|
||||
WindowsAndMessaging::KL_NAMELENGTH,
|
||||
};
|
||||
use windows_core::HSTRING;
|
||||
|
||||
use crate::{Modifiers, PlatformKeyboardLayout};
|
||||
use crate::{
|
||||
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
};
|
||||
|
||||
pub(crate) struct WindowsKeyboardLayout {
|
||||
id: 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 {
|
||||
fn id(&self) -> &str {
|
||||
&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 {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
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(
|
||||
vkey: VIRTUAL_KEY,
|
||||
scan_code: u32,
|
||||
|
@ -140,3 +243,134 @@ pub(crate) fn generate_key_char(
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
|
||||
match key {
|
||||
// ` => VK_OEM_3
|
||||
"`" => Some((VK_OEM_3.0, false)),
|
||||
"~" => Some((VK_OEM_3.0, true)),
|
||||
"1" => Some((VK_1.0, false)),
|
||||
"!" => Some((VK_1.0, true)),
|
||||
"2" => Some((VK_2.0, false)),
|
||||
"@" => Some((VK_2.0, true)),
|
||||
"3" => Some((VK_3.0, false)),
|
||||
"#" => Some((VK_3.0, true)),
|
||||
"4" => Some((VK_4.0, false)),
|
||||
"$" => Some((VK_4.0, true)),
|
||||
"5" => Some((VK_5.0, false)),
|
||||
"%" => Some((VK_5.0, true)),
|
||||
"6" => Some((VK_6.0, false)),
|
||||
"^" => Some((VK_6.0, true)),
|
||||
"7" => Some((VK_7.0, false)),
|
||||
"&" => Some((VK_7.0, true)),
|
||||
"8" => Some((VK_8.0, false)),
|
||||
"*" => Some((VK_8.0, true)),
|
||||
"9" => Some((VK_9.0, false)),
|
||||
"(" => Some((VK_9.0, true)),
|
||||
"0" => Some((VK_0.0, false)),
|
||||
")" => Some((VK_0.0, true)),
|
||||
"-" => Some((VK_OEM_MINUS.0, false)),
|
||||
"_" => Some((VK_OEM_MINUS.0, true)),
|
||||
"=" => Some((VK_OEM_PLUS.0, false)),
|
||||
"+" => Some((VK_OEM_PLUS.0, true)),
|
||||
"[" => Some((VK_OEM_4.0, false)),
|
||||
"{" => Some((VK_OEM_4.0, true)),
|
||||
"]" => Some((VK_OEM_6.0, false)),
|
||||
"}" => Some((VK_OEM_6.0, true)),
|
||||
"\\" => Some((VK_OEM_5.0, false)),
|
||||
"|" => Some((VK_OEM_5.0, true)),
|
||||
";" => Some((VK_OEM_1.0, false)),
|
||||
":" => Some((VK_OEM_1.0, true)),
|
||||
"'" => Some((VK_OEM_7.0, false)),
|
||||
"\"" => Some((VK_OEM_7.0, true)),
|
||||
"," => Some((VK_OEM_COMMA.0, false)),
|
||||
"<" => Some((VK_OEM_COMMA.0, true)),
|
||||
"." => Some((VK_OEM_PERIOD.0, false)),
|
||||
">" => Some((VK_OEM_PERIOD.0, true)),
|
||||
"/" => Some((VK_OEM_2.0, false)),
|
||||
"?" => Some((VK_OEM_2.0, true)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
|
||||
VK_OEM_3,
|
||||
VK_OEM_MINUS,
|
||||
VK_OEM_PLUS,
|
||||
VK_OEM_4,
|
||||
VK_OEM_5,
|
||||
VK_OEM_6,
|
||||
VK_OEM_1,
|
||||
VK_OEM_7,
|
||||
VK_OEM_COMMA,
|
||||
VK_OEM_PERIOD,
|
||||
VK_OEM_2,
|
||||
VK_OEM_102,
|
||||
VK_OEM_8,
|
||||
VK_ABNT_C1,
|
||||
VK_0,
|
||||
VK_1,
|
||||
VK_2,
|
||||
VK_3,
|
||||
VK_4,
|
||||
VK_5,
|
||||
VK_6,
|
||||
VK_7,
|
||||
VK_8,
|
||||
VK_9,
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
|
||||
|
||||
#[test]
|
||||
fn test_keyboard_mapper() {
|
||||
let mapper = WindowsKeyboardMapper::new();
|
||||
|
||||
// Normal case
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: "a".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||
assert_eq!(mapped.inner, keystroke);
|
||||
assert_eq!(mapped.display_key, "a");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control());
|
||||
|
||||
// Shifted case, ctrl-$
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: "$".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||
assert_eq!(mapped.inner, keystroke);
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
|
||||
// Shifted case, but shift is true
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control_shift(),
|
||||
key: "$".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
|
||||
// Windows style
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control_shift(),
|
||||
key: "4".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||
assert_eq!(mapped.inner.key, "$");
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()>) {
|
||||
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
|
||||
}
|
||||
|
|
|
@ -215,6 +215,7 @@ pub enum IconName {
|
|||
Tab,
|
||||
Terminal,
|
||||
TerminalAlt,
|
||||
TerminalGhost,
|
||||
TextSnippet,
|
||||
TextThread,
|
||||
Thread,
|
||||
|
|
|
@ -401,12 +401,19 @@ pub fn init(cx: &mut App) {
|
|||
mod persistence {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
define_connection! {
|
||||
pub static ref IMAGE_VIEWER: ImageViewerDb<WorkspaceDb> =
|
||||
&[sql!(
|
||||
pub struct ImageViewerDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for ImageViewerDb {
|
||||
const NAME: &str = stringify!(ImageViewerDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE image_viewers (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
|
@ -417,9 +424,11 @@ mod persistence {
|
|||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)];
|
||||
)];
|
||||
}
|
||||
|
||||
db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]);
|
||||
|
||||
impl ImageViewerDb {
|
||||
query! {
|
||||
pub async fn save_image_path(
|
||||
|
|
|
@ -4,12 +4,16 @@ use crate::{
|
|||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyView, App, AsyncApp, Entity, Task, Window};
|
||||
use http_client::Result;
|
||||
use parking_lot::Mutex;
|
||||
use smol::stream::StreamExt;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FakeLanguageModelProvider {
|
||||
|
@ -106,6 +110,7 @@ pub struct FakeLanguageModel {
|
|||
>,
|
||||
)>,
|
||||
>,
|
||||
forbid_requests: AtomicBool,
|
||||
}
|
||||
|
||||
impl Default for FakeLanguageModel {
|
||||
|
@ -114,11 +119,20 @@ impl Default for FakeLanguageModel {
|
|||
provider_id: LanguageModelProviderId::from("fake".to_string()),
|
||||
provider_name: LanguageModelProviderName::from("Fake".to_string()),
|
||||
current_completion_txs: Mutex::new(Vec::new()),
|
||||
forbid_requests: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeLanguageModel {
|
||||
pub fn allow_requests(&self) {
|
||||
self.forbid_requests.store(false, SeqCst);
|
||||
}
|
||||
|
||||
pub fn forbid_requests(&self) {
|
||||
self.forbid_requests.store(true, SeqCst);
|
||||
}
|
||||
|
||||
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
|
@ -251,9 +265,18 @@ impl LanguageModel for FakeLanguageModel {
|
|||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.current_completion_txs.lock().push((request, tx));
|
||||
async move { Ok(rx.boxed()) }.boxed()
|
||||
if self.forbid_requests.load(SeqCst) {
|
||||
async move {
|
||||
Err(LanguageModelCompletionError::Other(anyhow!(
|
||||
"requests are forbidden"
|
||||
)))
|
||||
}
|
||||
.boxed()
|
||||
} else {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.current_completion_txs.lock().push((request, tx));
|
||||
async move { Ok(rx.boxed()) }.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn as_fake(&self) -> &Self {
|
||||
|
|
|
@ -6,6 +6,7 @@ use collections::BTreeMap;
|
|||
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use thiserror::Error;
|
||||
use util::maybe;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let registry = cx.new(|_cx| LanguageModelRegistry::default());
|
||||
|
@ -41,9 +42,7 @@ impl std::fmt::Debug for ConfigurationError {
|
|||
#[derive(Default)]
|
||||
pub struct LanguageModelRegistry {
|
||||
default_model: Option<ConfiguredModel>,
|
||||
/// This model is automatically configured by a user's environment after
|
||||
/// authenticating all providers. It's only used when default_model is not available.
|
||||
environment_fallback_model: Option<ConfiguredModel>,
|
||||
default_fast_model: Option<ConfiguredModel>,
|
||||
inline_assistant_model: Option<ConfiguredModel>,
|
||||
commit_message_model: Option<ConfiguredModel>,
|
||||
thread_summary_model: Option<ConfiguredModel>,
|
||||
|
@ -99,6 +98,9 @@ impl ConfiguredModel {
|
|||
|
||||
pub enum Event {
|
||||
DefaultModelChanged,
|
||||
InlineAssistantModelChanged,
|
||||
CommitMessageModelChanged,
|
||||
ThreadSummaryModelChanged,
|
||||
ProviderStateChanged(LanguageModelProviderId),
|
||||
AddedProvider(LanguageModelProviderId),
|
||||
RemovedProvider(LanguageModelProviderId),
|
||||
|
@ -224,7 +226,7 @@ impl LanguageModelRegistry {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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(
|
||||
|
@ -233,7 +235,7 @@ impl LanguageModelRegistry {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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(
|
||||
|
@ -242,7 +244,7 @@ impl LanguageModelRegistry {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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
|
||||
|
@ -276,60 +278,68 @@ impl LanguageModelRegistry {
|
|||
}
|
||||
|
||||
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) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::DefaultModelChanged),
|
||||
}
|
||||
self.default_fast_model = maybe!({
|
||||
let provider = &model.as_ref()?.provider;
|
||||
let fast_model = provider.default_fast_model(cx)?;
|
||||
Some(ConfiguredModel {
|
||||
provider: provider.clone(),
|
||||
model: fast_model,
|
||||
})
|
||||
});
|
||||
self.default_model = model;
|
||||
}
|
||||
|
||||
pub fn set_environment_fallback_model(
|
||||
pub fn set_inline_assistant_model(
|
||||
&mut self,
|
||||
model: Option<ConfiguredModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.default_model.is_none() {
|
||||
match (self.environment_fallback_model.as_ref(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::DefaultModelChanged),
|
||||
}
|
||||
match (self.inline_assistant_model.as_ref(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::InlineAssistantModelChanged),
|
||||
}
|
||||
self.environment_fallback_model = model;
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, model: Option<ConfiguredModel>) {
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn default_model(&self) -> Option<ConfiguredModel> {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.default_model
|
||||
.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,
|
||||
})
|
||||
self.default_model.clone()
|
||||
}
|
||||
|
||||
pub fn inline_assistant_model(&self) -> Option<ConfiguredModel> {
|
||||
|
@ -343,7 +353,7 @@ impl LanguageModelRegistry {
|
|||
.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)]
|
||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||
return None;
|
||||
|
@ -351,11 +361,11 @@ impl LanguageModelRegistry {
|
|||
|
||||
self.commit_message_model
|
||||
.clone()
|
||||
.or_else(|| self.default_fast_model(cx))
|
||||
.or_else(|| self.default_fast_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)]
|
||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||
return None;
|
||||
|
@ -363,7 +373,7 @@ impl LanguageModelRegistry {
|
|||
|
||||
self.thread_summary_model
|
||||
.clone()
|
||||
.or_else(|| self.default_fast_model(cx))
|
||||
.or_else(|| self.default_fast_model.clone())
|
||||
.or_else(|| self.default_model.clone())
|
||||
}
|
||||
|
||||
|
@ -400,34 +410,4 @@ mod tests {
|
|||
let providers = registry.read(cx).providers();
|
||||
assert!(providers.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) {
|
||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||
|
||||
let provider = FakeLanguageModelProvider::default();
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.register_provider(provider.clone(), cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| provider.authenticate(cx)).await.unwrap();
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
let provider = registry.provider(&provider.id()).unwrap();
|
||||
|
||||
registry.set_environment_fallback_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: provider.clone(),
|
||||
model: provider.default_model(cx).unwrap(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
let default_model = registry.default_model().unwrap();
|
||||
let fallback_model = registry.environment_fallback_model.clone().unwrap();
|
||||
|
||||
assert_eq!(default_model.model.id(), fallback_model.model.id());
|
||||
assert_eq!(default_model.provider.id(), fallback_model.provider.id());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,6 @@ ollama = { workspace = true, features = ["schemars"] }
|
|||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
open_router = { workspace = true, features = ["schemars"] }
|
||||
partial-json-fixer.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
|
@ -3,12 +3,8 @@ use std::sync::Arc;
|
|||
use ::settings::{Settings, SettingsStore};
|
||||
use client::{Client, UserStore};
|
||||
use collections::HashSet;
|
||||
use futures::future;
|
||||
use gpui::{App, AppContext as _, Context, Entity};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use gpui::{App, Context, Entity};
|
||||
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
|
||||
use provider::deepseek::DeepSeekLanguageModelProvider;
|
||||
|
||||
pub mod provider;
|
||||
|
@ -17,7 +13,7 @@ pub mod ui;
|
|||
|
||||
use crate::provider::anthropic::AnthropicLanguageModelProvider;
|
||||
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::google::GoogleLanguageModelProvider;
|
||||
use crate::provider::lmstudio::LmStudioLanguageModelProvider;
|
||||
|
@ -52,13 +48,6 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
|||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let mut already_authenticated = false;
|
||||
if !DisableAiSettings::get_global(cx).disable_ai {
|
||||
authenticate_all_providers(registry.clone(), cx);
|
||||
already_authenticated = true;
|
||||
}
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx)
|
||||
.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;
|
||||
already_authenticated = false;
|
||||
}
|
||||
|
||||
if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated {
|
||||
authenticate_all_providers(registry.clone(), cx);
|
||||
already_authenticated = true;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
@ -168,83 +151,3 @@ fn register_language_model_providers(
|
|||
registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx);
|
||||
registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
|
||||
}
|
||||
|
||||
/// Authenticates all providers in the [`LanguageModelRegistry`].
|
||||
///
|
||||
/// We do this so that we can populate the language selector with all of the
|
||||
/// models from the configured providers.
|
||||
///
|
||||
/// This function won't do anything if AI is disabled.
|
||||
fn authenticate_all_providers(registry: Entity<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();
|
||||
}
|
||||
|
|
|
@ -44,8 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i
|
|||
use crate::provider::google::{GoogleEventMapper, into_google};
|
||||
use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
|
||||
|
||||
pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
|
||||
pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
|
||||
const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
|
||||
const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
pub struct ZedDotDevSettings {
|
||||
|
@ -146,7 +146,7 @@ impl State {
|
|||
default_fast_model: None,
|
||||
recommended_models: Vec::new(),
|
||||
_fetch_models_task: cx.spawn(async move |this, cx| {
|
||||
maybe!(async {
|
||||
maybe!(async move {
|
||||
let (client, llm_api_token) = this
|
||||
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ use gpui::{
|
|||
};
|
||||
use itertools::Itertools;
|
||||
use serde_json::json;
|
||||
use settings::get_key_equivalents;
|
||||
use ui::{Button, ButtonStyle};
|
||||
use ui::{
|
||||
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
|
||||
|
@ -169,7 +168,8 @@ impl Item for KeyContextView {
|
|||
impl Render for KeyContextView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
use itertools::Itertools;
|
||||
let key_equivalents = get_key_equivalents(cx.keyboard_layout().id());
|
||||
|
||||
let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
|
||||
v_flex()
|
||||
.id("key-context-view")
|
||||
.overflow_scroll()
|
||||
|
|
|
@ -1743,6 +1743,5 @@ pub enum Event {
|
|||
}
|
||||
|
||||
impl EventEmitter<Event> for LogStore {}
|
||||
impl EventEmitter<Event> for LspLogView {}
|
||||
impl EventEmitter<EditorEvent> for LspLogView {}
|
||||
impl EventEmitter<SearchEvent> for LspLogView {}
|
||||
|
|
|
@ -1323,7 +1323,7 @@ fn render_copy_code_block_button(
|
|||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Filled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Copy Code"))
|
||||
.tooltip(Tooltip::text("Copy"))
|
||||
.on_click({
|
||||
let markdown = markdown;
|
||||
move |_event, _window, cx| {
|
||||
|
|
|
@ -850,13 +850,19 @@ impl workspace::SerializableItem for Onboarding {
|
|||
}
|
||||
|
||||
mod persistence {
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use workspace::WorkspaceDb;
|
||||
|
||||
define_connection! {
|
||||
pub static ref ONBOARDING_PAGES: OnboardingPagesDb<WorkspaceDb> =
|
||||
&[
|
||||
sql!(
|
||||
pub struct OnboardingPagesDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for OnboardingPagesDb {
|
||||
const NAME: &str = stringify!(OnboardingPagesDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE onboarding_pages (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
|
@ -866,10 +872,11 @@ mod persistence {
|
|||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
),
|
||||
];
|
||||
)];
|
||||
}
|
||||
|
||||
db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
|
||||
|
||||
impl OnboardingPagesDb {
|
||||
query! {
|
||||
pub async fn save_onboarding_page(
|
||||
|
|
|
@ -414,13 +414,19 @@ impl workspace::SerializableItem for WelcomePage {
|
|||
}
|
||||
|
||||
mod persistence {
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use workspace::WorkspaceDb;
|
||||
|
||||
define_connection! {
|
||||
pub static ref WELCOME_PAGES: WelcomePagesDb<WorkspaceDb> =
|
||||
&[
|
||||
sql!(
|
||||
pub struct WelcomePagesDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for WelcomePagesDb {
|
||||
const NAME: &str = stringify!(WelcomePagesDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = (&[sql!(
|
||||
CREATE TABLE welcome_pages (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
|
@ -430,10 +436,11 @@ mod persistence {
|
|||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
),
|
||||
];
|
||||
)]);
|
||||
}
|
||||
|
||||
db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]);
|
||||
|
||||
impl WelcomePagesDb {
|
||||
query! {
|
||||
pub async fn save_welcome_page(
|
||||
|
|
|
@ -4089,6 +4089,7 @@ impl ProjectPanel {
|
|||
.when(!is_sticky, |this| {
|
||||
this
|
||||
.when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
|
||||
.when(settings.drag_and_drop, |this| this
|
||||
.on_drag_move::<ExternalPaths>(cx.listener(
|
||||
move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
|
||||
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);
|
||||
}),
|
||||
)
|
||||
))
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
|
@ -4433,6 +4434,7 @@ impl ProjectPanel {
|
|||
div()
|
||||
.when(!is_sticky, |div| {
|
||||
div
|
||||
.when(settings.drag_and_drop, |div| div
|
||||
.on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
|
||||
this.hover_scroll_task.take();
|
||||
this.drag_target_entry = None;
|
||||
|
@ -4464,7 +4466,7 @@ impl ProjectPanel {
|
|||
}
|
||||
|
||||
},
|
||||
))
|
||||
)))
|
||||
})
|
||||
.child(
|
||||
Label::new(DELIMITER.clone())
|
||||
|
@ -4484,6 +4486,7 @@ impl ProjectPanel {
|
|||
.when(index != components_len - 1, |div|{
|
||||
let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
|
||||
div
|
||||
.when(settings.drag_and_drop, |div| div
|
||||
.on_drag_move(cx.listener(
|
||||
move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
|
||||
if event.bounds.contains(&event.event.position) {
|
||||
|
@ -4521,7 +4524,7 @@ impl ProjectPanel {
|
|||
target.index == index
|
||||
), |this| {
|
||||
this.bg(item_colors.drag_over)
|
||||
})
|
||||
}))
|
||||
})
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
|
@ -5029,7 +5032,8 @@ impl ProjectPanel {
|
|||
|
||||
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 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 {
|
||||
let has_worktree = !self.visible_entries.is_empty();
|
||||
let project = self.project.read(cx);
|
||||
let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
|
||||
let show_indent_guides =
|
||||
ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
|
||||
let panel_settings = ProjectPanelSettings::get_global(cx);
|
||||
let indent_size = panel_settings.indent_size;
|
||||
let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
|
||||
let 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_scrolled = self.scroll_handle.offset().y < px(0.);
|
||||
is_scrollable && is_scrolled
|
||||
|
@ -5205,8 +5209,10 @@ impl Render for ProjectPanel {
|
|||
h_flex()
|
||||
.id("project-panel")
|
||||
.group("project-panel")
|
||||
.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
|
||||
.on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
|
||||
.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>))
|
||||
})
|
||||
.size_full()
|
||||
.relative()
|
||||
.on_modifiers_changed(cx.listener(
|
||||
|
@ -5544,30 +5550,32 @@ impl Render for ProjectPanel {
|
|||
})),
|
||||
)
|
||||
.when(is_local, |div| {
|
||||
div.drag_over::<ExternalPaths>(|style, _, _, cx| {
|
||||
style.bg(cx.theme().colors().drop_target_background)
|
||||
div.when(panel_settings.drag_and_drop, |div| {
|
||||
div.drag_over::<ExternalPaths>(|style, _, _, cx| {
|
||||
style.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
move |this, external_paths: &ExternalPaths, window, cx| {
|
||||
this.drag_target_entry = None;
|
||||
this.hover_scroll_task.take();
|
||||
if let Some(task) = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
true,
|
||||
external_paths.paths().to_owned(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
},
|
||||
))
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
move |this, external_paths: &ExternalPaths, window, cx| {
|
||||
this.drag_target_entry = None;
|
||||
this.hover_scroll_task.take();
|
||||
if let Some(task) = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
true,
|
||||
external_paths.paths().to_owned(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
},
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ pub struct ProjectPanelSettings {
|
|||
pub scrollbar: ScrollbarSettings,
|
||||
pub show_diagnostics: ShowDiagnostics,
|
||||
pub hide_root: bool,
|
||||
pub drag_and_drop: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
|
@ -160,6 +161,10 @@ pub struct ProjectPanelSettingsContent {
|
|||
///
|
||||
/// Default: true
|
||||
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 {
|
||||
|
|
|
@ -445,7 +445,7 @@ impl SshSocket {
|
|||
}
|
||||
|
||||
async fn platform(&self) -> Result<SshPlatform> {
|
||||
let uname = self.run_command("sh", &["-c", "uname -sm"]).await?;
|
||||
let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?;
|
||||
let Some((os, arch)) = uname.split_once(" ") else {
|
||||
anyhow::bail!("unknown uname: {uname:?}")
|
||||
};
|
||||
|
@ -476,7 +476,7 @@ impl SshSocket {
|
|||
}
|
||||
|
||||
async fn shell(&self) -> String {
|
||||
match self.run_command("sh", &["-c", "echo $SHELL"]).await {
|
||||
match self.run_command("sh", &["-lc", "echo $SHELL"]).await {
|
||||
Ok(shell) => shell.trim().to_owned(),
|
||||
Err(e) => {
|
||||
log::error!("Failed to get shell: {e}");
|
||||
|
@ -1533,7 +1533,7 @@ impl RemoteConnection for SshRemoteConnection {
|
|||
|
||||
let ssh_proxy_process = match self
|
||||
.socket
|
||||
.ssh_command("sh", &["-c", &start_proxy_command])
|
||||
.ssh_command("sh", &["-lc", &start_proxy_command])
|
||||
// IMPORTANT: we kill this process when we drop the task that uses it.
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
|
@ -1910,7 +1910,7 @@ impl SshRemoteConnection {
|
|||
.run_command(
|
||||
"sh",
|
||||
&[
|
||||
"-c",
|
||||
"-lc",
|
||||
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
|
||||
],
|
||||
)
|
||||
|
@ -1988,7 +1988,7 @@ impl SshRemoteConnection {
|
|||
.run_command(
|
||||
"sh",
|
||||
&[
|
||||
"-c",
|
||||
"-lc",
|
||||
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
|
||||
],
|
||||
)
|
||||
|
@ -2036,7 +2036,7 @@ impl SshRemoteConnection {
|
|||
dst_path = &dst_path.to_string()
|
||||
)
|
||||
};
|
||||
self.socket.run_command("sh", &["-c", &script]).await?;
|
||||
self.socket.run_command("sh", &["-lc", &script]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,8 @@ use collections::{BTreeMap, HashMap, IndexMap};
|
|||
use fs::Fs;
|
||||
use gpui::{
|
||||
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 serde::Deserialize;
|
||||
|
@ -211,9 +212,6 @@ impl KeymapFile {
|
|||
}
|
||||
|
||||
pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
|
||||
let key_equivalents =
|
||||
crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id());
|
||||
|
||||
if content.is_empty() {
|
||||
return KeymapFileLoadResult::Success {
|
||||
key_bindings: Vec::new(),
|
||||
|
@ -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();
|
||||
|
||||
if !unrecognized_fields.is_empty() {
|
||||
|
@ -278,7 +270,7 @@ impl KeymapFile {
|
|||
keystrokes,
|
||||
action,
|
||||
context_predicate.clone(),
|
||||
key_equivalents,
|
||||
*use_key_equivalents,
|
||||
cx,
|
||||
);
|
||||
match result {
|
||||
|
@ -336,7 +328,7 @@ impl KeymapFile {
|
|||
keystrokes: &str,
|
||||
action: &KeymapAction,
|
||||
context: Option<Rc<KeyBindingContextPredicate>>,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
use_key_equivalents: bool,
|
||||
cx: &App,
|
||||
) -> std::result::Result<KeyBinding, String> {
|
||||
let (build_result, action_input_string) = match &action.0 {
|
||||
|
@ -404,8 +396,9 @@ impl KeymapFile {
|
|||
keystrokes,
|
||||
action,
|
||||
context,
|
||||
key_equivalents,
|
||||
use_key_equivalents,
|
||||
action_input_string.map(SharedString::from),
|
||||
cx.keyboard_mapper().as_ref(),
|
||||
) {
|
||||
Ok(key_binding) => key_binding,
|
||||
Err(InvalidKeystrokeError { keystroke }) => {
|
||||
|
@ -607,6 +600,7 @@ impl KeymapFile {
|
|||
mut operation: KeybindUpdateOperation<'a>,
|
||||
mut keymap_contents: String,
|
||||
tab_size: usize,
|
||||
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
|
||||
) -> Result<String> {
|
||||
match 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()
|
||||
.context("Failed to generate target action JSON value")?;
|
||||
let Some((index, keystrokes_str)) =
|
||||
find_binding(&keymap, &target, &target_action_value)
|
||||
find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
|
||||
else {
|
||||
anyhow::bail!("Failed to find keybinding to remove");
|
||||
};
|
||||
|
@ -681,7 +675,7 @@ impl KeymapFile {
|
|||
.context("Failed to generate source action JSON value")?;
|
||||
|
||||
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 we are only changing the keybinding (common case)
|
||||
|
@ -781,7 +775,7 @@ impl KeymapFile {
|
|||
}
|
||||
let use_key_equivalents = from.and_then(|from| {
|
||||
let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?;
|
||||
let (index, _) = find_binding(&keymap, &from, &action_value)?;
|
||||
let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?;
|
||||
Some(keymap.0[index].use_key_equivalents)
|
||||
}).unwrap_or(false);
|
||||
if use_key_equivalents {
|
||||
|
@ -808,6 +802,7 @@ impl KeymapFile {
|
|||
keymap: &'b KeymapFile,
|
||||
target: &KeybindUpdateTarget<'a>,
|
||||
target_action_value: &Value,
|
||||
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
|
||||
) -> Option<(usize, &'b str)> {
|
||||
let target_context_parsed =
|
||||
KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
|
||||
|
@ -823,8 +818,11 @@ impl KeymapFile {
|
|||
for (keystrokes_str, action) in bindings {
|
||||
let Ok(keystrokes) = keystrokes_str
|
||||
.split_whitespace()
|
||||
.map(Keystroke::parse)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(|source| {
|
||||
let keystroke = Keystroke::parse(source)?;
|
||||
Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper))
|
||||
})
|
||||
.collect::<Result<Vec<_>, InvalidKeystrokeError>>()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
@ -832,7 +830,7 @@ impl KeymapFile {
|
|||
|| !keystrokes
|
||||
.iter()
|
||||
.zip(target.keystrokes)
|
||||
.all(|(a, b)| a.should_match(b))
|
||||
.all(|(a, b)| a.inner.should_match(b))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -847,7 +845,7 @@ impl KeymapFile {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum KeybindUpdateOperation<'a> {
|
||||
Replace {
|
||||
/// Describes the keybind to create
|
||||
|
@ -916,7 +914,7 @@ impl<'a> KeybindUpdateOperation<'a> {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct KeybindUpdateTarget<'a> {
|
||||
pub context: Option<&'a str>,
|
||||
pub keystrokes: &'a [Keystroke],
|
||||
pub keystrokes: &'a [KeybindingKeystroke],
|
||||
pub action_name: &'a str,
|
||||
pub action_arguments: Option<&'a str>,
|
||||
}
|
||||
|
@ -941,6 +939,9 @@ impl<'a> KeybindUpdateTarget<'a> {
|
|||
fn keystrokes_unparsed(&self) -> String {
|
||||
let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
|
||||
for keystroke in self.keystrokes {
|
||||
// The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()`
|
||||
// here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$`
|
||||
// by default on Windows.
|
||||
keystrokes.push_str(&keystroke.unparse());
|
||||
keystrokes.push(' ');
|
||||
}
|
||||
|
@ -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 {
|
||||
User,
|
||||
Vim,
|
||||
|
@ -1020,7 +1021,7 @@ impl From<KeybindSource> for KeyBindingMetaIndex {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::Keystroke;
|
||||
use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke};
|
||||
use unindent::Unindent;
|
||||
|
||||
use crate::{
|
||||
|
@ -1049,16 +1050,27 @@ mod tests {
|
|||
operation: KeybindUpdateOperation,
|
||||
expected: impl ToString,
|
||||
) {
|
||||
let result = KeymapFile::update_keybinding(operation, input.to_string(), 4)
|
||||
.expect("Update succeeded");
|
||||
let result = KeymapFile::update_keybinding(
|
||||
operation,
|
||||
input.to_string(),
|
||||
4,
|
||||
&gpui::DummyKeyboardMapper,
|
||||
)
|
||||
.expect("Update succeeded");
|
||||
pretty_assertions::assert_eq!(expected.to_string(), result);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> {
|
||||
fn parse_keystrokes(keystrokes: &str) -> Vec<KeybindingKeystroke> {
|
||||
keystrokes
|
||||
.split(' ')
|
||||
.map(|s| Keystroke::parse(s).expect("Keystrokes valid"))
|
||||
.map(|s| {
|
||||
KeybindingKeystroke::new(
|
||||
Keystroke::parse(s).expect("Keystrokes valid"),
|
||||
false,
|
||||
&DummyKeyboardMapper,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
mod base_keymap_setting;
|
||||
mod editable_setting_control;
|
||||
mod key_equivalents;
|
||||
mod keymap_file;
|
||||
mod settings_file;
|
||||
mod settings_json;
|
||||
|
@ -14,7 +13,6 @@ use util::asset_str;
|
|||
|
||||
pub use base_keymap_setting::*;
|
||||
pub use editable_setting_control::*;
|
||||
pub use key_equivalents::*;
|
||||
pub use keymap_file::{
|
||||
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
|
||||
KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
|
||||
|
@ -89,7 +87,10 @@ pub fn default_settings() -> Cow<'static, str> {
|
|||
#[cfg(target_os = "macos")]
|
||||
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 fn default_keymap() -> Cow<'static, str> {
|
||||
|
|
|
@ -14,9 +14,9 @@ use gpui::{
|
|||
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Global, IsZero,
|
||||
KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
|
||||
KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful,
|
||||
StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred,
|
||||
div,
|
||||
KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point,
|
||||
ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,
|
||||
TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,
|
||||
};
|
||||
use language::{Language, LanguageConfig, ToOffset as _};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
|
@ -174,7 +174,7 @@ impl FilterState {
|
|||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
|
||||
struct ActionMapping {
|
||||
keystrokes: Vec<Keystroke>,
|
||||
keystrokes: Vec<KeybindingKeystroke>,
|
||||
context: Option<SharedString>,
|
||||
}
|
||||
|
||||
|
@ -236,7 +236,7 @@ struct ConflictState {
|
|||
}
|
||||
|
||||
type ConflictKeybindMapping = HashMap<
|
||||
Vec<Keystroke>,
|
||||
Vec<KeybindingKeystroke>,
|
||||
Vec<(
|
||||
Option<gpui::KeyBindingContextPredicate>,
|
||||
Vec<ConflictOrigin>,
|
||||
|
@ -414,12 +414,14 @@ impl Focusable for KeymapEditor {
|
|||
}
|
||||
}
|
||||
/// 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
|
||||
.iter()
|
||||
.zip(keystrokes2)
|
||||
.all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers)
|
||||
&& keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {
|
||||
k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers
|
||||
})
|
||||
}
|
||||
|
||||
impl KeymapEditor {
|
||||
|
@ -509,7 +511,7 @@ impl KeymapEditor {
|
|||
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 {
|
||||
SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
|
||||
SearchMode::Normal => Default::default(),
|
||||
|
@ -530,7 +532,7 @@ impl KeymapEditor {
|
|||
|
||||
let keystroke_query = keystroke_query
|
||||
.into_iter()
|
||||
.map(|keystroke| keystroke.unparse())
|
||||
.map(|keystroke| keystroke.inner.unparse())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ");
|
||||
|
||||
|
@ -554,7 +556,7 @@ impl KeymapEditor {
|
|||
async fn update_matches(
|
||||
this: WeakEntity<Self>,
|
||||
action_query: String,
|
||||
keystroke_query: Vec<Keystroke>,
|
||||
keystroke_query: Vec<KeybindingKeystroke>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<()> {
|
||||
let action_query = command_palette::normalize_action_query(&action_query);
|
||||
|
@ -603,13 +605,15 @@ impl KeymapEditor {
|
|||
{
|
||||
let query = &keystroke_query[query_cursor];
|
||||
let keystroke = &keystrokes[keystroke_cursor];
|
||||
let matches =
|
||||
query.modifiers.is_subset_of(&keystroke.modifiers)
|
||||
&& ((query.key.is_empty()
|
||||
|| query.key == keystroke.key)
|
||||
&& query.key_char.as_ref().is_none_or(
|
||||
|q_kc| q_kc == &keystroke.key,
|
||||
));
|
||||
let matches = query
|
||||
.inner
|
||||
.modifiers
|
||||
.is_subset_of(&keystroke.inner.modifiers)
|
||||
&& ((query.inner.key.is_empty()
|
||||
|| query.inner.key == keystroke.inner.key)
|
||||
&& query.inner.key_char.as_ref().is_none_or(
|
||||
|q_kc| q_kc == &keystroke.inner.key,
|
||||
));
|
||||
if matches {
|
||||
found_count += 1;
|
||||
query_cursor += 1;
|
||||
|
@ -678,7 +682,7 @@ impl KeymapEditor {
|
|||
.map(KeybindSource::from_meta)
|
||||
.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)
|
||||
.vim_mode(source == KeybindSource::Vim);
|
||||
|
||||
|
@ -1202,8 +1206,11 @@ impl KeymapEditor {
|
|||
.read(cx)
|
||||
.get_scrollbar_offset(Axis::Vertical),
|
||||
));
|
||||
cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
|
||||
.detach_and_notify_err(window, cx);
|
||||
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);
|
||||
}
|
||||
|
||||
fn copy_context_to_clipboard(
|
||||
|
@ -1422,7 +1429,7 @@ impl ProcessedBinding {
|
|||
.map(|keybind| keybind.get_action_mapping())
|
||||
}
|
||||
|
||||
fn keystrokes(&self) -> Option<&[Keystroke]> {
|
||||
fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> {
|
||||
self.ui_key_binding()
|
||||
.map(|binding| binding.keystrokes.as_slice())
|
||||
}
|
||||
|
@ -2220,7 +2227,7 @@ impl KeybindingEditorModal {
|
|||
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
|
||||
.keybind_editor
|
||||
.read_with(cx, |editor, _| editor.keystrokes().to_vec());
|
||||
|
@ -2316,6 +2323,7 @@ impl KeybindingEditorModal {
|
|||
}).unwrap_or(Ok(()))?;
|
||||
|
||||
let create = self.creating;
|
||||
let keyboard_mapper = cx.keyboard_mapper().clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let action_name = existing_keybind.action().name;
|
||||
|
@ -2328,6 +2336,7 @@ impl KeybindingEditorModal {
|
|||
new_action_args.as_deref(),
|
||||
&fs,
|
||||
tab_size,
|
||||
keyboard_mapper.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
@ -2445,11 +2454,21 @@ impl KeybindingEditorModal {
|
|||
}
|
||||
}
|
||||
|
||||
fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
|
||||
Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
..Default::default()
|
||||
fn remove_key_char(
|
||||
KeybindingKeystroke {
|
||||
inner,
|
||||
display_modifiers,
|
||||
display_key,
|
||||
}: KeybindingKeystroke,
|
||||
) -> KeybindingKeystroke {
|
||||
KeybindingKeystroke {
|
||||
inner: Keystroke {
|
||||
modifiers: inner.modifiers,
|
||||
key: inner.key,
|
||||
key_char: None,
|
||||
},
|
||||
display_modifiers,
|
||||
display_key,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2992,6 +3011,7 @@ async fn save_keybinding_update(
|
|||
new_args: Option<&str>,
|
||||
fs: &Arc<dyn Fs>,
|
||||
tab_size: usize,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> anyhow::Result<()> {
|
||||
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
|
||||
.await
|
||||
|
@ -3034,9 +3054,13 @@ async fn save_keybinding_update(
|
|||
|
||||
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
||||
|
||||
let updated_keymap_contents =
|
||||
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
|
||||
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
|
||||
let updated_keymap_contents = settings::KeymapFile::update_keybinding(
|
||||
operation,
|
||||
keymap_contents,
|
||||
tab_size,
|
||||
keyboard_mapper,
|
||||
)
|
||||
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
|
||||
fs.write(
|
||||
paths::keymap_file().as_path(),
|
||||
updated_keymap_contents.as_bytes(),
|
||||
|
@ -3057,6 +3081,7 @@ async fn remove_keybinding(
|
|||
existing: ProcessedBinding,
|
||||
fs: &Arc<dyn Fs>,
|
||||
tab_size: usize,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(keystrokes) = existing.keystrokes() else {
|
||||
anyhow::bail!("Cannot remove a keybinding that does not exist");
|
||||
|
@ -3080,9 +3105,13 @@ async fn remove_keybinding(
|
|||
};
|
||||
|
||||
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
||||
let updated_keymap_contents =
|
||||
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
|
||||
.context("Failed to update keybinding")?;
|
||||
let updated_keymap_contents = settings::KeymapFile::update_keybinding(
|
||||
operation,
|
||||
keymap_contents,
|
||||
tab_size,
|
||||
keyboard_mapper,
|
||||
)
|
||||
.context("Failed to update keybinding")?;
|
||||
fs.write(
|
||||
paths::keymap_file().as_path(),
|
||||
updated_keymap_contents.as_bytes(),
|
||||
|
@ -3348,12 +3377,15 @@ impl SerializableItem for KeymapEditor {
|
|||
}
|
||||
|
||||
mod persistence {
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
use db::{query, sqlez::domain::Domain, sqlez_macros::sql};
|
||||
use workspace::WorkspaceDb;
|
||||
|
||||
define_connection! {
|
||||
pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
|
||||
&[sql!(
|
||||
pub struct KeybindingEditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl Domain for KeybindingEditorDb {
|
||||
const NAME: &str = stringify!(KeybindingEditorDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE keybinding_editors (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
|
@ -3362,9 +3394,11 @@ mod persistence {
|
|||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)];
|
||||
)];
|
||||
}
|
||||
|
||||
db::static_connection!(KEYBINDING_EDITORS, KeybindingEditorDb, [WorkspaceDb]);
|
||||
|
||||
impl KeybindingEditorDb {
|
||||
query! {
|
||||
pub async fn save_keybinding_editor(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use gpui::{
|
||||
Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
|
||||
Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
|
||||
KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
|
||||
};
|
||||
use ui::{
|
||||
ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
|
||||
|
@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult {
|
|||
}
|
||||
|
||||
pub struct KeystrokeInput {
|
||||
keystrokes: Vec<Keystroke>,
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
keystrokes: Vec<KeybindingKeystroke>,
|
||||
placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
|
||||
outer_focus_handle: FocusHandle,
|
||||
inner_focus_handle: FocusHandle,
|
||||
intercept_subscription: Option<Subscription>,
|
||||
|
@ -70,7 +70,7 @@ impl KeystrokeInput {
|
|||
const KEYSTROKE_COUNT_MAX: usize = 3;
|
||||
|
||||
pub fn new(
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<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_changed(cx);
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ impl KeystrokeInput {
|
|||
self.search = search;
|
||||
}
|
||||
|
||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
||||
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
||||
if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
||||
&& self.keystrokes.is_empty()
|
||||
{
|
||||
|
@ -116,18 +116,22 @@ impl KeystrokeInput {
|
|||
&& self
|
||||
.keystrokes
|
||||
.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];
|
||||
}
|
||||
&self.keystrokes
|
||||
}
|
||||
|
||||
fn dummy(modifiers: Modifiers) -> Keystroke {
|
||||
Keystroke {
|
||||
modifiers,
|
||||
key: "".to_string(),
|
||||
key_char: None,
|
||||
fn dummy(modifiers: Modifiers) -> KeybindingKeystroke {
|
||||
KeybindingKeystroke {
|
||||
inner: Keystroke {
|
||||
modifiers,
|
||||
key: "".to_string(),
|
||||
key_char: None,
|
||||
},
|
||||
display_modifiers: modifiers,
|
||||
display_key: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,7 +258,7 @@ impl KeystrokeInput {
|
|||
self.keystrokes_changed(cx);
|
||||
|
||||
if let Some(last) = self.keystrokes.last_mut()
|
||||
&& last.key.is_empty()
|
||||
&& last.display_key.is_empty()
|
||||
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
|
||||
{
|
||||
if !self.search && !event.modifiers.modified() {
|
||||
|
@ -263,13 +267,15 @@ impl KeystrokeInput {
|
|||
}
|
||||
if self.search {
|
||||
if self.previous_modifiers.modified() {
|
||||
last.modifiers |= event.modifiers;
|
||||
last.display_modifiers |= event.modifiers;
|
||||
last.inner.modifiers |= event.modifiers;
|
||||
} else {
|
||||
self.keystrokes.push(Self::dummy(event.modifiers));
|
||||
}
|
||||
self.previous_modifiers |= event.modifiers;
|
||||
} else {
|
||||
last.modifiers = event.modifiers;
|
||||
last.display_modifiers = event.modifiers;
|
||||
last.inner.modifiers = event.modifiers;
|
||||
return;
|
||||
}
|
||||
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
|
||||
|
@ -297,14 +303,17 @@ impl KeystrokeInput {
|
|||
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()
|
||||
&& last.key.is_empty()
|
||||
&& last.display_key.is_empty()
|
||||
&& (!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.key = key;
|
||||
keystroke.display_key = display_key;
|
||||
keystroke.inner.key = inner_key;
|
||||
self.keystrokes.pop();
|
||||
}
|
||||
|
||||
|
@ -324,11 +333,14 @@ impl KeystrokeInput {
|
|||
self.keystrokes_changed(cx);
|
||||
|
||||
if self.search {
|
||||
self.previous_modifiers = keystroke.modifiers;
|
||||
self.previous_modifiers = keystroke.display_modifiers;
|
||||
return;
|
||||
}
|
||||
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() {
|
||||
self.keystrokes.push(Self::dummy(keystroke.modifiers));
|
||||
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX
|
||||
&& keystroke.display_modifiers.modified()
|
||||
{
|
||||
self.keystrokes
|
||||
.push(Self::dummy(keystroke.display_modifiers));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -364,7 +376,7 @@ impl KeystrokeInput {
|
|||
&self.keystrokes
|
||||
};
|
||||
keystrokes.iter().map(move |keystroke| {
|
||||
h_flex().children(ui::render_keystroke(
|
||||
h_flex().children(ui::render_keybinding_keystroke(
|
||||
keystroke,
|
||||
Some(Color::Default),
|
||||
Some(rems(0.875).into()),
|
||||
|
@ -809,9 +821,13 @@ mod tests {
|
|||
/// Verifies that the keystrokes match the expected strings
|
||||
#[track_caller]
|
||||
pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
|
||||
let actual = self
|
||||
.input
|
||||
.read_with(&self.cx, |input, _| input.keystrokes.clone());
|
||||
let actual: Vec<Keystroke> = self.input.read_with(&self.cx, |input, _| {
|
||||
input
|
||||
.keystrokes
|
||||
.iter()
|
||||
.map(|keystroke| keystroke.inner.clone())
|
||||
.collect()
|
||||
});
|
||||
Self::expect_keystrokes_equal(&actual, expected);
|
||||
self
|
||||
}
|
||||
|
@ -939,7 +955,7 @@ mod tests {
|
|||
}
|
||||
|
||||
struct KeystrokeUpdateTracker {
|
||||
initial_keystrokes: Vec<Keystroke>,
|
||||
initial_keystrokes: Vec<KeybindingKeystroke>,
|
||||
_subscription: Subscription,
|
||||
input: Entity<KeystrokeInput>,
|
||||
received_keystrokes_updated: bool,
|
||||
|
@ -983,8 +999,8 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
fn keystrokes_str(ks: &[Keystroke]) -> String {
|
||||
ks.iter().map(|ks| ks.unparse()).join(" ")
|
||||
fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String {
|
||||
ks.iter().map(|ks| ks.inner.unparse()).join(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
use crate::connection::Connection;
|
||||
|
||||
pub trait Domain: 'static {
|
||||
fn name() -> &'static str;
|
||||
fn migrations() -> &'static [&'static str];
|
||||
const NAME: &str;
|
||||
const MIGRATIONS: &[&str];
|
||||
|
||||
fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Migrator: 'static {
|
||||
|
@ -17,7 +21,11 @@ impl Migrator for () {
|
|||
|
||||
impl<D: Domain> Migrator for D {
|
||||
fn migrate(connection: &Connection) -> anyhow::Result<()> {
|
||||
connection.migrate(Self::name(), Self::migrations())
|
||||
connection.migrate(
|
||||
Self::NAME,
|
||||
Self::MIGRATIONS,
|
||||
Self::should_allow_migration_change,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,12 @@ impl Connection {
|
|||
/// Note: Unlike everything else in SQLez, migrations are run eagerly, without first
|
||||
/// preparing the SQL statements. This makes it possible to do multi-statement schema
|
||||
/// updates in a single string without running into prepare errors.
|
||||
pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> {
|
||||
pub fn migrate(
|
||||
&self,
|
||||
domain: &'static str,
|
||||
migrations: &[&'static str],
|
||||
mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool,
|
||||
) -> Result<()> {
|
||||
self.with_savepoint("migrating", || {
|
||||
// Setup the migrations table unconditionally
|
||||
self.exec(indoc! {"
|
||||
|
@ -65,9 +70,14 @@ impl Connection {
|
|||
&sqlformat::QueryParams::None,
|
||||
Default::default(),
|
||||
);
|
||||
if completed_migration == migration {
|
||||
if completed_migration == migration
|
||||
|| migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE")
|
||||
{
|
||||
// Migration already run. Continue
|
||||
continue;
|
||||
} else if should_allow_migration_change(index, &completed_migration, &migration)
|
||||
{
|
||||
continue;
|
||||
} else {
|
||||
anyhow::bail!(formatdoc! {"
|
||||
Migration changed for {domain} at step {index}
|
||||
|
@ -108,6 +118,7 @@ mod test {
|
|||
a TEXT,
|
||||
b TEXT
|
||||
)"}],
|
||||
disallow_migration_change,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -136,6 +147,7 @@ mod test {
|
|||
d TEXT
|
||||
)"},
|
||||
],
|
||||
disallow_migration_change,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -214,7 +226,11 @@ mod test {
|
|||
|
||||
// Run the migration verifying that the row got dropped
|
||||
connection
|
||||
.migrate("test", &["DELETE FROM test_table"])
|
||||
.migrate(
|
||||
"test",
|
||||
&["DELETE FROM test_table"],
|
||||
disallow_migration_change,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
connection
|
||||
|
@ -232,7 +248,11 @@ mod test {
|
|||
|
||||
// Run the same migration again and verify that the table was left unchanged
|
||||
connection
|
||||
.migrate("test", &["DELETE FROM test_table"])
|
||||
.migrate(
|
||||
"test",
|
||||
&["DELETE FROM test_table"],
|
||||
disallow_migration_change,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
connection
|
||||
|
@ -252,27 +272,28 @@ mod test {
|
|||
.migrate(
|
||||
"test migration",
|
||||
&[
|
||||
indoc! {"
|
||||
CREATE TABLE test (
|
||||
col INTEGER
|
||||
)"},
|
||||
indoc! {"
|
||||
INSERT INTO test (col) VALUES (1)"},
|
||||
"CREATE TABLE test (col INTEGER)",
|
||||
"INSERT INTO test (col) VALUES (1)",
|
||||
],
|
||||
disallow_migration_change,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut migration_changed = false;
|
||||
|
||||
// Create another migration with the same domain but different steps
|
||||
let second_migration_result = connection.migrate(
|
||||
"test migration",
|
||||
&[
|
||||
indoc! {"
|
||||
CREATE TABLE test (
|
||||
color INTEGER
|
||||
)"},
|
||||
indoc! {"
|
||||
INSERT INTO test (color) VALUES (1)"},
|
||||
"CREATE TABLE test (color INTEGER )",
|
||||
"INSERT INTO test (color) VALUES (1)",
|
||||
],
|
||||
|_, old, new| {
|
||||
assert_eq!(old, "CREATE TABLE test (col INTEGER)");
|
||||
assert_eq!(new, "CREATE TABLE test (color INTEGER)");
|
||||
migration_changed = true;
|
||||
false
|
||||
},
|
||||
);
|
||||
|
||||
// Verify new migration returns error when run
|
||||
|
@ -284,7 +305,11 @@ mod test {
|
|||
let connection = Connection::open_memory(Some("test_create_alter_drop"));
|
||||
|
||||
connection
|
||||
.migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"])
|
||||
.migrate(
|
||||
"first_migration",
|
||||
&["CREATE TABLE table1(a TEXT) STRICT;"],
|
||||
disallow_migration_change,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
connection
|
||||
|
@ -305,6 +330,7 @@ mod test {
|
|||
|
||||
ALTER TABLE table2 RENAME TO table1;
|
||||
"}],
|
||||
disallow_migration_change,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -312,4 +338,8 @@ mod test {
|
|||
|
||||
assert_eq!(res, "test text");
|
||||
}
|
||||
|
||||
fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -278,12 +278,8 @@ mod test {
|
|||
|
||||
enum TestDomain {}
|
||||
impl Domain for TestDomain {
|
||||
fn name() -> &'static str {
|
||||
"test"
|
||||
}
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]
|
||||
}
|
||||
const NAME: &str = "test";
|
||||
const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"];
|
||||
}
|
||||
|
||||
for _ in 0..100 {
|
||||
|
@ -312,12 +308,9 @@ mod test {
|
|||
fn wild_zed_lost_failure() {
|
||||
enum TestWorkspace {}
|
||||
impl Domain for TestWorkspace {
|
||||
fn name() -> &'static str {
|
||||
"workspace"
|
||||
}
|
||||
const NAME: &str = "workspace";
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&["
|
||||
const MIGRATIONS: &[&str] = &["
|
||||
CREATE TABLE workspaces(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
dock_visible INTEGER, -- Boolean
|
||||
|
@ -336,8 +329,7 @@ mod test {
|
|||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) STRICT;
|
||||
"]
|
||||
}
|
||||
"];
|
||||
}
|
||||
|
||||
let builder =
|
||||
|
|
|
@ -9,7 +9,11 @@ use std::path::{Path, PathBuf};
|
|||
use ui::{App, Context, Pixels, Window};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use workspace::{
|
||||
ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace,
|
||||
WorkspaceDb, WorkspaceId,
|
||||
|
@ -375,9 +379,13 @@ impl<'de> Deserialize<'de> for SerializedAxis {
|
|||
}
|
||||
}
|
||||
|
||||
define_connection! {
|
||||
pub static ref TERMINAL_DB: TerminalDb<WorkspaceDb> =
|
||||
&[sql!(
|
||||
pub struct TerminalDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for TerminalDb {
|
||||
const NAME: &str = stringify!(TerminalDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql!(
|
||||
CREATE TABLE terminals (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
|
@ -414,6 +422,8 @@ define_connection! {
|
|||
];
|
||||
}
|
||||
|
||||
db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]);
|
||||
|
||||
impl TerminalDb {
|
||||
query! {
|
||||
pub async fn update_workspace_id(
|
||||
|
|
|
@ -119,7 +119,7 @@ impl Render for OnboardingBanner {
|
|||
h_flex()
|
||||
.h_full()
|
||||
.gap_1()
|
||||
.child(Icon::new(self.details.icon_name).size(IconSize::Small))
|
||||
.child(Icon::new(self.details.icon_name).size(IconSize::XSmall))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
|
|
|
@ -275,11 +275,11 @@ impl TitleBar {
|
|||
|
||||
let banner = cx.new(|cx| {
|
||||
OnboardingBanner::new(
|
||||
"Debugger Onboarding",
|
||||
IconName::Debug,
|
||||
"The Debugger",
|
||||
None,
|
||||
zed_actions::debugger::OpenOnboardingModal.boxed_clone(),
|
||||
"ACP Onboarding",
|
||||
IconName::Sparkle,
|
||||
"Bring Your Own Agent",
|
||||
Some("Introducing:".into()),
|
||||
zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
|
@ -13,6 +13,9 @@ use crate::prelude::*;
|
|||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum VectorName {
|
||||
AcpGrid,
|
||||
AcpLogo,
|
||||
AcpLogoSerif,
|
||||
AiGrid,
|
||||
DebuggerGrid,
|
||||
Grid,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::PlatformStyle;
|
||||
use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window,
|
||||
relative,
|
||||
Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke,
|
||||
Modifiers, Window, relative,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
|
||||
|
@ -13,7 +13,7 @@ pub struct KeyBinding {
|
|||
/// More than one keystroke produces a chord.
|
||||
///
|
||||
/// This should always contain at least one keystroke.
|
||||
pub keystrokes: Vec<Keystroke>,
|
||||
pub keystrokes: Vec<KeybindingKeystroke>,
|
||||
|
||||
/// The [`PlatformStyle`] to use when displaying this keybinding.
|
||||
platform_style: PlatformStyle,
|
||||
|
@ -59,7 +59,7 @@ impl KeyBinding {
|
|||
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 {
|
||||
keystrokes,
|
||||
platform_style: PlatformStyle::platform(),
|
||||
|
@ -99,16 +99,16 @@ impl KeyBinding {
|
|||
}
|
||||
|
||||
fn render_key(
|
||||
keystroke: &Keystroke,
|
||||
key: &str,
|
||||
color: Option<Color>,
|
||||
platform_style: PlatformStyle,
|
||||
size: impl Into<Option<AbsoluteLength>>,
|
||||
) -> AnyElement {
|
||||
let key_icon = icon_for_key(keystroke, platform_style);
|
||||
let key_icon = icon_for_key(key, platform_style);
|
||||
match key_icon {
|
||||
Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
|
||||
None => {
|
||||
let key = util::capitalize(&keystroke.key);
|
||||
let key = util::capitalize(key);
|
||||
Key::new(&key, color).size(size).into_any_element()
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding {
|
|||
"KEY_BINDING-{}",
|
||||
self.keystrokes
|
||||
.iter()
|
||||
.map(|k| k.key.to_string())
|
||||
.map(|k| k.display_key.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
)
|
||||
|
@ -137,7 +137,7 @@ impl RenderOnce for KeyBinding {
|
|||
.py_0p5()
|
||||
.rounded_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.children(render_keystroke(
|
||||
.children(render_keybinding_keystroke(
|
||||
keystroke,
|
||||
color,
|
||||
self.size,
|
||||
|
@ -148,8 +148,8 @@ impl RenderOnce for KeyBinding {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn render_keystroke(
|
||||
keystroke: &Keystroke,
|
||||
pub fn render_keybinding_keystroke(
|
||||
keystroke: &KeybindingKeystroke,
|
||||
color: Option<Color>,
|
||||
size: impl Into<Option<AbsoluteLength>>,
|
||||
platform_style: PlatformStyle,
|
||||
|
@ -163,26 +163,39 @@ pub fn render_keystroke(
|
|||
let size = size.into();
|
||||
|
||||
if use_text {
|
||||
let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color)
|
||||
.size(size)
|
||||
.into_any_element();
|
||||
let element = Key::new(
|
||||
keystroke_text(
|
||||
&keystroke.display_modifiers,
|
||||
&keystroke.display_key,
|
||||
platform_style,
|
||||
vim_mode,
|
||||
),
|
||||
color,
|
||||
)
|
||||
.size(size)
|
||||
.into_any_element();
|
||||
vec![element]
|
||||
} else {
|
||||
let mut elements = Vec::new();
|
||||
elements.extend(render_modifiers(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.display_modifiers,
|
||||
platform_style,
|
||||
color,
|
||||
size,
|
||||
true,
|
||||
));
|
||||
elements.push(render_key(keystroke, color, platform_style, size));
|
||||
elements.push(render_key(
|
||||
&keystroke.display_key,
|
||||
color,
|
||||
platform_style,
|
||||
size,
|
||||
));
|
||||
elements
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
|
||||
match keystroke.key.as_str() {
|
||||
fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option<IconName> {
|
||||
match key {
|
||||
"left" => Some(IconName::ArrowLeft),
|
||||
"right" => Some(IconName::ArrowRight),
|
||||
"up" => Some(IconName::ArrowUp),
|
||||
|
@ -379,7 +392,7 @@ impl KeyIcon {
|
|||
/// Returns a textual representation of the key binding for the given [`Action`].
|
||||
pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
|
||||
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 {
|
||||
|
@ -387,22 +400,50 @@ pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
|
|||
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
||||
keystrokes
|
||||
.iter()
|
||||
.map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled))
|
||||
.map(|keystroke| {
|
||||
keystroke_text(
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
platform_style,
|
||||
vim_enabled,
|
||||
)
|
||||
})
|
||||
.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 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`].
|
||||
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 delimiter = '-';
|
||||
|
||||
if keystroke.modifiers.function {
|
||||
if modifiers.function {
|
||||
match vim_mode {
|
||||
false => 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);
|
||||
}
|
||||
|
||||
if keystroke.modifiers.control {
|
||||
if modifiers.control {
|
||||
match (platform_style, vim_mode) {
|
||||
(PlatformStyle::Mac, false) => text.push_str("Control"),
|
||||
(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);
|
||||
}
|
||||
|
||||
if keystroke.modifiers.platform {
|
||||
if modifiers.platform {
|
||||
match (platform_style, vim_mode) {
|
||||
(PlatformStyle::Mac, false) => text.push_str("Command"),
|
||||
(PlatformStyle::Mac, true) => text.push_str("cmd"),
|
||||
|
@ -434,7 +475,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
|||
text.push(delimiter);
|
||||
}
|
||||
|
||||
if keystroke.modifiers.alt {
|
||||
if modifiers.alt {
|
||||
match (platform_style, vim_mode) {
|
||||
(PlatformStyle::Mac, false) => text.push_str("Option"),
|
||||
(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);
|
||||
}
|
||||
|
||||
if keystroke.modifiers.shift {
|
||||
if modifiers.shift {
|
||||
match (platform_style, vim_mode) {
|
||||
(_, false) => 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 {
|
||||
text.push_str(&keystroke.key)
|
||||
text.push_str(key)
|
||||
} else {
|
||||
let key = match keystroke.key.as_str() {
|
||||
let key = match key {
|
||||
"pageup" => "PageUp",
|
||||
"pagedown" => "PageDown",
|
||||
key => &util::capitalize(key),
|
||||
|
@ -562,9 +603,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_text_for_keystroke() {
|
||||
let keystroke = Keystroke::parse("cmd-c").unwrap();
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&Keystroke::parse("cmd-c").unwrap(),
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
PlatformStyle::Mac,
|
||||
false
|
||||
),
|
||||
|
@ -572,7 +615,8 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&Keystroke::parse("cmd-c").unwrap(),
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
PlatformStyle::Linux,
|
||||
false
|
||||
),
|
||||
|
@ -580,16 +624,19 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&Keystroke::parse("cmd-c").unwrap(),
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
PlatformStyle::Windows,
|
||||
false
|
||||
),
|
||||
"Win-C".to_string()
|
||||
);
|
||||
|
||||
let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap();
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
PlatformStyle::Mac,
|
||||
false
|
||||
),
|
||||
|
@ -597,7 +644,8 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
PlatformStyle::Linux,
|
||||
false
|
||||
),
|
||||
|
@ -605,16 +653,19 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
PlatformStyle::Windows,
|
||||
false
|
||||
),
|
||||
"Ctrl-Alt-Delete".to_string()
|
||||
);
|
||||
|
||||
let keystroke = Keystroke::parse("shift-pageup").unwrap();
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&Keystroke::parse("shift-pageup").unwrap(),
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
PlatformStyle::Mac,
|
||||
false
|
||||
),
|
||||
|
@ -622,7 +673,8 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&Keystroke::parse("shift-pageup").unwrap(),
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
PlatformStyle::Linux,
|
||||
false,
|
||||
),
|
||||
|
@ -630,7 +682,8 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
keystroke_text(
|
||||
&Keystroke::parse("shift-pageup").unwrap(),
|
||||
&keystroke.modifiers,
|
||||
&keystroke.key,
|
||||
PlatformStyle::Windows,
|
||||
false
|
||||
),
|
||||
|
|
|
@ -23,6 +23,8 @@ actions!(
|
|||
HelixInsert,
|
||||
/// Appends at the end of the selection.
|
||||
HelixAppend,
|
||||
/// Goes to the location of the last modification.
|
||||
HelixGotoLastModification,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -31,6 +33,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|||
Vim::action(editor, cx, Vim::helix_insert);
|
||||
Vim::action(editor, cx, Vim::helix_append);
|
||||
Vim::action(editor, cx, Vim::helix_yank);
|
||||
Vim::action(editor, cx, Vim::helix_goto_last_modification);
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
|
@ -430,6 +433,15 @@ impl Vim {
|
|||
});
|
||||
self.switch_mode(Mode::HelixNormal, true, window, cx);
|
||||
}
|
||||
|
||||
pub fn helix_goto_last_modification(
|
||||
&mut self,
|
||||
_: &HelixGotoLastModification,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.jump(".".into(), false, false, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -441,6 +453,7 @@ mod test {
|
|||
#[gpui::test]
|
||||
async fn test_word_motions(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
// «
|
||||
// ˇ
|
||||
// »
|
||||
|
@ -502,6 +515,7 @@ mod test {
|
|||
#[gpui::test]
|
||||
async fn test_delete(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// test delete a selection
|
||||
cx.set_state(
|
||||
|
@ -582,6 +596,7 @@ mod test {
|
|||
#[gpui::test]
|
||||
async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
|
@ -635,6 +650,7 @@ mod test {
|
|||
#[gpui::test]
|
||||
async fn test_newline_char(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
|
||||
|
||||
|
@ -652,6 +668,7 @@ mod test {
|
|||
#[gpui::test]
|
||||
async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
«The ˇ»quick brown
|
||||
|
@ -674,6 +691,7 @@ mod test {
|
|||
#[gpui::test]
|
||||
async fn test_append(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
// test from the end of the selection
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
|
@ -716,6 +734,7 @@ mod test {
|
|||
#[gpui::test]
|
||||
async fn test_replace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// No selection (single character)
|
||||
cx.set_state("ˇaa", Mode::HelixNormal);
|
||||
|
@ -763,4 +782,72 @@ mod test {
|
|||
cx.shared_clipboard().assert_eq("worl");
|
||||
cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// First copy some text to clipboard
|
||||
cx.set_state("«hello worldˇ»", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("y");
|
||||
|
||||
// Test paste with shift-r on single cursor
|
||||
cx.set_state("foo ˇbar", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("shift-r");
|
||||
|
||||
cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
|
||||
|
||||
// Test paste with shift-r on selection
|
||||
cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("shift-r");
|
||||
|
||||
cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// Make a modification at a specific location
|
||||
cx.set_state("ˇhello", Mode::HelixNormal);
|
||||
assert_eq!(cx.mode(), Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.simulate_keystrokes("escape");
|
||||
assert_eq!(cx.mode(), Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// Make a modification at a specific location
|
||||
cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
|
||||
cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("i");
|
||||
cx.simulate_keystrokes("escape");
|
||||
cx.simulate_keystrokes("i");
|
||||
cx.simulate_keystrokes("m o d i f i e d space");
|
||||
cx.simulate_keystrokes("escape");
|
||||
|
||||
// TODO: this fails, because state is no longer helix
|
||||
cx.assert_state(
|
||||
"line one\nline modified ˇtwo\nline three",
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// Move cursor away from the modification
|
||||
cx.simulate_keystrokes("up");
|
||||
|
||||
// Use "g ." to go back to last modification
|
||||
cx.simulate_keystrokes("g .");
|
||||
|
||||
// Verify we're back at the modification location and still in HelixNormal mode
|
||||
cx.assert_state(
|
||||
"line one\nline modifiedˇ two\nline three",
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -203,7 +203,10 @@ impl Vim {
|
|||
|
||||
// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
|
||||
fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) {
|
||||
// Preserve the current mode when resetting search state
|
||||
let current_mode = self.mode;
|
||||
self.search = Default::default();
|
||||
self.search.prior_mode = current_mode;
|
||||
cx.propagate();
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,10 @@ use crate::{motion::Motion, object::Object};
|
|||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
|
||||
use db::define_connection;
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use editor::display_map::{is_invisible, replacement};
|
||||
use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint};
|
||||
use gpui::{
|
||||
|
@ -1668,8 +1670,12 @@ impl MarksView {
|
|||
}
|
||||
}
|
||||
|
||||
define_connection! (
|
||||
pub static ref DB: VimDb<WorkspaceDb> = &[
|
||||
pub struct VimDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for VimDb {
|
||||
const NAME: &str = stringify!(VimDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql! (
|
||||
CREATE TABLE vim_marks (
|
||||
workspace_id INTEGER,
|
||||
|
@ -1689,7 +1695,9 @@ define_connection! (
|
|||
ON vim_global_marks_paths(workspace_id, mark_name);
|
||||
),
|
||||
];
|
||||
);
|
||||
}
|
||||
|
||||
db::static_connection!(DB, VimDb, [WorkspaceDb]);
|
||||
|
||||
struct SerializedMark {
|
||||
path: Arc<Path>,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use gpui::{EventEmitter, FocusHandle, Focusable};
|
||||
use ui::{
|
||||
|
@ -12,7 +12,7 @@ use crate::Item;
|
|||
/// A view to display when a certain buffer fails to open.
|
||||
pub struct InvalidBufferView {
|
||||
/// Which path was attempted to open.
|
||||
pub abs_path: Arc<PathBuf>,
|
||||
pub abs_path: Arc<Path>,
|
||||
/// An error message, happened when opening the buffer.
|
||||
pub error: SharedString,
|
||||
is_local: bool,
|
||||
|
@ -21,7 +21,7 @@ pub struct InvalidBufferView {
|
|||
|
||||
impl InvalidBufferView {
|
||||
pub fn new(
|
||||
abs_path: PathBuf,
|
||||
abs_path: &Path,
|
||||
is_local: bool,
|
||||
e: &anyhow::Error,
|
||||
_: &mut Window,
|
||||
|
@ -29,7 +29,7 @@ impl InvalidBufferView {
|
|||
) -> Self {
|
||||
Self {
|
||||
is_local,
|
||||
abs_path: Arc::new(abs_path),
|
||||
abs_path: Arc::from(abs_path),
|
||||
error: format!("{e}").into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ impl Item for InvalidBufferView {
|
|||
// Ensure we always render at least the filename.
|
||||
detail += 1;
|
||||
|
||||
let path = self.abs_path.as_path();
|
||||
let path = self.abs_path.as_ref();
|
||||
|
||||
let mut prefix = path;
|
||||
while detail > 0 {
|
||||
|
|
|
@ -23,7 +23,7 @@ use std::{
|
|||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
|
@ -1168,7 +1168,7 @@ pub trait ProjectItem: Item {
|
|||
/// with the error from that failure as an argument.
|
||||
/// Allows to open an item that can gracefully display and handle errors.
|
||||
fn for_broken_project_item(
|
||||
_abs_path: PathBuf,
|
||||
_abs_path: &Path,
|
||||
_is_local: bool,
|
||||
_e: &anyhow::Error,
|
||||
_window: &mut Window,
|
||||
|
|
|
@ -58,11 +58,7 @@ impl PathList {
|
|||
let mut paths: Vec<PathBuf> = if serialized.paths.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
serde_json::from_str::<Vec<PathBuf>>(&serialized.paths)
|
||||
.unwrap_or(Vec::new())
|
||||
.into_iter()
|
||||
.map(|s| SanitizedPath::from(s).into())
|
||||
.collect()
|
||||
serialized.paths.split('\n').map(PathBuf::from).collect()
|
||||
};
|
||||
|
||||
let mut order: Vec<usize> = serialized
|
||||
|
@ -85,7 +81,13 @@ impl PathList {
|
|||
pub fn serialize(&self) -> SerializedPathList {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
let paths = serde_json::to_string(&self.paths).unwrap_or_default();
|
||||
let mut paths = String::new();
|
||||
for path in self.paths.iter() {
|
||||
if !paths.is_empty() {
|
||||
paths.push('\n');
|
||||
}
|
||||
paths.push_str(&path.to_string_lossy());
|
||||
}
|
||||
|
||||
let mut order = String::new();
|
||||
for ix in self.order.iter() {
|
||||
|
|
|
@ -10,7 +10,11 @@ use std::{
|
|||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use collections::HashMap;
|
||||
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{connection::Connection, domain::Domain},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
|
||||
use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
|
||||
|
||||
|
@ -275,186 +279,189 @@ impl sqlez::bindable::Bind for SerializedPixels {
|
|||
}
|
||||
}
|
||||
|
||||
define_connection! {
|
||||
pub static ref DB: WorkspaceDb<()> =
|
||||
&[
|
||||
sql!(
|
||||
CREATE TABLE workspaces(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
workspace_location BLOB UNIQUE,
|
||||
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
left_sidebar_open INTEGER, // Boolean
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
|
||||
) STRICT;
|
||||
pub struct WorkspaceDb(ThreadSafeConnection);
|
||||
|
||||
CREATE TABLE pane_groups(
|
||||
group_id INTEGER PRIMARY KEY,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
parent_group_id INTEGER, // NULL indicates that this is a root node
|
||||
position INTEGER, // NULL indicates that this is a root node
|
||||
axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
impl Domain for WorkspaceDb {
|
||||
const NAME: &str = stringify!(WorkspaceDb);
|
||||
|
||||
CREATE TABLE panes(
|
||||
pane_id INTEGER PRIMARY KEY,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
active INTEGER NOT NULL, // Boolean
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) STRICT;
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql!(
|
||||
CREATE TABLE workspaces(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
workspace_location BLOB UNIQUE,
|
||||
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
left_sidebar_open INTEGER, // Boolean
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE center_panes(
|
||||
pane_id INTEGER PRIMARY KEY,
|
||||
parent_group_id INTEGER, // NULL means that this is a root pane
|
||||
position INTEGER, // NULL means that this is a root pane
|
||||
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
CREATE TABLE pane_groups(
|
||||
group_id INTEGER PRIMARY KEY,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
parent_group_id INTEGER, // NULL indicates that this is a root node
|
||||
position INTEGER, // NULL indicates that this is a root node
|
||||
axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE items(
|
||||
item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
|
||||
workspace_id INTEGER NOT NULL,
|
||||
pane_id INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
active INTEGER NOT NULL,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
|
||||
ON DELETE CASCADE,
|
||||
PRIMARY KEY(item_id, workspace_id)
|
||||
) STRICT;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN window_state TEXT;
|
||||
ALTER TABLE workspaces ADD COLUMN window_x REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN window_y REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN window_width REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN window_height REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN display BLOB;
|
||||
),
|
||||
// Drop foreign key constraint from workspaces.dock_pane to panes table.
|
||||
sql!(
|
||||
CREATE TABLE workspaces_2(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
workspace_location BLOB UNIQUE,
|
||||
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
left_sidebar_open INTEGER, // Boolean
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
window_state TEXT,
|
||||
window_x REAL,
|
||||
window_y REAL,
|
||||
window_width REAL,
|
||||
window_height REAL,
|
||||
display BLOB
|
||||
) STRICT;
|
||||
INSERT INTO workspaces_2 SELECT * FROM workspaces;
|
||||
DROP TABLE workspaces;
|
||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||
),
|
||||
// Add panels related information
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
|
||||
ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
|
||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
|
||||
),
|
||||
// Add panel zoom persistence
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
|
||||
),
|
||||
// Add pane group flex data
|
||||
sql!(
|
||||
ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
|
||||
),
|
||||
// Add fullscreen field to workspace
|
||||
// Deprecated, `WindowBounds` holds the fullscreen state now.
|
||||
// Preserving so users can downgrade Zed.
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
|
||||
),
|
||||
// Add preview field to items
|
||||
sql!(
|
||||
ALTER TABLE items ADD COLUMN preview INTEGER; //bool
|
||||
),
|
||||
// Add centered_layout field to workspace
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE remote_projects (
|
||||
remote_project_id INTEGER NOT NULL UNIQUE,
|
||||
path TEXT,
|
||||
dev_server_name TEXT
|
||||
);
|
||||
ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
|
||||
ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
|
||||
),
|
||||
sql!(
|
||||
DROP TABLE remote_projects;
|
||||
CREATE TABLE dev_server_projects (
|
||||
id INTEGER NOT NULL UNIQUE,
|
||||
path TEXT,
|
||||
dev_server_name TEXT
|
||||
);
|
||||
ALTER TABLE workspaces DROP COLUMN remote_project_id;
|
||||
ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE ssh_projects (
|
||||
id INTEGER PRIMARY KEY,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER,
|
||||
path TEXT NOT NULL,
|
||||
user TEXT
|
||||
);
|
||||
ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE toolchains (
|
||||
workspace_id INTEGER,
|
||||
worktree_id INTEGER,
|
||||
language_name TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, worktree_id, language_name)
|
||||
);
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE panes(
|
||||
pane_id INTEGER PRIMARY KEY,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
active INTEGER NOT NULL, // Boolean
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE center_panes(
|
||||
pane_id INTEGER PRIMARY KEY,
|
||||
parent_group_id INTEGER, // NULL means that this is a root pane
|
||||
position INTEGER, // NULL means that this is a root pane
|
||||
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE items(
|
||||
item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
|
||||
workspace_id INTEGER NOT NULL,
|
||||
pane_id INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
active INTEGER NOT NULL,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
|
||||
ON DELETE CASCADE,
|
||||
PRIMARY KEY(item_id, workspace_id)
|
||||
) STRICT;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN window_state TEXT;
|
||||
ALTER TABLE workspaces ADD COLUMN window_x REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN window_y REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN window_width REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN window_height REAL;
|
||||
ALTER TABLE workspaces ADD COLUMN display BLOB;
|
||||
),
|
||||
// Drop foreign key constraint from workspaces.dock_pane to panes table.
|
||||
sql!(
|
||||
CREATE TABLE workspaces_2(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
workspace_location BLOB UNIQUE,
|
||||
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
|
||||
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||
left_sidebar_open INTEGER, // Boolean
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
window_state TEXT,
|
||||
window_x REAL,
|
||||
window_y REAL,
|
||||
window_width REAL,
|
||||
window_height REAL,
|
||||
display BLOB
|
||||
) STRICT;
|
||||
INSERT INTO workspaces_2 SELECT * FROM workspaces;
|
||||
DROP TABLE workspaces;
|
||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||
),
|
||||
// Add panels related information
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
|
||||
ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
|
||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
|
||||
),
|
||||
// Add panel zoom persistence
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
|
||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
|
||||
),
|
||||
// Add pane group flex data
|
||||
sql!(
|
||||
ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
|
||||
),
|
||||
// Add fullscreen field to workspace
|
||||
// Deprecated, `WindowBounds` holds the fullscreen state now.
|
||||
// Preserving so users can downgrade Zed.
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
|
||||
),
|
||||
// Add preview field to items
|
||||
sql!(
|
||||
ALTER TABLE items ADD COLUMN preview INTEGER; //bool
|
||||
),
|
||||
// Add centered_layout field to workspace
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE remote_projects (
|
||||
remote_project_id INTEGER NOT NULL UNIQUE,
|
||||
path TEXT,
|
||||
dev_server_name TEXT
|
||||
);
|
||||
ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
|
||||
ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
|
||||
),
|
||||
sql!(
|
||||
DROP TABLE remote_projects;
|
||||
CREATE TABLE dev_server_projects (
|
||||
id INTEGER NOT NULL UNIQUE,
|
||||
path TEXT,
|
||||
dev_server_name TEXT
|
||||
);
|
||||
ALTER TABLE workspaces DROP COLUMN remote_project_id;
|
||||
ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE ssh_projects (
|
||||
id INTEGER PRIMARY KEY,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER,
|
||||
path TEXT NOT NULL,
|
||||
user TEXT
|
||||
);
|
||||
ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE toolchains (
|
||||
workspace_id INTEGER,
|
||||
worktree_id INTEGER,
|
||||
language_name TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, worktree_id, language_name)
|
||||
);
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE breakpoints (
|
||||
workspace_id INTEGER NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
|
@ -466,141 +473,172 @@ define_connection! {
|
|||
ON UPDATE CASCADE
|
||||
);
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
|
||||
CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
|
||||
ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE breakpoints DROP COLUMN kind
|
||||
),
|
||||
sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
|
||||
sql!(
|
||||
ALTER TABLE breakpoints ADD COLUMN condition TEXT;
|
||||
ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
|
||||
),
|
||||
sql!(CREATE TABLE toolchains2 (
|
||||
workspace_id INTEGER,
|
||||
worktree_id INTEGER,
|
||||
language_name TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
raw_json TEXT NOT NULL,
|
||||
relative_worktree_path TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
|
||||
INSERT INTO toolchains2
|
||||
SELECT * FROM toolchains;
|
||||
DROP TABLE toolchains;
|
||||
ALTER TABLE toolchains2 RENAME TO toolchains;
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE ssh_connections (
|
||||
id INTEGER PRIMARY KEY,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER,
|
||||
user TEXT
|
||||
);
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
|
||||
CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
|
||||
ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE breakpoints DROP COLUMN kind
|
||||
),
|
||||
sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
|
||||
sql!(
|
||||
ALTER TABLE breakpoints ADD COLUMN condition TEXT;
|
||||
ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
|
||||
),
|
||||
sql!(CREATE TABLE toolchains2 (
|
||||
workspace_id INTEGER,
|
||||
worktree_id INTEGER,
|
||||
language_name TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
raw_json TEXT NOT NULL,
|
||||
relative_worktree_path TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
|
||||
INSERT INTO toolchains2
|
||||
SELECT * FROM toolchains;
|
||||
DROP TABLE toolchains;
|
||||
ALTER TABLE toolchains2 RENAME TO toolchains;
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE ssh_connections (
|
||||
id INTEGER PRIMARY KEY,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER,
|
||||
user TEXT
|
||||
);
|
||||
|
||||
INSERT INTO ssh_connections (host, port, user)
|
||||
SELECT DISTINCT host, port, user
|
||||
FROM ssh_projects;
|
||||
INSERT INTO ssh_connections (host, port, user)
|
||||
SELECT DISTINCT host, port, user
|
||||
FROM ssh_projects;
|
||||
|
||||
CREATE TABLE workspaces_2(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
paths TEXT,
|
||||
paths_order TEXT,
|
||||
ssh_connection_id INTEGER REFERENCES ssh_connections(id),
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
window_state TEXT,
|
||||
window_x REAL,
|
||||
window_y REAL,
|
||||
window_width REAL,
|
||||
window_height REAL,
|
||||
display BLOB,
|
||||
left_dock_visible INTEGER,
|
||||
left_dock_active_panel TEXT,
|
||||
right_dock_visible INTEGER,
|
||||
right_dock_active_panel TEXT,
|
||||
bottom_dock_visible INTEGER,
|
||||
bottom_dock_active_panel TEXT,
|
||||
left_dock_zoom INTEGER,
|
||||
right_dock_zoom INTEGER,
|
||||
bottom_dock_zoom INTEGER,
|
||||
fullscreen INTEGER,
|
||||
centered_layout INTEGER,
|
||||
session_id TEXT,
|
||||
window_id INTEGER
|
||||
) STRICT;
|
||||
CREATE TABLE workspaces_2(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
paths TEXT,
|
||||
paths_order TEXT,
|
||||
ssh_connection_id INTEGER REFERENCES ssh_connections(id),
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
window_state TEXT,
|
||||
window_x REAL,
|
||||
window_y REAL,
|
||||
window_width REAL,
|
||||
window_height REAL,
|
||||
display BLOB,
|
||||
left_dock_visible INTEGER,
|
||||
left_dock_active_panel TEXT,
|
||||
right_dock_visible INTEGER,
|
||||
right_dock_active_panel TEXT,
|
||||
bottom_dock_visible INTEGER,
|
||||
bottom_dock_active_panel TEXT,
|
||||
left_dock_zoom INTEGER,
|
||||
right_dock_zoom INTEGER,
|
||||
bottom_dock_zoom INTEGER,
|
||||
fullscreen INTEGER,
|
||||
centered_layout INTEGER,
|
||||
session_id TEXT,
|
||||
window_id INTEGER
|
||||
) STRICT;
|
||||
|
||||
INSERT
|
||||
INTO workspaces_2
|
||||
SELECT
|
||||
workspaces.workspace_id,
|
||||
CASE
|
||||
WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
|
||||
INSERT
|
||||
INTO workspaces_2
|
||||
SELECT
|
||||
workspaces.workspace_id,
|
||||
CASE
|
||||
WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
|
||||
ELSE
|
||||
CASE
|
||||
WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
|
||||
NULL
|
||||
ELSE
|
||||
replace(workspaces.local_paths_array, ',', CHAR(10))
|
||||
END
|
||||
END as paths,
|
||||
|
||||
CASE
|
||||
WHEN ssh_projects.id IS NOT NULL THEN ""
|
||||
ELSE workspaces.local_paths_order_array
|
||||
END as paths_order,
|
||||
|
||||
CASE
|
||||
WHEN ssh_projects.id IS NOT NULL THEN (
|
||||
SELECT ssh_connections.id
|
||||
FROM ssh_connections
|
||||
WHERE
|
||||
ssh_connections.host IS ssh_projects.host AND
|
||||
ssh_connections.port IS ssh_projects.port AND
|
||||
ssh_connections.user IS ssh_projects.user
|
||||
)
|
||||
ELSE NULL
|
||||
END as ssh_connection_id,
|
||||
|
||||
workspaces.timestamp,
|
||||
workspaces.window_state,
|
||||
workspaces.window_x,
|
||||
workspaces.window_y,
|
||||
workspaces.window_width,
|
||||
workspaces.window_height,
|
||||
workspaces.display,
|
||||
workspaces.left_dock_visible,
|
||||
workspaces.left_dock_active_panel,
|
||||
workspaces.right_dock_visible,
|
||||
workspaces.right_dock_active_panel,
|
||||
workspaces.bottom_dock_visible,
|
||||
workspaces.bottom_dock_active_panel,
|
||||
workspaces.left_dock_zoom,
|
||||
workspaces.right_dock_zoom,
|
||||
workspaces.bottom_dock_zoom,
|
||||
workspaces.fullscreen,
|
||||
workspaces.centered_layout,
|
||||
workspaces.session_id,
|
||||
workspaces.window_id
|
||||
FROM
|
||||
workspaces LEFT JOIN
|
||||
ssh_projects ON
|
||||
workspaces.ssh_project_id = ssh_projects.id;
|
||||
|
||||
DELETE FROM workspaces_2
|
||||
WHERE workspace_id NOT IN (
|
||||
SELECT MAX(workspace_id)
|
||||
FROM workspaces_2
|
||||
GROUP BY ssh_connection_id, paths
|
||||
);
|
||||
|
||||
DROP TABLE ssh_projects;
|
||||
DROP TABLE workspaces;
|
||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||
|
||||
CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
|
||||
),
|
||||
// Fix any data from when workspaces.paths were briefly encoded as JSON arrays
|
||||
sql!(
|
||||
UPDATE workspaces
|
||||
SET paths = CASE
|
||||
WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN
|
||||
replace(
|
||||
substr(paths, 3, length(paths) - 4),
|
||||
'"' || ',' || '"',
|
||||
CHAR(10)
|
||||
)
|
||||
ELSE
|
||||
CASE
|
||||
WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
|
||||
NULL
|
||||
ELSE
|
||||
json('[' || '"' || replace(workspaces.local_paths_array, ',', '"' || "," || '"') || '"' || ']')
|
||||
END
|
||||
END as paths,
|
||||
|
||||
CASE
|
||||
WHEN ssh_projects.id IS NOT NULL THEN ""
|
||||
ELSE workspaces.local_paths_order_array
|
||||
END as paths_order,
|
||||
|
||||
CASE
|
||||
WHEN ssh_projects.id IS NOT NULL THEN (
|
||||
SELECT ssh_connections.id
|
||||
FROM ssh_connections
|
||||
WHERE
|
||||
ssh_connections.host IS ssh_projects.host AND
|
||||
ssh_connections.port IS ssh_projects.port AND
|
||||
ssh_connections.user IS ssh_projects.user
|
||||
)
|
||||
ELSE NULL
|
||||
END as ssh_connection_id,
|
||||
|
||||
workspaces.timestamp,
|
||||
workspaces.window_state,
|
||||
workspaces.window_x,
|
||||
workspaces.window_y,
|
||||
workspaces.window_width,
|
||||
workspaces.window_height,
|
||||
workspaces.display,
|
||||
workspaces.left_dock_visible,
|
||||
workspaces.left_dock_active_panel,
|
||||
workspaces.right_dock_visible,
|
||||
workspaces.right_dock_active_panel,
|
||||
workspaces.bottom_dock_visible,
|
||||
workspaces.bottom_dock_active_panel,
|
||||
workspaces.left_dock_zoom,
|
||||
workspaces.right_dock_zoom,
|
||||
workspaces.bottom_dock_zoom,
|
||||
workspaces.fullscreen,
|
||||
workspaces.centered_layout,
|
||||
workspaces.session_id,
|
||||
workspaces.window_id
|
||||
FROM
|
||||
workspaces LEFT JOIN
|
||||
ssh_projects ON
|
||||
workspaces.ssh_project_id = ssh_projects.id;
|
||||
|
||||
DROP TABLE ssh_projects;
|
||||
DROP TABLE workspaces;
|
||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||
|
||||
CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
|
||||
),
|
||||
replace(paths, ',', CHAR(10))
|
||||
END
|
||||
WHERE paths IS NOT NULL
|
||||
),
|
||||
];
|
||||
|
||||
// Allow recovering from bad migration that was initially shipped to nightly
|
||||
// when introducing the ssh_connections table.
|
||||
fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool {
|
||||
old.starts_with("CREATE TABLE ssh_connections")
|
||||
&& new.starts_with("CREATE TABLE ssh_connections")
|
||||
}
|
||||
}
|
||||
|
||||
db::static_connection!(DB, WorkspaceDb, []);
|
||||
|
||||
impl WorkspaceDb {
|
||||
/// Returns a serialized workspace for the given worktree_roots. If the passed array
|
||||
/// is empty, the most recent workspace is returned instead. If no workspace for the
|
||||
|
@ -1803,6 +1841,7 @@ mod tests {
|
|||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)],
|
||||
|_, _, _| false,
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
|
@ -1851,6 +1890,7 @@ mod tests {
|
|||
REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;)],
|
||||
|_, _, _| false,
|
||||
)
|
||||
})
|
||||
.await
|
||||
|
|
|
@ -613,48 +613,59 @@ impl ProjectItemRegistry {
|
|||
self.build_project_item_for_path_fns
|
||||
.push(|project, project_path, window, cx| {
|
||||
let project_path = project_path.clone();
|
||||
let abs_path = project.read(cx).absolute_path(&project_path, cx);
|
||||
let is_file = project
|
||||
.read(cx)
|
||||
.entry_for_path(&project_path, cx)
|
||||
.is_some_and(|entry| entry.is_file());
|
||||
let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
|
||||
let is_local = project.read(cx).is_local();
|
||||
let project_item =
|
||||
<T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
|
||||
let project = project.clone();
|
||||
Some(window.spawn(cx, async move |cx| match project_item.await {
|
||||
Ok(project_item) => {
|
||||
let project_item = project_item;
|
||||
let project_entry_id: Option<ProjectEntryId> =
|
||||
project_item.read_with(cx, project::ProjectItem::entry_id)?;
|
||||
let build_workspace_item = Box::new(
|
||||
|pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
|
||||
Box::new(cx.new(|cx| {
|
||||
T::for_project_item(
|
||||
project,
|
||||
Some(pane),
|
||||
project_item,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})) as Box<dyn ItemHandle>
|
||||
},
|
||||
) as Box<_>;
|
||||
Ok((project_entry_id, build_workspace_item))
|
||||
}
|
||||
Err(e) => match abs_path {
|
||||
Some(abs_path) => match cx.update(|window, cx| {
|
||||
T::for_broken_project_item(abs_path, is_local, &e, window, cx)
|
||||
})? {
|
||||
Some(broken_project_item_view) => {
|
||||
let build_workspace_item = Box::new(
|
||||
Some(window.spawn(cx, async move |cx| {
|
||||
match project_item.await.with_context(|| {
|
||||
format!(
|
||||
"opening project path {:?}",
|
||||
entry_abs_path.as_deref().unwrap_or(&project_path.path)
|
||||
)
|
||||
}) {
|
||||
Ok(project_item) => {
|
||||
let project_item = project_item;
|
||||
let project_entry_id: Option<ProjectEntryId> =
|
||||
project_item.read_with(cx, project::ProjectItem::entry_id)?;
|
||||
let build_workspace_item = Box::new(
|
||||
|pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
|
||||
Box::new(cx.new(|cx| {
|
||||
T::for_project_item(
|
||||
project,
|
||||
Some(pane),
|
||||
project_item,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})) as Box<dyn ItemHandle>
|
||||
},
|
||||
) as Box<_>;
|
||||
Ok((project_entry_id, build_workspace_item))
|
||||
}
|
||||
Err(e) => match entry_abs_path.as_deref().filter(|_| is_file) {
|
||||
Some(abs_path) => match cx.update(|window, cx| {
|
||||
T::for_broken_project_item(abs_path, is_local, &e, window, cx)
|
||||
})? {
|
||||
Some(broken_project_item_view) => {
|
||||
let build_workspace_item = Box::new(
|
||||
move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
|
||||
cx.new(|_| broken_project_item_view).boxed_clone()
|
||||
},
|
||||
)
|
||||
as Box<_>;
|
||||
Ok((None, build_workspace_item))
|
||||
}
|
||||
Ok((None, build_workspace_item))
|
||||
}
|
||||
None => Err(e)?,
|
||||
},
|
||||
None => Err(e)?,
|
||||
},
|
||||
None => Err(e)?,
|
||||
},
|
||||
}
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
@ -4011,52 +4022,6 @@ impl Workspace {
|
|||
maybe_pane_handle
|
||||
}
|
||||
|
||||
pub fn split_pane_with_item(
|
||||
&mut self,
|
||||
pane_to_split: WeakEntity<Pane>,
|
||||
split_direction: SplitDirection,
|
||||
from: WeakEntity<Pane>,
|
||||
item_id_to_move: EntityId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(pane_to_split) = pane_to_split.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(from) = from.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_pane = self.add_pane(window, cx);
|
||||
move_item(&from, &new_pane, item_id_to_move, 0, true, window, cx);
|
||||
self.center
|
||||
.split(&pane_to_split, &new_pane, split_direction)
|
||||
.unwrap();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn split_pane_with_project_entry(
|
||||
&mut self,
|
||||
pane_to_split: WeakEntity<Pane>,
|
||||
split_direction: SplitDirection,
|
||||
project_entry: ProjectEntryId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let pane_to_split = pane_to_split.upgrade()?;
|
||||
let new_pane = self.add_pane(window, cx);
|
||||
self.center
|
||||
.split(&pane_to_split, &new_pane, split_direction)
|
||||
.unwrap();
|
||||
|
||||
let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
|
||||
let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx);
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let active_item = self.active_pane.read(cx).active_item();
|
||||
for pane in &self.panes {
|
||||
|
|
|
@ -1308,11 +1308,11 @@ pub fn handle_keymap_file_changes(
|
|||
})
|
||||
.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| {
|
||||
let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
|
||||
if next_mapping != current_mapping {
|
||||
current_mapping = next_mapping;
|
||||
let next_layout_id = cx.keyboard_layout().id();
|
||||
if next_layout_id != current_layout_id {
|
||||
current_layout_id = next_layout_id.to_string();
|
||||
keyboard_layout_tx.unbounded_send(()).ok();
|
||||
}
|
||||
})
|
||||
|
@ -4434,7 +4434,6 @@ mod tests {
|
|||
assert_eq!(actions_without_namespace, Vec::<&str>::new());
|
||||
|
||||
let expected_namespaces = vec![
|
||||
"acp",
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
|
@ -4730,7 +4729,7 @@ mod tests {
|
|||
// and key strokes contain the given key
|
||||
bindings
|
||||
.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 {}",
|
||||
line,
|
||||
action.name(),
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
use anyhow::Result;
|
||||
use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
define_connection! {
|
||||
pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb<WorkspaceDb> =
|
||||
&[sql!(
|
||||
pub struct ComponentPreviewDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for ComponentPreviewDb {
|
||||
const NAME: &str = stringify!(ComponentPreviewDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE component_previews (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
|
@ -13,9 +20,11 @@ define_connection! {
|
|||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)];
|
||||
)];
|
||||
}
|
||||
|
||||
db::static_connection!(COMPONENT_PREVIEW_DB, ComponentPreviewDb, [WorkspaceDb]);
|
||||
|
||||
impl ComponentPreviewDb {
|
||||
pub async fn save_active_page(
|
||||
&self,
|
||||
|
|
|
@ -72,7 +72,10 @@ impl QuickActionBar {
|
|||
Tooltip::with_meta(
|
||||
tooltip_text,
|
||||
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,
|
||||
cx,
|
||||
)
|
||||
|
|
|
@ -284,6 +284,8 @@ pub mod agent {
|
|||
OpenSettings,
|
||||
/// Opens the agent onboarding modal.
|
||||
OpenOnboardingModal,
|
||||
/// Opens the ACP onboarding modal.
|
||||
OpenAcpOnboardingModal,
|
||||
/// Resets the agent onboarding state.
|
||||
ResetOnboarding,
|
||||
/// Starts a chat conversation with the agent.
|
||||
|
|
|
@ -3243,6 +3243,7 @@ Run the `theme selector: toggle` action in the command palette to see a current
|
|||
"indent_size": 20,
|
||||
"auto_reveal_entries": true,
|
||||
"auto_fold_dirs": true,
|
||||
"drag_and_drop": true,
|
||||
"scrollbar": {
|
||||
"show": null
|
||||
},
|
||||
|
|
|
@ -45,9 +45,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
|
|||
// Whether to show the task line in the output of the spawned task, defaults to `true`.
|
||||
"show_summary": true,
|
||||
// Whether to show the command line in the output of the spawned task, defaults to `true`.
|
||||
"show_output": true,
|
||||
"show_output": true
|
||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||
"tags": []
|
||||
// "tags": []
|
||||
}
|
||||
]
|
||||
```
|
||||
|
|
|
@ -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_fold_dirs": true, // Fold dirs with single subdir
|
||||
"sticky_scroll": true, // Stick parent directories at top of the project panel.
|
||||
"drag_and_drop": true, // Whether drag and drop is enabled
|
||||
"scrollbar": { // Project panel scrollbar settings
|
||||
"show": null // Show/hide: (auto, system, always, never)
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue