Compare commits

..

22 commits

Author SHA1 Message Date
MrSubidubi
0e8696df84 Improve padding 2025-08-26 11:37:39 +02:00
MrSubidubi
c0891800dc Improve state handling 2025-08-25 23:39:07 +02:00
MrSubidubi
b4223d318f Merge branch 'main' into ui-scrollbar-teardown 2025-08-25 22:54:12 +02:00
MrSubidubi
2a171a971c Use proper opacity 2025-08-25 22:45:12 +02:00
MrSubidubi
a067729525 Improve layout when two scrollbars are shown 2025-08-24 21:53:15 +02:00
MrSubidubi
1f2d18e691 Only update hover state on scroll 2025-08-20 16:48:52 +02:00
MrSubidubi
76842eed31 Merge branch 'main' into ui-scrollbar-teardown 2025-08-20 10:48:16 +02:00
MrSubidubi
da9084226d WIP 2025-08-20 10:41:52 +02:00
MrSubidubi
fd8fb1ed16 Dim if no space reserved and fix some issues 2025-08-18 17:23:31 +02:00
MrSubidubi
8b458b2a7a Some more fixes 2025-08-18 16:07:47 +02:00
MrSubidubi
351db21ff9 Merge branch 'main' into ui-scrollbar-teardown 2025-08-18 14:28:43 +02:00
MrSubidubi
7622ba09ee Clippy 2025-08-16 00:44:52 +02:00
MrSubidubi
91cdf69924 Merge branch 'main' into ui-scrollbar-teardown 2025-08-16 00:38:25 +02:00
MrSubidubi
6414589243 Merge branch 'main' into ui-scrollbar-teardown 2025-08-16 00:37:13 +02:00
MrSubidubi
3da0c0aa60 Some fixes, impl for uniformlist 2025-08-16 00:36:11 +02:00
MrSubidubi
a9dbfce8f9 Impl for uniformlist and notify cleanupds 2025-08-14 17:22:55 +02:00
MrSubidubi
fd33832609 Do not notify the parent unintentionally 2025-08-13 18:15:40 +02:00
MrSubidubi
40084aa94c Even less clones 2025-08-13 15:13:18 +02:00
MrSubidubi
afcfd0979a Reduce clones 2025-08-13 14:56:49 +02:00
MrSubidubi
3b611313e1 Resolve conflicts 2025-08-13 13:05:31 +02:00
MrSubidubi
bfd71db0a3 Use new div impl to reduce footprint 2025-08-13 12:18:47 +02:00
MrSubidubi
c28d873a2f WIP 2025-08-12 19:53:57 +02:00
122 changed files with 5327 additions and 9205 deletions

View file

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

1
Cargo.lock generated
View file

@ -17521,6 +17521,7 @@ dependencies = [
"icons", "icons",
"itertools 0.14.0", "itertools 0.14.0",
"menu", "menu",
"schemars",
"serde", "serde",
"settings", "settings",
"smallvec", "smallvec",

View file

@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 336 B

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 176 KiB

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -428,13 +428,11 @@
"g h": "vim::StartOfLine", "g h": "vim::StartOfLine",
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
"g e": "vim::EndOfDocument", "g e": "vim::EndOfDocument",
"g .": "vim::HelixGotoLastModification", // go to last modification
"g r": "editor::FindAllReferences", // zed specific "g r": "editor::FindAllReferences", // zed specific
"g t": "vim::WindowTop", "g t": "vim::WindowTop",
"g c": "vim::WindowMiddle", "g c": "vim::WindowMiddle",
"g b": "vim::WindowBottom", "g b": "vim::WindowBottom",
"shift-r": "editor::Paste",
"x": "editor::SelectLine", "x": "editor::SelectLine",
"shift-x": "editor::SelectLine", "shift-x": "editor::SelectLine",
"%": "editor::SelectAll", "%": "editor::SelectAll",

View file

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

View file

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

View file

@ -21,12 +21,12 @@ use ui::prelude::*;
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{Item, Workspace}; use workspace::{Item, Workspace};
actions!(dev, [OpenAcpLogs]); actions!(acp, [OpenDebugTools]);
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.observe_new( cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| { |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| { workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| {
let acp_tools = let acp_tools =
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx))); Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx); workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);

View file

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

View file

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

View file

@ -72,7 +72,6 @@ async fn test_echo(cx: &mut TestAppContext) {
} }
#[gpui::test] #[gpui::test]
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_thinking(cx: &mut TestAppContext) { async fn test_thinking(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake(); let fake_model = model.as_fake();
@ -472,7 +471,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
tool_name: ToolRequiringPermission::name().into(), tool_name: ToolRequiringPermission::name().into(),
is_error: true, is_error: true,
content: "Permission to run tool denied by user".into(), content: "Permission to run tool denied by user".into(),
output: Some("Permission to run tool denied by user".into()) output: None
}) })
] ]
); );
@ -1348,7 +1347,6 @@ async fn test_cancellation(cx: &mut TestAppContext) {
} }
#[gpui::test] #[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) { async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake(); let fake_model = model.as_fake();
@ -1822,11 +1820,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
let clock = Arc::new(clock::FakeSystemClock::new()); let clock = Arc::new(clock::FakeSystemClock::new());
let client = Client::new(clock, http_client, cx); let client = Client::new(clock, http_client, cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
Project::init_settings(cx);
agent_settings::init(cx);
language_model::init(client.clone(), cx); language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx); language_models::init(user_store, client.clone(), cx);
Project::init_settings(cx);
LanguageModelRegistry::test(cx); LanguageModelRegistry::test(cx);
agent_settings::init(cx);
}); });
cx.executor().forbid_parking(); cx.executor().forbid_parking();

View file

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

View file

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

View file

@ -162,34 +162,12 @@ impl AgentConnection for AcpConnection {
let conn = self.connection.clone(); let conn = self.connection.clone();
let sessions = self.sessions.clone(); let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf(); 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| { cx.spawn(async move |cx| {
let response = conn let response = conn
.new_session(acp::NewSessionRequest { mcp_servers, cwd }) .new_session(acp::NewSessionRequest {
mcp_servers: vec![],
cwd,
})
.await .await
.map_err(|err| { .map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code { if err.code == acp::ErrorCode::AUTH_REQUIRED.code {

View file

@ -0,0 +1,524 @@
// Translates old acp agents into the new schema
use action_log::ActionLog;
use agent_client_protocol as acp;
use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
use anyhow::{Context as _, Result, anyhow};
use futures::channel::oneshot;
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use project::Project;
use std::{any::Any, cell::RefCell, path::Path, rc::Rc};
use ui::App;
use util::ResultExt as _;
use crate::AgentServerCommand;
use acp_thread::{AcpThread, AgentConnection, AuthRequired};
#[derive(Clone)]
struct OldAcpClientDelegate {
thread: Rc<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
}
}

View file

@ -0,0 +1,376 @@
use acp_tools::AcpConnectionRegistry;
use action_log::ActionLog;
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
use anyhow::anyhow;
use collections::HashMap;
use futures::AsyncBufReadExt as _;
use futures::channel::oneshot;
use futures::io::BufReader;
use project::Project;
use serde::Deserialize;
use std::path::Path;
use std::rc::Rc;
use std::{any::Any, cell::RefCell};
use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use crate::{AgentServerCommand, acp::UnsupportedVersion};
use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError};
pub struct AcpConnection {
server_name: &'static str,
connection: Rc<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(&notification.session_id)
.context("Failed to get session")?;
session.thread.update(cx, |thread, cx| {
thread.handle_session_update(notification.update, cx)
})??;
Ok(())
}
}

View file

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

View file

@ -5,15 +5,15 @@ use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate; use fuzzy::StringMatchCandidate;
use gpui::{ use gpui::{
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
UniformListScrollHandle, WeakEntity, Window, uniform_list, UniformListScrollHandle, WeakEntity, Window, uniform_list,
}; };
use std::{fmt::Display, ops::Range}; use std::{fmt::Display, ops::Range};
use text::Bias; use text::Bias;
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::{ use ui::{
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, WithScrollbar,
Tooltip, prelude::*, prelude::*,
}; };
pub struct AcpThreadHistory { pub struct AcpThreadHistory {
@ -26,8 +26,6 @@ pub struct AcpThreadHistory {
visible_items: Vec<ListItemType>, visible_items: Vec<ListItemType>,
scrollbar_visibility: bool,
scrollbar_state: ScrollbarState,
local_timezone: UtcOffset, local_timezone: UtcOffset,
_update_task: Task<()>, _update_task: Task<()>,
@ -90,7 +88,6 @@ impl AcpThreadHistory {
}); });
let scroll_handle = UniformListScrollHandle::default(); let scroll_handle = UniformListScrollHandle::default();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self { let mut this = Self {
history_store, history_store,
@ -99,8 +96,6 @@ impl AcpThreadHistory {
hovered_index: None, hovered_index: None,
visible_items: Default::default(), visible_items: Default::default(),
search_editor, search_editor,
scrollbar_visibility: true,
scrollbar_state,
local_timezone: UtcOffset::from_whole_seconds( local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(), chrono::Local::now().offset().local_minus_utc(),
) )
@ -339,43 +334,6 @@ impl AcpThreadHistory {
task.detach_and_log_err(cx); task.detach_and_log_err(cx);
} }
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
return None;
}
Some(
div()
.occlude()
.id("thread-history-scroll")
.h_full()
.bg(cx.theme().colors().panel_background.opacity(0.8))
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.absolute()
.right_1()
.top_0()
.bottom_0()
.w_4()
.pl_1()
.cursor_default()
.on_mouse_move(cx.listener(|_, _, _window, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _window, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _window, cx| {
cx.stop_propagation();
})
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
}
fn render_list_items( fn render_list_items(
&mut self, &mut self,
range: Range<usize>, range: Range<usize>,
@ -491,7 +449,7 @@ impl Focusable for AcpThreadHistory {
} }
impl Render for AcpThreadHistory { impl Render for AcpThreadHistory {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.key_context("ThreadHistory") .key_context("ThreadHistory")
.size_full() .size_full()
@ -542,22 +500,23 @@ impl Render for AcpThreadHistory {
), ),
) )
} else { } else {
view.pr_5() view.pr_5().child(
.child( uniform_list(
uniform_list( "thread-history",
"thread-history", self.visible_items.len(),
self.visible_items.len(), cx.processor(|this, range: Range<usize>, window, cx| {
cx.processor(|this, range: Range<usize>, window, cx| { this.render_list_items(range, window, cx)
this.render_list_items(range, window, cx) }),
}),
)
.p_1()
.track_scroll(self.scroll_handle.clone())
.flex_grow(),
) )
.when_some(self.render_scrollbar(cx), |div, scrollbar| { .p_1()
div.child(scrollbar) .track_scroll(self.scroll_handle.clone())
}) .flex_grow()
.vertical_scrollbar_for(
self.scroll_handle.clone(),
window,
cx,
),
)
} }
}) })
} }

File diff suppressed because it is too large Load diff

View file

@ -22,10 +22,9 @@ use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, Selec
use gpui::{ use gpui::{
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry, AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry,
ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla, ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla,
ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful, ListAlignment, ListOffset, ListState, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement,
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation, Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
UnderlineStyle, WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, percentage, WindowHandle, linear_color_stop, linear_gradient, list, percentage, pulsating_between,
pulsating_between,
}; };
use language::{Buffer, Language, LanguageRegistry}; use language::{Buffer, Language, LanguageRegistry};
use language_model::{ use language_model::{
@ -46,8 +45,7 @@ use std::time::Duration;
use text::ToPoint; use text::ToPoint;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize, Banner, Disclosure, KeyBinding, PopoverMenuHandle, TextSize, Tooltip, WithScrollbar, prelude::*,
Tooltip, prelude::*,
}; };
use util::ResultExt as _; use util::ResultExt as _;
use util::markdown::MarkdownCodeBlock; use util::markdown::MarkdownCodeBlock;
@ -68,7 +66,6 @@ pub struct ActiveThread {
save_thread_task: Option<Task<()>>, save_thread_task: Option<Task<()>>,
messages: Vec<MessageId>, messages: Vec<MessageId>,
list_state: ListState, list_state: ListState,
scrollbar_state: ScrollbarState,
rendered_messages_by_id: HashMap<MessageId, RenderedMessage>, rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>, rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>,
editing_message: Option<(MessageId, EditingMessageState)>, editing_message: Option<(MessageId, EditingMessageState)>,
@ -799,8 +796,7 @@ impl ActiveThread {
expanded_tool_uses: HashMap::default(), expanded_tool_uses: HashMap::default(),
expanded_thinking_segments: HashMap::default(), expanded_thinking_segments: HashMap::default(),
expanded_code_blocks: HashMap::default(), expanded_code_blocks: HashMap::default(),
list_state: list_state.clone(), list_state,
scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
editing_message: None, editing_message: None,
last_error: None, last_error: None,
copied_code_block_ids: HashSet::default(), copied_code_block_ids: HashSet::default(),
@ -3491,39 +3487,6 @@ impl ActiveThread {
} }
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.occlude()
.id("active-thread-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
}
pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool { pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool {
self.expanded_code_blocks self.expanded_code_blocks
.get(&(message_id, ix)) .get(&(message_id, ix))
@ -3557,13 +3520,13 @@ pub enum ActiveThreadEvent {
impl EventEmitter<ActiveThreadEvent> for ActiveThread {} impl EventEmitter<ActiveThreadEvent> for ActiveThread {}
impl Render for ActiveThread { impl Render for ActiveThread {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.size_full() .size_full()
.relative() .relative()
.bg(cx.theme().colors().panel_background) .bg(cx.theme().colors().panel_background)
.child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow()) .child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow())
.child(self.render_vertical_scrollbar(cx)) .vertical_scrollbar_for(self.list_state.clone(), window, cx)
} }
} }

View file

@ -3,23 +3,20 @@ mod configure_context_server_modal;
mod manage_profiles_modal; mod manage_profiles_modal;
mod tool_picker; mod tool_picker;
use std::{ops::Range, sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini}; use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet}; use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan; use cloud_llm_client::Plan;
use collections::HashMap; use collections::HashMap;
use context_server::ContextServerId; use context_server::ContextServerId;
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest; use extension::ExtensionManifest;
use extension_host::ExtensionStore; use extension_host::ExtensionStore;
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity, Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
WeakEntity, percentage,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{
@ -34,10 +31,10 @@ use project::{
use settings::{Settings, SettingsStore, update_settings_file}; use settings::{Settings, SettingsStore, update_settings_file};
use ui::{ use ui::{
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*,
}; };
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file}; use workspace::Workspace;
use zed_actions::ExtensionCategoryFilter; use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal; pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
@ -61,7 +58,6 @@ pub struct AgentConfiguration {
tools: Entity<ToolWorkingSet>, tools: Entity<ToolWorkingSet>,
_registry_subscription: Subscription, _registry_subscription: Subscription,
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
gemini_is_installed: bool, gemini_is_installed: bool,
_check_for_gemini: Task<()>, _check_for_gemini: Task<()>,
} }
@ -105,7 +101,6 @@ impl AgentConfiguration {
.detach(); .detach();
let scroll_handle = ScrollHandle::new(); let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self { let mut this = Self {
fs, fs,
@ -120,7 +115,6 @@ impl AgentConfiguration {
tools, tools,
_registry_subscription: registry_subscription, _registry_subscription: registry_subscription,
scroll_handle, scroll_handle,
scrollbar_state,
gemini_is_installed: false, gemini_is_installed: false,
_check_for_gemini: Task::ready(()), _check_for_gemini: Task::ready(()),
}; };
@ -1061,39 +1055,10 @@ impl AgentConfiguration {
.child( .child(
v_flex() v_flex()
.gap_0p5() .gap_0p5()
.child( .child(Headline::new("External Agents"))
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( .child(
Label::new( Label::new(
"Bring the agent of your choice to Zed via our new Agent Client Protocol.", "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
) )
.color(Color::Muted), .color(Color::Muted),
), ),
@ -1241,32 +1206,7 @@ impl Render for AgentConfiguration {
.child(self.render_context_servers_section(window, cx)) .child(self.render_context_servers_section(window, cx))
.child(self.render_provider_configuration_section(cx)), .child(self.render_provider_configuration_section(cx)),
) )
.child( .vertical_scrollbar(window, cx)
div()
.id("assistant-configuration-scrollbar")
.occlude()
.absolute()
.right(px(3.))
.top_0()
.bottom_0()
.pb_6()
.w(px(12.))
.cursor_default()
.on_mouse_move(cx.listener(|_, _, _window, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _window, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _window, cx| {
cx.stop_propagation();
})
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
} }
} }
@ -1356,109 +1296,3 @@ fn show_unable_to_uninstall_extension_with_context_server(
workspace.toggle_status_toast(status_toast, cx); workspace.toggle_status_toast(status_toast, cx);
} }
async fn open_new_agent_servers_entry_in_settings_editor(
workspace: WeakEntity<Workspace>,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let settings_editor = workspace
.update_in(cx, |_, window, cx| {
create_and_open_local_file(paths::settings_file(), window, cx, || {
settings::initial_user_settings_content().as_ref().into()
})
})?
.await?
.downcast::<Editor>()
.unwrap();
settings_editor
.downgrade()
.update_in(cx, |item, window, cx| {
let text = item.buffer().read(cx).snapshot(cx).text();
let settings = cx.global::<SettingsStore>();
let mut unique_server_name = None;
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
let server_name: Option<SharedString> = (0..u8::MAX)
.map(|i| {
if i == 0 {
"your_agent".into()
} else {
format!("your_agent_{}", i).into()
}
})
.find(|name| !file.custom.contains_key(name));
if let Some(server_name) = server_name {
unique_server_name = Some(server_name.clone());
file.custom.insert(
server_name,
AgentServerSettings {
command: AgentServerCommand {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
},
},
);
}
});
if edits.is_empty() {
return;
}
let ranges = edits
.iter()
.map(|(range, _)| range.clone())
.collect::<Vec<_>>();
item.edit(edits, cx);
if let Some((unique_server_name, buffer)) =
unique_server_name.zip(item.buffer().read(cx).as_singleton())
{
let snapshot = buffer.read(cx).snapshot();
if let Some(range) =
find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
{
item.change_selections(
SelectionEffects::scroll(Autoscroll::newest()),
window,
cx,
|selections| {
selections.select_ranges(vec![range]);
},
);
}
}
})
}
fn find_text_in_buffer(
text: &str,
start: usize,
snapshot: &language::BufferSnapshot,
) -> Option<Range<usize>> {
let chars = text.chars().collect::<Vec<char>>();
let mut offset = start;
let mut char_offset = 0;
for c in snapshot.chars_at(start) {
if char_offset >= chars.len() {
break;
}
offset += 1;
if c == chars[char_offset] {
char_offset += 1;
} else {
char_offset = 0;
}
}
if char_offset == chars.len() {
Some(offset.saturating_sub(chars.len())..offset)
} else {
None
}
}

View file

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

View file

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

View file

@ -4,14 +4,14 @@ use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Task,
UniformListScrollHandle, WeakEntity, Window, uniform_list, UniformListScrollHandle, WeakEntity, Window, uniform_list,
}; };
use std::{fmt::Display, ops::Range, sync::Arc}; use std::{fmt::Display, ops::Range, sync::Arc};
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::{ use ui::{
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, ScrollAxes, Scrollbars, Tooltip,
Tooltip, prelude::*, WithScrollbar, prelude::*,
}; };
use util::ResultExt; use util::ResultExt;
@ -30,8 +30,6 @@ pub struct ThreadHistory {
separated_item_indexes: Vec<u32>, separated_item_indexes: Vec<u32>,
_separated_items_task: Option<Task<()>>, _separated_items_task: Option<Task<()>>,
search_state: SearchState, search_state: SearchState,
scrollbar_visibility: bool,
scrollbar_state: ScrollbarState,
_subscriptions: Vec<gpui::Subscription>, _subscriptions: Vec<gpui::Subscription>,
} }
@ -90,7 +88,6 @@ impl ThreadHistory {
}); });
let scroll_handle = UniformListScrollHandle::default(); let scroll_handle = UniformListScrollHandle::default();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self { let mut this = Self {
agent_panel, agent_panel,
@ -103,8 +100,6 @@ impl ThreadHistory {
separated_items: Default::default(), separated_items: Default::default(),
separated_item_indexes: Default::default(), separated_item_indexes: Default::default(),
search_editor, search_editor,
scrollbar_visibility: true,
scrollbar_state,
_subscriptions: vec![search_editor_subscription, history_store_subscription], _subscriptions: vec![search_editor_subscription, history_store_subscription],
_separated_items_task: None, _separated_items_task: None,
}; };
@ -363,43 +358,6 @@ impl ThreadHistory {
cx.notify(); cx.notify();
} }
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
return None;
}
Some(
div()
.occlude()
.id("thread-history-scroll")
.h_full()
.bg(cx.theme().colors().panel_background.opacity(0.8))
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.absolute()
.right_1()
.top_0()
.bottom_0()
.w_4()
.pl_1()
.cursor_default()
.on_mouse_move(cx.listener(|_, _, _window, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _window, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _window, cx| {
cx.stop_propagation();
})
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) { fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(entry) = self.get_match(self.selected_index) { if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry { let task_result = match entry {
@ -536,7 +494,7 @@ impl Focusable for ThreadHistory {
} }
impl Render for ThreadHistory { impl Render for ThreadHistory {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.key_context("ThreadHistory") .key_context("ThreadHistory")
.size_full() .size_full()
@ -601,9 +559,14 @@ impl Render for ThreadHistory {
.track_scroll(self.scroll_handle.clone()) .track_scroll(self.scroll_handle.clone())
.flex_grow(), .flex_grow(),
) )
.when_some(self.render_scrollbar(cx), |div, scrollbar| { .custom_scrollbars(
div.child(scrollbar) Scrollbars::new(ScrollAxes::Vertical)
}) .tracked_scroll_handle(self.scroll_handle.clone())
.width_sm()
.with_track_along(ScrollAxes::Vertical),
window,
cx,
)
} }
}) })
} }

View file

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

View file

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

View file

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

View file

@ -1,10 +1,7 @@
use anyhow::Result; use anyhow::Result;
use db::{ use db::{
query, define_connection, query,
sqlez::{ sqlez::{bindable::Column, statement::Statement},
bindable::Column, domain::Domain, statement::Statement,
thread_safe_connection::ThreadSafeConnection,
},
sqlez_macros::sql, sqlez_macros::sql,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -53,11 +50,8 @@ impl Column for SerializedCommandInvocation {
} }
} }
pub struct CommandPaletteDB(ThreadSafeConnection); define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
&[sql!(
impl Domain for CommandPaletteDB {
const NAME: &str = stringify!(CommandPaletteDB);
const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE IF NOT EXISTS command_invocations( CREATE TABLE IF NOT EXISTS command_invocations(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
command_name TEXT NOT NULL, command_name TEXT NOT NULL,
@ -65,9 +59,7 @@ impl Domain for CommandPaletteDB {
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
) STRICT; ) STRICT;
)]; )];
} );
db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
impl CommandPaletteDB { impl CommandPaletteDB {
pub async fn write_command_invocation( pub async fn write_command_invocation(

View file

@ -110,14 +110,11 @@ pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection {
} }
/// Implements a basic DB wrapper for a given domain /// 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_export]
macro_rules! static_connection { macro_rules! define_connection {
($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => { (pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => {
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
impl ::std::ops::Deref for $t { impl ::std::ops::Deref for $t {
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
@ -126,6 +123,16 @@ macro_rules! static_connection {
} }
} }
impl $crate::sqlez::domain::Domain for $t {
fn name() -> &'static str {
stringify!($t)
}
fn migrations() -> &'static [&'static str] {
$migrations
}
}
impl $t { impl $t {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub async fn open_test_db(name: &'static str) -> Self { pub async fn open_test_db(name: &'static str) -> Self {
@ -135,8 +142,7 @@ macro_rules! static_connection {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
#[allow(unused_parens)] $t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id))))
$t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id))))
}); });
#[cfg(not(any(test, feature = "test-support")))] #[cfg(not(any(test, feature = "test-support")))]
@ -147,10 +153,46 @@ macro_rules! static_connection {
} else { } else {
$crate::RELEASE_CHANNEL.dev_name() $crate::RELEASE_CHANNEL.dev_name()
}; };
#[allow(unused_parens)] $t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope)))
$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) pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
@ -177,12 +219,17 @@ mod tests {
enum BadDB {} enum BadDB {}
impl Domain for BadDB { impl Domain for BadDB {
const NAME: &str = "db_tests"; fn name() -> &'static str {
const MIGRATIONS: &[&str] = &[ "db_tests"
sql!(CREATE TABLE test(value);), }
// failure because test already exists
sql!(CREATE TABLE test(value);), fn migrations() -> &'static [&'static str] {
]; &[
sql!(CREATE TABLE test(value);),
// failure because test already exists
sql!(CREATE TABLE test(value);),
]
}
} }
let tempdir = tempfile::Builder::new() let tempdir = tempfile::Builder::new()
@ -204,15 +251,25 @@ mod tests {
enum CorruptedDB {} enum CorruptedDB {}
impl Domain for CorruptedDB { impl Domain for CorruptedDB {
const NAME: &str = "db_tests"; fn name() -> &'static str {
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; "db_tests"
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);)]
}
} }
enum GoodDB {} enum GoodDB {}
impl Domain for GoodDB { impl Domain for GoodDB {
const NAME: &str = "db_tests"; //Notice same name fn name() -> &'static str {
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; "db_tests" //Notice same name
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
} }
let tempdir = tempfile::Builder::new() let tempdir = tempfile::Builder::new()
@ -248,16 +305,25 @@ mod tests {
enum CorruptedDB {} enum CorruptedDB {}
impl Domain for CorruptedDB { impl Domain for CorruptedDB {
const NAME: &str = "db_tests"; fn name() -> &'static str {
"db_tests"
}
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);)]
}
} }
enum GoodDB {} enum GoodDB {}
impl Domain for GoodDB { impl Domain for GoodDB {
const NAME: &str = "db_tests"; //Notice same name fn name() -> &'static str {
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration "db_tests" //Notice same name
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
} }
let tempdir = tempfile::Builder::new() let tempdir = tempfile::Builder::new()

View file

@ -2,26 +2,16 @@ use gpui::App;
use sqlez_macros::sql; use sqlez_macros::sql;
use util::ResultExt as _; use util::ResultExt as _;
use crate::{ use crate::{define_connection, query, write_and_log};
query,
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
write_and_log,
};
pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection); define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
&[sql!(
impl Domain for KeyValueStore {
const NAME: &str = stringify!(KeyValueStore);
const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE IF NOT EXISTS kv_store( CREATE TABLE IF NOT EXISTS kv_store(
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT NOT NULL value TEXT NOT NULL
) STRICT; ) STRICT;
)]; )];
} );
crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
pub trait Dismissable { pub trait Dismissable {
const KEY: &'static str; const KEY: &'static str;
@ -101,19 +91,15 @@ mod tests {
} }
} }
pub struct GlobalKeyValueStore(ThreadSafeConnection); define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> =
&[sql!(
impl Domain for GlobalKeyValueStore {
const NAME: &str = stringify!(GlobalKeyValueStore);
const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE IF NOT EXISTS kv_store( CREATE TABLE IF NOT EXISTS kv_store(
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT NOT NULL value TEXT NOT NULL
) STRICT; ) STRICT;
)]; )];
} global
);
crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global);
impl GlobalKeyValueStore { impl GlobalKeyValueStore {
query! { query! {

View file

@ -10,7 +10,7 @@ use db::kvp::KEY_VALUE_STORE;
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
Stateful, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
}; };
use language::Point; use language::Point;
use project::{ use project::{
@ -23,8 +23,8 @@ use project::{
worktree_store::WorktreeStore, worktree_store::WorktreeStore,
}; };
use ui::{ use ui::{
Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render, Scrollbar, Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render,
ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*, StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
}; };
use workspace::Workspace; use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint}; use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
@ -49,7 +49,6 @@ pub(crate) struct BreakpointList {
breakpoint_store: Entity<BreakpointStore>, breakpoint_store: Entity<BreakpointStore>,
dap_store: Entity<DapStore>, dap_store: Entity<DapStore>,
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
scrollbar_state: ScrollbarState,
breakpoints: Vec<BreakpointEntry>, breakpoints: Vec<BreakpointEntry>,
session: Option<Entity<Session>>, session: Option<Entity<Session>>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
@ -87,7 +86,6 @@ impl BreakpointList {
let dap_store = project.dap_store(); let dap_store = project.dap_store();
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
let scroll_handle = UniformListScrollHandle::new(); let scroll_handle = UniformListScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let adapter_name = session.as_ref().map(|session| session.read(cx).adapter()); let adapter_name = session.as_ref().map(|session| session.read(cx).adapter());
cx.new(|cx| { cx.new(|cx| {
@ -95,7 +93,6 @@ impl BreakpointList {
breakpoint_store, breakpoint_store,
dap_store, dap_store,
worktree_store, worktree_store,
scrollbar_state,
breakpoints: Default::default(), breakpoints: Default::default(),
workspace, workspace,
session, session,
@ -576,39 +573,6 @@ impl BreakpointList {
.flex_1() .flex_1()
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.occlude()
.id("breakpoint-list-vertical-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
}
pub(crate) fn render_control_strip(&self) -> AnyElement { pub(crate) fn render_control_strip(&self) -> AnyElement {
let selection_kind = self.selection_kind(); let selection_kind = self.selection_kind();
let focus_handle = self.focus_handle.clone(); let focus_handle = self.focus_handle.clone();
@ -789,7 +753,7 @@ impl Render for BreakpointList {
.size_full() .size_full()
.pt_1() .pt_1()
.child(self.render_list(cx)) .child(self.render_list(cx))
.child(self.render_vertical_scrollbar(cx)) .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
.when_some(self.strip_mode, |this, _| { .when_some(self.strip_mode, |this, _| {
this.child(Divider::horizontal().color(DividerColor::Border)) this.child(Divider::horizontal().color(DividerColor::Border))
.child( .child(

View file

@ -9,7 +9,7 @@ use std::{
use editor::{Editor, EditorElement, EditorStyle}; use editor::{Editor, EditorElement, EditorStyle};
use gpui::{ use gpui::{
Action, AppContext, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle, Focusable, Action, AppContext, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle, Focusable,
MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task, TextStyle, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Subscription, Task, TextStyle,
UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point, UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point,
uniform_list, uniform_list,
}; };
@ -19,7 +19,7 @@ use settings::Settings;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
ContextMenu, Divider, DropdownMenu, FluentBuilder, IntoElement, PopoverMenuHandle, Render, ContextMenu, Divider, DropdownMenu, FluentBuilder, IntoElement, PopoverMenuHandle, Render,
Scrollbar, ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*, ScrollableHandle, StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
}; };
use workspace::Workspace; use workspace::Workspace;
@ -30,7 +30,6 @@ actions!(debugger, [GoToSelectedAddress]);
pub(crate) struct MemoryView { pub(crate) struct MemoryView {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
scroll_handle: UniformListScrollHandle, scroll_handle: UniformListScrollHandle,
scroll_state: ScrollbarState,
stack_frame_list: WeakEntity<StackFrameList>, stack_frame_list: WeakEntity<StackFrameList>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
view_state: ViewState, view_state: ViewState,
@ -121,11 +120,10 @@ impl ViewState {
} }
} }
struct ScrollbarDragging;
static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> = static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> =
LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}")))); LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}"))));
static UNKNOWN_BYTE: SharedString = SharedString::new_static("??"); static UNKNOWN_BYTE: SharedString = SharedString::new_static("??");
impl MemoryView { impl MemoryView {
pub(crate) fn new( pub(crate) fn new(
session: Entity<Session>, session: Entity<Session>,
@ -139,10 +137,8 @@ impl MemoryView {
let query_editor = cx.new(|cx| Editor::single_line(window, cx)); let query_editor = cx.new(|cx| Editor::single_line(window, cx));
let scroll_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self { let mut this = Self {
workspace, workspace,
scroll_state,
scroll_handle, scroll_handle,
stack_frame_list, stack_frame_list,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
@ -162,43 +158,6 @@ impl MemoryView {
this this
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.occlude()
.id("memory-view-vertical-scrollbar")
.on_drag_move(cx.listener(|this, evt, _, cx| {
let did_handle = this.handle_scroll_drag(evt);
cx.notify();
if did_handle {
cx.stop_propagation()
}
}))
.on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scroll_state.clone()).map(|s| s.auto_hide(cx)))
}
fn render_memory(&self, cx: &mut Context<Self>) -> UniformList { fn render_memory(&self, cx: &mut Context<Self>) -> UniformList {
let weak = cx.weak_entity(); let weak = cx.weak_entity();
let session = self.session.clone(); let session = self.session.clone();
@ -233,10 +192,9 @@ impl MemoryView {
.track_scroll(self.scroll_handle.clone()) .track_scroll(self.scroll_handle.clone())
.on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| { .on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| {
let delta = evt.delta.pixel_delta(window.line_height()); let delta = evt.delta.pixel_delta(window.line_height());
let scroll_handle = this.scroll_state.scroll_handle(); let size = this.scroll_handle.content_size();
let size = scroll_handle.content_size(); let viewport = this.scroll_handle.viewport();
let viewport = scroll_handle.viewport(); let current_offset = this.scroll_handle.offset();
let current_offset = scroll_handle.offset();
let first_entry_offset_boundary = size.height / this.view_state.row_count() as f32; let first_entry_offset_boundary = size.height / this.view_state.row_count() as f32;
let last_entry_offset_boundary = size.height - first_entry_offset_boundary; let last_entry_offset_boundary = size.height - first_entry_offset_boundary;
if first_entry_offset_boundary + viewport.size.height > current_offset.y.abs() { if first_entry_offset_boundary + viewport.size.height > current_offset.y.abs() {
@ -245,7 +203,8 @@ impl MemoryView {
} else if last_entry_offset_boundary < current_offset.y.abs() + viewport.size.height { } else if last_entry_offset_boundary < current_offset.y.abs() + viewport.size.height {
this.view_state.schedule_scroll_down(); this.view_state.schedule_scroll_down();
} }
scroll_handle.set_offset(current_offset + point(px(0.), delta.y)); this.scroll_handle
.set_offset(current_offset + point(px(0.), delta.y));
})) }))
} }
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement { fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
@ -297,7 +256,7 @@ impl MemoryView {
} }
let row_count = self.view_state.row_count(); let row_count = self.view_state.row_count();
debug_assert!(row_count > 1); debug_assert!(row_count > 1);
let scroll_handle = self.scroll_state.scroll_handle(); let scroll_handle = &self.scroll_handle;
let viewport = scroll_handle.viewport(); let viewport = scroll_handle.viewport();
if viewport.bottom() < evt.event.position.y { if viewport.bottom() < evt.event.position.y {
@ -307,13 +266,15 @@ impl MemoryView {
} }
} }
fn handle_scroll_drag(&mut self, evt: &DragMoveEvent<ScrollbarDragging>) -> bool { #[allow(unused)]
if !self.scroll_state.is_dragging() { fn handle_scroll_drag(&mut self, evt: &DragMoveEvent<()>) -> bool {
return false; // todo!
} // if !self.scroll_state.is_dragging() {
// return false;
// }
let row_count = self.view_state.row_count(); let row_count = self.view_state.row_count();
debug_assert!(row_count > 1); debug_assert!(row_count > 1);
let scroll_handle = self.scroll_state.scroll_handle(); let scroll_handle = &self.scroll_handle;
let viewport = scroll_handle.viewport(); let viewport = scroll_handle.viewport();
if viewport.bottom() < evt.event.position.y { if viewport.bottom() < evt.event.position.y {
@ -943,7 +904,7 @@ impl Render for MemoryView {
) )
.with_priority(1) .with_priority(1)
})) }))
.child(self.render_vertical_scrollbar(cx)), .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
) )
} }
} }

View file

@ -1,15 +1,15 @@
use anyhow::anyhow; use anyhow::anyhow;
use dap::Module; use dap::Module;
use gpui::{ use gpui::{
AnyElement, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful, AnyElement, Entity, FocusHandle, Focusable, ScrollStrategy, Subscription, Task,
Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list, UniformListScrollHandle, WeakEntity, uniform_list,
}; };
use project::{ use project::{
ProjectItem as _, ProjectPath, ProjectItem as _, ProjectPath,
debugger::session::{Session, SessionEvent}, debugger::session::{Session, SessionEvent},
}; };
use std::{ops::Range, path::Path, sync::Arc}; use std::{ops::Range, path::Path, sync::Arc};
use ui::{Scrollbar, ScrollbarState, prelude::*}; use ui::{WithScrollbar, prelude::*};
use workspace::Workspace; use workspace::Workspace;
pub struct ModuleList { pub struct ModuleList {
@ -18,7 +18,6 @@ pub struct ModuleList {
session: Entity<Session>, session: Entity<Session>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
scrollbar_state: ScrollbarState,
entries: Vec<Module>, entries: Vec<Module>,
_rebuild_task: Option<Task<()>>, _rebuild_task: Option<Task<()>>,
_subscription: Subscription, _subscription: Subscription,
@ -44,7 +43,6 @@ impl ModuleList {
let scroll_handle = UniformListScrollHandle::new(); let scroll_handle = UniformListScrollHandle::new();
Self { Self {
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle, scroll_handle,
session, session,
workspace, workspace,
@ -167,38 +165,6 @@ impl ModuleList {
self.session self.session
.update(cx, |session, cx| session.modules(cx).to_vec()) .update(cx, |session, cx| session.modules(cx).to_vec())
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.occlude()
.id("module-list-vertical-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) { fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let Some(ix) = self.selected_ix else { return }; let Some(ix) = self.selected_ix else { return };
@ -313,6 +279,6 @@ impl Render for ModuleList {
.size_full() .size_full()
.p_1() .p_1()
.child(self.render_list(window, cx)) .child(self.render_list(window, cx))
.child(self.render_vertical_scrollbar(cx)) .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
} }
} }

View file

@ -5,8 +5,8 @@ use std::time::Duration;
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use dap::StackFrameId; use dap::StackFrameId;
use gpui::{ use gpui::{
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, Subscription,
Stateful, Subscription, Task, WeakEntity, list, Task, WeakEntity, list,
}; };
use util::debug_panic; use util::debug_panic;
@ -15,7 +15,7 @@ use language::PointUtf16;
use project::debugger::breakpoint_store::ActiveStackFrame; use project::debugger::breakpoint_store::ActiveStackFrame;
use project::debugger::session::{Session, SessionEvent, StackFrame}; use project::debugger::session::{Session, SessionEvent, StackFrame};
use project::{ProjectItem, ProjectPath}; use project::{ProjectItem, ProjectPath};
use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*}; use ui::{Tooltip, WithScrollbar, prelude::*};
use workspace::{ItemHandle, Workspace}; use workspace::{ItemHandle, Workspace};
use super::RunningState; use super::RunningState;
@ -35,7 +35,6 @@ pub struct StackFrameList {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
selected_ix: Option<usize>, selected_ix: Option<usize>,
opened_stack_frame_id: Option<StackFrameId>, opened_stack_frame_id: Option<StackFrameId>,
scrollbar_state: ScrollbarState,
list_state: ListState, list_state: ListState,
error: Option<SharedString>, error: Option<SharedString>,
_refresh_task: Task<()>, _refresh_task: Task<()>,
@ -71,7 +70,6 @@ impl StackFrameList {
}); });
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
let scrollbar_state = ScrollbarState::new(list_state.clone());
let mut this = Self { let mut this = Self {
session, session,
@ -84,7 +82,6 @@ impl StackFrameList {
selected_ix: None, selected_ix: None,
opened_stack_frame_id: None, opened_stack_frame_id: None,
list_state, list_state,
scrollbar_state,
_refresh_task: Task::ready(()), _refresh_task: Task::ready(()),
}; };
this.schedule_refresh(true, window, cx); this.schedule_refresh(true, window, cx);
@ -581,39 +578,6 @@ impl StackFrameList {
} }
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.occlude()
.id("stack-frame-list-vertical-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) { fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix; self.selected_ix = ix;
cx.notify(); cx.notify();
@ -740,7 +704,7 @@ impl Render for StackFrameList {
) )
}) })
.child(self.render_list(window, cx)) .child(self.render_list(window, cx))
.child(self.render_vertical_scrollbar(cx)) .vertical_scrollbar_for(self.list_state.clone(), window, cx)
} }
} }

View file

@ -8,9 +8,8 @@ use dap::{
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity, Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription, FocusHandle, Focusable, Hsla, MouseDownEvent, Point, Subscription, TextStyleRefinement,
TextStyleRefinement, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,
uniform_list,
}; };
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious}; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
use project::debugger::{ use project::debugger::{
@ -18,7 +17,7 @@ use project::debugger::{
session::{Session, SessionEvent, Watcher}, session::{Session, SessionEvent, Watcher},
}; };
use std::{collections::HashMap, ops::Range, sync::Arc}; use std::{collections::HashMap, ops::Range, sync::Arc};
use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*}; use ui::{ContextMenu, ListItem, ScrollableHandle, Tooltip, WithScrollbar, prelude::*};
use util::{debug_panic, maybe}; use util::{debug_panic, maybe};
actions!( actions!(
@ -189,7 +188,6 @@ pub struct VariableList {
entry_states: HashMap<EntryPath, EntryState>, entry_states: HashMap<EntryPath, EntryState>,
selected_stack_frame_id: Option<StackFrameId>, selected_stack_frame_id: Option<StackFrameId>,
list_handle: UniformListScrollHandle, list_handle: UniformListScrollHandle,
scrollbar_state: ScrollbarState,
session: Entity<Session>, session: Entity<Session>,
selection: Option<EntryPath>, selection: Option<EntryPath>,
open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
@ -235,7 +233,6 @@ impl VariableList {
let list_state = UniformListScrollHandle::default(); let list_state = UniformListScrollHandle::default();
Self { Self {
scrollbar_state: ScrollbarState::new(list_state.clone()),
list_handle: list_state, list_handle: list_state,
session, session,
focus_handle, focus_handle,
@ -1500,39 +1497,6 @@ impl VariableList {
) )
.into_any() .into_any()
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.occlude()
.id("variable-list-vertical-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
} }
impl Focusable for VariableList { impl Focusable for VariableList {
@ -1542,7 +1506,7 @@ impl Focusable for VariableList {
} }
impl Render for VariableList { impl Render for VariableList {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.key_context("VariableList") .key_context("VariableList")
@ -1587,7 +1551,7 @@ impl Render for VariableList {
) )
.with_priority(1) .with_priority(1)
})) }))
.child(self.render_vertical_scrollbar(cx)) .vertical_scrollbar_for(self.list_handle.clone(), window, cx)
} }
} }

View file

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

View file

@ -55,7 +55,7 @@ pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPla
pub use edit_prediction::Direction; pub use edit_prediction::Direction;
pub use editor_settings::{ pub use editor_settings::{
CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode, CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode,
ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar, ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap,
}; };
pub use editor_settings_controls::*; pub use editor_settings_controls::*;
pub use element::{ pub use element::{
@ -165,7 +165,7 @@ use project::{
}; };
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
use rpc::{ErrorCode, ErrorExt, proto::PeerId}; use rpc::{ErrorCode, ErrorExt, proto::PeerId};
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
use selections_collection::{ use selections_collection::{
MutableSelectionsCollection, SelectionsCollection, resolve_selections, MutableSelectionsCollection, SelectionsCollection, resolve_selections,
}; };
@ -198,7 +198,7 @@ use theme::{
}; };
use ui::{ use ui::{
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName, ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*, IconSize, Indicator, Key, Tooltip, h_flex, prelude::*, scrollbars::ScrollbarAutoHide,
}; };
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc}; use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
use workspace::{ use workspace::{
@ -2588,7 +2588,7 @@ impl Editor {
|| binding || binding
.keystrokes() .keystrokes()
.first() .first()
.is_some_and(|keystroke| keystroke.display_modifiers.modified()) .is_some_and(|keystroke| keystroke.modifiers.modified())
})) }))
} }
@ -7686,16 +7686,16 @@ impl Editor {
.keystroke() .keystroke()
{ {
modifiers_held = modifiers_held modifiers_held = modifiers_held
|| (&accept_keystroke.display_modifiers == modifiers || (&accept_keystroke.modifiers == modifiers
&& accept_keystroke.display_modifiers.modified()); && accept_keystroke.modifiers.modified());
}; };
if let Some(accept_partial_keystroke) = self if let Some(accept_partial_keystroke) = self
.accept_edit_prediction_keybind(true, window, cx) .accept_edit_prediction_keybind(true, window, cx)
.keystroke() .keystroke()
{ {
modifiers_held = modifiers_held modifiers_held = modifiers_held
|| (&accept_partial_keystroke.display_modifiers == modifiers || (&accept_partial_keystroke.modifiers == modifiers
&& accept_partial_keystroke.display_modifiers.modified()); && accept_partial_keystroke.modifiers.modified());
} }
if modifiers_held { if modifiers_held {
@ -9044,7 +9044,7 @@ impl Editor {
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() { let modifiers_color = if accept_keystroke.modifiers == window.modifiers() {
Color::Accent Color::Accent
} else { } else {
Color::Muted Color::Muted
@ -9056,19 +9056,19 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::XSmall.rems(cx)) .text_size(TextSize::XSmall.rems(cx))
.child(h_flex().children(ui::render_modifiers( .child(h_flex().children(ui::render_modifiers(
&accept_keystroke.display_modifiers, &accept_keystroke.modifiers,
PlatformStyle::platform(), PlatformStyle::platform(),
Some(modifiers_color), Some(modifiers_color),
Some(IconSize::XSmall.rems().into()), Some(IconSize::XSmall.rems().into()),
true, true,
))) )))
.when(is_platform_style_mac, |parent| { .when(is_platform_style_mac, |parent| {
parent.child(accept_keystroke.display_key.clone()) parent.child(accept_keystroke.key.clone())
}) })
.when(!is_platform_style_mac, |parent| { .when(!is_platform_style_mac, |parent| {
parent.child( parent.child(
Key::new( Key::new(
util::capitalize(&accept_keystroke.display_key), util::capitalize(&accept_keystroke.key),
Some(Color::Default), Some(Color::Default),
) )
.size(Some(IconSize::XSmall.rems().into())), .size(Some(IconSize::XSmall.rems().into())),
@ -9171,7 +9171,7 @@ impl Editor {
max_width: Pixels, max_width: Pixels,
cursor_point: Point, cursor_point: Point,
style: &EditorStyle, style: &EditorStyle,
accept_keystroke: Option<&gpui::KeybindingKeystroke>, accept_keystroke: Option<&gpui::Keystroke>,
_window: &Window, _window: &Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> Option<AnyElement> { ) -> Option<AnyElement> {
@ -9249,7 +9249,7 @@ impl Editor {
accept_keystroke.as_ref(), accept_keystroke.as_ref(),
|el, accept_keystroke| { |el, accept_keystroke| {
el.child(h_flex().children(ui::render_modifiers( el.child(h_flex().children(ui::render_modifiers(
&accept_keystroke.display_modifiers, &accept_keystroke.modifiers,
PlatformStyle::platform(), PlatformStyle::platform(),
Some(Color::Default), Some(Color::Default),
Some(IconSize::XSmall.rems().into()), Some(IconSize::XSmall.rems().into()),
@ -9319,7 +9319,7 @@ impl Editor {
.child(completion), .child(completion),
) )
.when_some(accept_keystroke, |el, accept_keystroke| { .when_some(accept_keystroke, |el, accept_keystroke| {
if !accept_keystroke.display_modifiers.modified() { if !accept_keystroke.modifiers.modified() {
return el; return el;
} }
@ -9338,7 +9338,7 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.when(is_platform_style_mac, |parent| parent.gap_1()) .when(is_platform_style_mac, |parent| parent.gap_1())
.child(h_flex().children(ui::render_modifiers( .child(h_flex().children(ui::render_modifiers(
&accept_keystroke.display_modifiers, &accept_keystroke.modifiers,
PlatformStyle::platform(), PlatformStyle::platform(),
Some(if !has_completion { Some(if !has_completion {
Color::Muted Color::Muted

View file

@ -7,6 +7,7 @@ use project::project_settings::DiagnosticSeverity;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, VsCodeSettings}; use settings::{Settings, SettingsSources, VsCodeSettings};
use ui::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar};
use util::serde::default_true; use util::serde::default_true;
/// Imports from the VSCode settings at /// Imports from the VSCode settings at
@ -196,23 +197,6 @@ pub struct Gutter {
pub folds: bool, pub folds: bool,
} }
/// When to show the scrollbar in the editor.
///
/// Default: auto
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ShowScrollbar {
/// Show the scrollbar if there's important information or
/// follow the system's configured behavior.
Auto,
/// Match the system's configured behavior.
System,
/// Always show the scrollbar.
Always,
/// Never show the scrollbar.
Never,
}
/// When to show the minimap in the editor. /// When to show the minimap in the editor.
/// ///
/// Default: never /// Default: never
@ -735,6 +719,12 @@ impl EditorSettings {
} }
} }
impl ScrollbarVisibilitySetting for EditorSettings {
fn scrollbar_visibility(&self, _cx: &App) -> ShowScrollbar {
self.scrollbar.show
}
}
impl Settings for EditorSettings { impl Settings for EditorSettings {
const KEY: Option<&'static str> = None; const KEY: Option<&'static str> = None;

View file

@ -18,7 +18,7 @@ use crate::{
editor_settings::{ editor_settings::{
CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap, CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap,
MinimapThumb, MinimapThumbBorder, ScrollBeyondLastLine, ScrollbarAxes, MinimapThumb, MinimapThumbBorder, ScrollBeyondLastLine, ScrollbarAxes,
ScrollbarDiagnostics, ShowMinimap, ShowScrollbar, ScrollbarDiagnostics, ShowMinimap,
}, },
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer}, git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
hover_popover::{ hover_popover::{
@ -43,10 +43,10 @@ use gpui::{
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
transparent_black, transparent_black,
}; };
@ -84,7 +84,7 @@ use text::{BufferId, SelectionGoal};
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
use ui::{ use ui::{
ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*,
right_click_menu, right_click_menu, scrollbars::ShowScrollbar,
}; };
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use util::post_inc; use util::post_inc;
@ -7150,7 +7150,7 @@ fn header_jump_data(
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>); pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
impl AcceptEditPredictionBinding { impl AcceptEditPredictionBinding {
pub fn keystroke(&self) -> Option<&KeybindingKeystroke> { pub fn keystroke(&self) -> Option<&Keystroke> {
if let Some(binding) = self.0.as_ref() { if let Some(binding) = self.0.as_ref() {
match &binding.keystrokes() { match &binding.keystrokes() {
[keystroke, ..] => Some(keystroke), [keystroke, ..] => Some(keystroke),

View file

@ -9,8 +9,8 @@ use anyhow::Context as _;
use gpui::{ use gpui::{
AnyElement, AsyncWindowContext, Context, Entity, Focusable as _, FontWeight, Hsla, AnyElement, AsyncWindowContext, Context, Entity, Focusable as _, FontWeight, Hsla,
InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size,
Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, TextStyleRefinement,
TextStyleRefinement, Window, div, px, Window, div, px,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::{DiagnosticEntry, Language, LanguageRegistry}; use language::{DiagnosticEntry, Language, LanguageRegistry};
@ -23,7 +23,7 @@ use std::{borrow::Cow, cell::RefCell};
use std::{ops::Range, sync::Arc, time::Duration}; use std::{ops::Range, sync::Arc, time::Duration};
use std::{path::PathBuf, rc::Rc}; use std::{path::PathBuf, rc::Rc};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{Scrollbar, ScrollbarState, prelude::*, theme_is_transparent}; use ui::{Scrollbars, WithScrollbar, prelude::*, theme_is_transparent};
use url::Url; use url::Url;
use util::TryFutureExt; use util::TryFutureExt;
use workspace::{OpenOptions, OpenVisible, Workspace}; use workspace::{OpenOptions, OpenVisible, Workspace};
@ -184,7 +184,6 @@ pub fn hover_at_inlay(
let hover_popover = InfoPopover { let hover_popover = InfoPopover {
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
parsed_content, parsed_content,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle, scroll_handle,
keyboard_grace: Rc::new(RefCell::new(false)), keyboard_grace: Rc::new(RefCell::new(false)),
anchor: None, anchor: None,
@ -387,7 +386,6 @@ fn show_hover(
local_diagnostic, local_diagnostic,
markdown, markdown,
border_color, border_color,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle, scroll_handle,
background_color, background_color,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
@ -457,7 +455,6 @@ fn show_hover(
info_popovers.push(InfoPopover { info_popovers.push(InfoPopover {
symbol_range: RangeInEditor::Text(range), symbol_range: RangeInEditor::Text(range),
parsed_content, parsed_content,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle, scroll_handle,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor), anchor: Some(anchor),
@ -507,7 +504,6 @@ fn show_hover(
info_popovers.push(InfoPopover { info_popovers.push(InfoPopover {
symbol_range: RangeInEditor::Text(range), symbol_range: RangeInEditor::Text(range),
parsed_content, parsed_content,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle, scroll_handle,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor), anchor: Some(anchor),
@ -846,7 +842,6 @@ pub struct InfoPopover {
pub symbol_range: RangeInEditor, pub symbol_range: RangeInEditor,
pub parsed_content: Option<Entity<Markdown>>, pub parsed_content: Option<Entity<Markdown>>,
pub scroll_handle: ScrollHandle, pub scroll_handle: ScrollHandle,
pub scrollbar_state: ScrollbarState,
pub keyboard_grace: Rc<RefCell<bool>>, pub keyboard_grace: Rc<RefCell<bool>>,
pub anchor: Option<Anchor>, pub anchor: Option<Anchor>,
_subscription: Option<Subscription>, _subscription: Option<Subscription>,
@ -891,7 +886,12 @@ impl InfoPopover {
.on_url_click(open_markdown_url), .on_url_click(open_markdown_url),
), ),
) )
.child(self.render_vertical_scrollbar(cx)) .custom_scrollbars(
Scrollbars::for_settings::<EditorSettings>()
.tracked_scroll_handle(self.scroll_handle.clone()),
window,
cx,
)
}) })
.into_any_element() .into_any_element()
} }
@ -905,39 +905,6 @@ impl InfoPopover {
cx.notify(); cx.notify();
self.scroll_handle.set_offset(current); self.scroll_handle.set_offset(current);
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
div()
.occlude()
.id("info-popover-vertical-scroll")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
} }
pub struct DiagnosticPopover { pub struct DiagnosticPopover {
@ -949,7 +916,6 @@ pub struct DiagnosticPopover {
pub anchor: Anchor, pub anchor: Anchor,
_subscription: Subscription, _subscription: Subscription,
pub scroll_handle: ScrollHandle, pub scroll_handle: ScrollHandle,
pub scrollbar_state: ScrollbarState,
} }
impl DiagnosticPopover { impl DiagnosticPopover {
@ -1013,43 +979,15 @@ impl DiagnosticPopover {
), ),
), ),
) )
.child(self.render_vertical_scrollbar(cx)), .custom_scrollbars(
Scrollbars::for_settings::<EditorSettings>()
.tracked_scroll_handle(self.scroll_handle.clone()),
window,
cx,
),
) )
.into_any_element() .into_any_element()
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
div()
.occlude()
.id("diagnostic-popover-vertical-scroll")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1404,7 +1404,7 @@ impl ProjectItem for Editor {
} }
fn for_broken_project_item( fn for_broken_project_item(
abs_path: &Path, abs_path: PathBuf,
is_local: bool, is_local: bool,
e: &anyhow::Error, e: &anyhow::Error,
window: &mut Window, window: &mut Window,

View file

@ -1,17 +1,13 @@
use anyhow::Result; use anyhow::Result;
use db::{ use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
query, use db::sqlez::statement::Statement;
sqlez::{
bindable::{Bind, Column, StaticColumnCount},
domain::Domain,
statement::Statement,
},
sqlez_macros::sql,
};
use fs::MTime; use fs::MTime;
use itertools::Itertools as _; use itertools::Itertools as _;
use std::path::PathBuf; use std::path::PathBuf;
use db::sqlez_macros::sql;
use db::{define_connection, query};
use workspace::{ItemId, WorkspaceDb, WorkspaceId}; use workspace::{ItemId, WorkspaceDb, WorkspaceId};
#[derive(Clone, Debug, PartialEq, Default)] #[derive(Clone, Debug, PartialEq, Default)]
@ -87,11 +83,7 @@ impl Column for SerializedEditor {
} }
} }
pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); define_connection!(
impl Domain for EditorDb {
const NAME: &str = stringify!(EditorDb);
// Current schema shape using pseudo-rust syntax: // Current schema shape using pseudo-rust syntax:
// editors( // editors(
// item_id: usize, // item_id: usize,
@ -121,8 +113,7 @@ impl Domain for EditorDb {
// start: usize, // start: usize,
// end: usize, // end: usize,
// ) // )
pub static ref DB: EditorDb<WorkspaceDb> = &[
const MIGRATIONS: &[&str] = &[
sql! ( sql! (
CREATE TABLE editors( CREATE TABLE editors(
item_id INTEGER NOT NULL, item_id INTEGER NOT NULL,
@ -198,9 +189,7 @@ impl Domain for EditorDb {
) STRICT; ) STRICT;
), ),
]; ];
} );
db::static_connection!(DB, EditorDb, [WorkspaceDb]);
// https://www.sqlite.org/limits.html // https://www.sqlite.org/limits.html
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, // > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,

View file

@ -12,7 +12,7 @@ use crate::{
}; };
pub use autoscroll::{Autoscroll, AutoscrollStrategy}; pub use autoscroll::{Autoscroll, AutoscrollStrategy};
use core::fmt::Debug; use core::fmt::Debug;
use gpui::{Along, App, Axis, Context, Global, Pixels, Task, Window, point, px}; use gpui::{Along, App, Axis, Context, Pixels, Task, Window, point, px};
use language::language_settings::{AllLanguageSettings, SoftWrap}; use language::language_settings::{AllLanguageSettings, SoftWrap};
use language::{Bias, Point}; use language::{Bias, Point};
pub use scroll_amount::ScrollAmount; pub use scroll_amount::ScrollAmount;
@ -21,6 +21,7 @@ use std::{
cmp::Ordering, cmp::Ordering,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use ui::scrollbars::ScrollbarAutoHide;
use util::ResultExt; use util::ResultExt;
use workspace::{ItemId, WorkspaceId}; use workspace::{ItemId, WorkspaceId};
@ -29,11 +30,6 @@ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
pub struct WasScrolled(pub(crate) bool); pub struct WasScrolled(pub(crate) bool);
#[derive(Default)]
pub struct ScrollbarAutoHide(pub bool);
impl Global for ScrollbarAutoHide {}
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollAnchor { pub struct ScrollAnchor {
pub offset: gpui::Point<f32>, pub offset: gpui::Point<f32>,
@ -327,7 +323,7 @@ impl ScrollManager {
cx.notify(); cx.notify();
} }
if cx.default_global::<ScrollbarAutoHide>().0 { if cx.default_global::<ScrollbarAutoHide>().should_hide() {
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| { self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| {
cx.background_executor() cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL) .timer(SCROLLBAR_SHOW_INTERVAL)

View file

@ -2,8 +2,8 @@ use crate::actions::ShowSignatureHelp;
use crate::hover_popover::open_markdown_url; use crate::hover_popover::open_markdown_url;
use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style}; use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style};
use gpui::{ use gpui::{
App, Context, Div, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, Stateful, App, Context, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, StyledText, Task,
StyledText, Task, TextStyle, Window, combine_highlights, TextStyle, Window, combine_highlights,
}; };
use language::BufferSnapshot; use language::BufferSnapshot;
use markdown::{Markdown, MarkdownElement}; use markdown::{Markdown, MarkdownElement};
@ -15,8 +15,8 @@ use theme::ThemeSettings;
use ui::{ use ui::{
ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton, ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton,
IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon,
LabelSize, ParentElement, Pixels, Scrollbar, ScrollbarState, SharedString, LabelSize, ParentElement, Pixels, SharedString, StatefulInteractiveElement, Styled, StyledExt,
StatefulInteractiveElement, Styled, StyledExt, div, px, relative, WithScrollbar, div, relative,
}; };
// Language-specific settings may define quotes as "brackets", so filter them out separately. // Language-specific settings may define quotes as "brackets", so filter them out separately.
@ -243,7 +243,6 @@ impl Editor {
.min(signatures.len().saturating_sub(1)); .min(signatures.len().saturating_sub(1));
let signature_help_popover = SignatureHelpPopover { let signature_help_popover = SignatureHelpPopover {
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
style, style,
signatures, signatures,
current_signature, current_signature,
@ -330,7 +329,6 @@ pub struct SignatureHelpPopover {
pub signatures: Vec<SignatureHelp>, pub signatures: Vec<SignatureHelp>,
pub current_signature: usize, pub current_signature: usize,
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
} }
impl SignatureHelpPopover { impl SignatureHelpPopover {
@ -391,7 +389,8 @@ impl SignatureHelpPopover {
) )
}), }),
) )
.child(self.render_vertical_scrollbar(cx)); .vertical_scrollbar(window, cx);
let controls = if self.signatures.len() > 1 { let controls = if self.signatures.len() > 1 {
let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp) let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
@ -460,26 +459,4 @@ impl SignatureHelpPopover {
.child(main_content) .child(main_content)
.into_any_element() .into_any_element()
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
div()
.occlude()
.id("signature_help_scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| cx.stop_propagation())
.on_any_mouse_down(|_, _, cx| cx.stop_propagation())
.on_mouse_up(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify()))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_1()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
} }

View file

@ -24,8 +24,8 @@ use settings::Settings;
use strum::IntoEnumIterator as _; use strum::IntoEnumIterator as _;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState, CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, ToggleButton, Tooltip,
ToggleButton, Tooltip, prelude::*, WithScrollbar, prelude::*,
}; };
use vim_mode_setting::VimModeSetting; use vim_mode_setting::VimModeSetting;
use workspace::{ use workspace::{
@ -290,7 +290,6 @@ pub struct ExtensionsPage {
_subscriptions: [gpui::Subscription; 2], _subscriptions: [gpui::Subscription; 2],
extension_fetch_task: Option<Task<()>>, extension_fetch_task: Option<Task<()>>,
upsells: BTreeSet<Feature>, upsells: BTreeSet<Feature>,
scrollbar_state: ScrollbarState,
} }
impl ExtensionsPage { impl ExtensionsPage {
@ -339,7 +338,7 @@ impl ExtensionsPage {
let mut this = Self { let mut this = Self {
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
list: scroll_handle.clone(), list: scroll_handle,
is_fetching_extensions: false, is_fetching_extensions: false,
filter: ExtensionFilter::All, filter: ExtensionFilter::All,
dev_extension_entries: Vec::new(), dev_extension_entries: Vec::new(),
@ -351,7 +350,6 @@ impl ExtensionsPage {
_subscriptions: subscriptions, _subscriptions: subscriptions,
query_editor, query_editor,
upsells: BTreeSet::default(), upsells: BTreeSet::default(),
scrollbar_state: ScrollbarState::new(scroll_handle),
}; };
this.fetch_extensions( this.fetch_extensions(
this.search_query(cx), this.search_query(cx),
@ -1375,7 +1373,7 @@ impl ExtensionsPage {
} }
impl Render for ExtensionsPage { impl Render for ExtensionsPage {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.size_full() .size_full()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
@ -1520,25 +1518,24 @@ impl Render for ExtensionsPage {
} }
if count == 0 { if count == 0 {
return this.py_4().child(self.render_empty_state(cx)); this.py_4()
} .child(self.render_empty_state(cx))
.into_any_element()
let scroll_handle = self.list.clone(); } else {
this.child( let scroll_handle = self.list.clone();
uniform_list("entries", count, cx.processor(Self::render_extensions)) this.child(
uniform_list(
"entries",
count,
cx.processor(Self::render_extensions),
)
.flex_grow() .flex_grow()
.pb_4() .pb_4()
.track_scroll(scroll_handle), .track_scroll(scroll_handle.clone()),
) )
.child( .vertical_scrollbar_for(scroll_handle, window, cx)
div() .into_any_element()
.absolute() }
.right_1()
.top_0()
.bottom_0()
.w(px(12.))
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
}), }),
) )
} }

View file

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

View file

@ -13,10 +13,7 @@ use agent_settings::AgentSettings;
use anyhow::Context as _; use anyhow::Context as _;
use askpass::AskPassDelegate; use askpass::AskPassDelegate;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::{ use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
scroll::ScrollbarAutoHide,
};
use futures::StreamExt as _; use futures::StreamExt as _;
use git::blame::ParsedCommitMessage; use git::blame::ParsedCommitMessage;
use git::repository::{ use git::repository::{
@ -31,7 +28,7 @@ use git::{
UnstageAll, UnstageAll,
}; };
use gpui::{ use gpui::{
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, ClickEvent, Corner,
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point,
PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle, PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
@ -63,9 +60,10 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames}; use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime; use time::OffsetDateTime;
use ui::{ use ui::{
Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu, Scrollbar, Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu,
ScrollbarState, SplitButton, Tooltip, prelude::*, SplitButton, Tooltip, prelude::*,
}; };
use ui::{ScrollAxes, Scrollbars, WithScrollbar};
use util::{ResultExt, TryFutureExt, maybe}; use util::{ResultExt, TryFutureExt, maybe};
use workspace::SERIALIZATION_THROTTLE_TIME; use workspace::SERIALIZATION_THROTTLE_TIME;
@ -276,61 +274,6 @@ struct PendingOperation {
op_id: usize, op_id: usize,
} }
// computed state related to how to render scrollbars
// one per axis
// on render we just read this off the panel
// we update it when
// - settings change
// - on focus in, on focus out, on hover, etc.
#[derive(Debug)]
struct ScrollbarProperties {
axis: Axis,
show_scrollbar: bool,
show_track: bool,
auto_hide: bool,
hide_task: Option<Task<()>>,
state: ScrollbarState,
}
impl ScrollbarProperties {
// Shows the scrollbar and cancels any pending hide task
fn show(&mut self, cx: &mut Context<GitPanel>) {
if !self.auto_hide {
return;
}
self.show_scrollbar = true;
self.hide_task.take();
cx.notify();
}
fn hide(&mut self, window: &mut Window, cx: &mut Context<GitPanel>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
if !self.auto_hide {
return;
}
let axis = self.axis;
self.hide_task = Some(cx.spawn_in(window, async move |panel, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
if let Some(panel) = panel.upgrade() {
panel
.update(cx, |panel, cx| {
match axis {
Axis::Vertical => panel.vertical_scrollbar.show_scrollbar = false,
Axis::Horizontal => panel.horizontal_scrollbar.show_scrollbar = false,
}
cx.notify();
})
.log_err();
}
}));
}
}
pub struct GitPanel { pub struct GitPanel {
pub(crate) active_repository: Option<Entity<Repository>>, pub(crate) active_repository: Option<Entity<Repository>>,
pub(crate) commit_editor: Entity<Editor>, pub(crate) commit_editor: Entity<Editor>,
@ -343,8 +286,6 @@ pub struct GitPanel {
single_tracked_entry: Option<GitStatusEntry>, single_tracked_entry: Option<GitStatusEntry>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
horizontal_scrollbar: ScrollbarProperties,
vertical_scrollbar: ScrollbarProperties,
new_count: usize, new_count: usize,
entry_count: usize, entry_count: usize,
new_staged_count: usize, new_staged_count: usize,
@ -429,10 +370,6 @@ impl GitPanel {
cx.new(|cx| { cx.new(|cx| {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, Self::focus_in).detach(); cx.on_focus(&focus_handle, window, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.hide_scrollbars(window, cx);
})
.detach();
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
cx.observe_global::<SettingsStore>(move |this, cx| { cx.observe_global::<SettingsStore>(move |this, cx| {
@ -457,24 +394,6 @@ impl GitPanel {
let scroll_handle = UniformListScrollHandle::new(); let scroll_handle = UniformListScrollHandle::new();
let vertical_scrollbar = ScrollbarProperties {
axis: Axis::Vertical,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let horizontal_scrollbar = ScrollbarProperties {
axis: Axis::Horizontal,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let mut assistant_enabled = AgentSettings::get_global(cx).enabled; let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
let mut was_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; let mut was_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| { let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
@ -555,8 +474,6 @@ impl GitPanel {
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
modal_open: false, modal_open: false,
entry_count: 0, entry_count: 0,
horizontal_scrollbar,
vertical_scrollbar,
bulk_staging: None, bulk_staging: None,
_settings_subscription, _settings_subscription,
}; };
@ -566,86 +483,6 @@ impl GitPanel {
}) })
} }
fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.horizontal_scrollbar.hide(window, cx);
self.vertical_scrollbar.hide(window, cx);
}
fn update_scrollbar_properties(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
// TODO: This PR should have defined Editor's `scrollbar.axis`
// as an Option<ScrollbarAxis>, not a ScrollbarAxes as it would allow you to
// `.unwrap_or(EditorSettings::get_global(cx).scrollbar.show)`.
//
// Once this is fixed we can extend the GitPanelSettings with a `scrollbar.axis`
// so we can show each axis based on the settings.
//
// We should fix this. PR: https://github.com/zed-industries/zed/pull/19495
let show_setting = GitPanelSettings::get_global(cx)
.scrollbar
.show
.unwrap_or(EditorSettings::get_global(cx).scrollbar.show);
let scroll_handle = self.scroll_handle.0.borrow();
let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
ShowScrollbar::Auto => true,
ShowScrollbar::System => cx
.try_global::<ScrollbarAutoHide>()
.map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
ShowScrollbar::Always => false,
ShowScrollbar::Never => false,
};
let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
(size.contents.width > size.item.width).then_some(size.contents.width)
});
// is there an item long enough that we should show a horizontal scrollbar?
let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
} else {
true
};
let show_horizontal = match (show_setting, item_wider_than_container) {
(_, false) => false,
(ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always, true) => true,
(ShowScrollbar::Never, true) => false,
};
let show_vertical = match show_setting {
ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
};
let show_horizontal_track =
show_horizontal && matches!(show_setting, ShowScrollbar::Always);
// TODO: we probably should hide the scroll track when the list doesn't need to scroll
let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
self.vertical_scrollbar = ScrollbarProperties {
axis: self.vertical_scrollbar.axis,
state: self.vertical_scrollbar.state.clone(),
show_scrollbar: show_vertical,
show_track: show_vertical_track,
auto_hide: autohide(show_setting, cx),
hide_task: None,
};
self.horizontal_scrollbar = ScrollbarProperties {
axis: self.horizontal_scrollbar.axis,
state: self.horizontal_scrollbar.state.clone(),
show_scrollbar: show_horizontal,
show_track: show_horizontal_track,
auto_hide: autohide(show_setting, cx),
hide_task: None,
};
cx.notify();
}
pub fn entry_by_path(&self, path: &RepoPath, cx: &App) -> Option<usize> { pub fn entry_by_path(&self, path: &RepoPath, cx: &App) -> Option<usize> {
if GitPanelSettings::get_global(cx).sort_by_path { if GitPanelSettings::get_global(cx).sort_by_path {
return self return self
@ -2594,12 +2431,11 @@ impl GitPanel {
cx.background_executor().timer(UPDATE_DEBOUNCE).await; cx.background_executor().timer(UPDATE_DEBOUNCE).await;
if let Some(git_panel) = handle.upgrade() { if let Some(git_panel) = handle.upgrade() {
git_panel git_panel
.update_in(cx, |git_panel, window, cx| { .update(cx, |git_panel, cx| {
if clear_pending { if clear_pending {
git_panel.clear_pending(); git_panel.clear_pending();
} }
git_panel.update_visible_entries(cx); git_panel.update_visible_entries(cx);
git_panel.update_scrollbar_properties(window, cx);
}) })
.ok(); .ok();
} }
@ -3710,110 +3546,6 @@ impl GitPanel {
) )
} }
fn render_vertical_scrollbar(
&self,
show_horizontal_scrollbar_container: bool,
cx: &mut Context<Self>,
) -> impl IntoElement {
div()
.id("git-panel-vertical-scroll")
.occlude()
.flex_none()
.h_full()
.cursor_default()
.absolute()
.right_0()
.top_0()
.bottom_0()
.w(px(12.))
.when(show_horizontal_scrollbar_container, |this| {
this.pb_neg_3p5()
})
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
if !this.vertical_scrollbar.state.is_dragging()
&& !this.focus_handle.contains_focused(window, cx)
{
this.vertical_scrollbar.hide(window, cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(
// percentage as f32..end_offset as f32,
self.vertical_scrollbar.state.clone(),
))
}
/// Renders the horizontal scrollbar.
///
/// The right offset is used to determine how far to the right the
/// scrollbar should extend to, useful for ensuring it doesn't collide
/// with the vertical scrollbar when visible.
fn render_horizontal_scrollbar(
&self,
right_offset: Pixels,
cx: &mut Context<Self>,
) -> impl IntoElement {
div()
.id("git-panel-horizontal-scroll")
.occlude()
.flex_none()
.w_full()
.cursor_default()
.absolute()
.bottom_neg_px()
.left_0()
.right_0()
.pr(right_offset)
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
if !this.horizontal_scrollbar.state.is_dragging()
&& !this.focus_handle.contains_focused(window, cx)
{
this.horizontal_scrollbar.hide(window, cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.children(Scrollbar::horizontal(
// percentage as f32..end_offset as f32,
self.horizontal_scrollbar.state.clone(),
))
}
fn render_buffer_header_controls( fn render_buffer_header_controls(
&self, &self,
entity: &Entity<Self>, entity: &Entity<Self>,
@ -3861,33 +3593,16 @@ impl GitPanel {
fn render_entries( fn render_entries(
&self, &self,
has_write_access: bool, has_write_access: bool,
_: &Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let entry_count = self.entries.len(); let entry_count = self.entries.len();
let scroll_track_size = px(16.);
let h_scroll_offset = if self.vertical_scrollbar.show_scrollbar {
// magic number
px(3.)
} else {
px(0.)
};
v_flex() v_flex()
.flex_1() .flex_1()
.size_full() .size_full()
.overflow_hidden() .overflow_hidden()
.relative() .relative()
// Show a border on the top and bottom of the container when
// the vertical scrollbar container is visible so we don't have a
// floating left border in the panel.
.when(self.vertical_scrollbar.show_track, |this| {
this.border_t_1()
.border_b_1()
.border_color(cx.theme().colors().border)
})
.child( .child(
h_flex() h_flex()
.flex_1() .flex_1()
@ -3928,15 +3643,6 @@ impl GitPanel {
items items
}), }),
) )
.when(
!self.horizontal_scrollbar.show_track
&& self.horizontal_scrollbar.show_scrollbar,
|this| {
// when not showing the horizontal scrollbar track, make sure we don't
// obscure the last entry
this.pb(scroll_track_size)
},
)
.size_full() .size_full()
.flex_grow() .flex_grow()
.with_sizing_behavior(ListSizingBehavior::Auto) .with_sizing_behavior(ListSizingBehavior::Auto)
@ -3952,72 +3658,14 @@ impl GitPanel {
this.deploy_panel_context_menu(event.position, window, cx) this.deploy_panel_context_menu(event.position, window, cx)
}), }),
) )
.when(self.vertical_scrollbar.show_track, |this| { .custom_scrollbars(
this.child( Scrollbars::for_settings::<GitPanelSettings>()
v_flex() .tracked_scroll_handle(self.scroll_handle.clone())
.h_full() .with_track_along(ScrollAxes::Horizontal),
.flex_none() window,
.w(scroll_track_size) cx,
.bg(cx.theme().colors().panel_background) ),
.child(
div()
.size_full()
.flex_1()
.border_l_1()
.border_color(cx.theme().colors().border),
),
)
})
.when(self.vertical_scrollbar.show_scrollbar, |this| {
this.child(
self.render_vertical_scrollbar(
self.horizontal_scrollbar.show_track,
cx,
),
)
}),
) )
.when(self.horizontal_scrollbar.show_track, |this| {
this.child(
h_flex()
.w_full()
.h(scroll_track_size)
.flex_none()
.relative()
.child(
div()
.w_full()
.flex_1()
// for some reason the horizontal scrollbar is 1px
// taller than the vertical scrollbar??
.h(scroll_track_size - px(1.))
.bg(cx.theme().colors().panel_background)
.border_t_1()
.border_color(cx.theme().colors().border),
)
.when(self.vertical_scrollbar.show_track, |this| {
this.child(
div()
.flex_none()
// -1px prevents a missing pixel between the two container borders
.w(scroll_track_size - px(1.))
.h_full(),
)
.child(
// HACK: Fill the missing 1px 🥲
div()
.absolute()
.right(scroll_track_size - px(1.))
.bottom(scroll_track_size - px(1.))
.size_px()
.bg(cx.theme().colors().border),
)
}),
)
})
.when(self.horizontal_scrollbar.show_scrollbar, |this| {
this.child(self.render_horizontal_scrollbar(h_scroll_offset, cx))
})
} }
fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label { fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label {
@ -4466,7 +4114,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn Language
is_enabled is_enabled
.then(|| { .then(|| {
let ConfiguredModel { provider, model } = let ConfiguredModel { provider, model } =
LanguageModelRegistry::read_global(cx).commit_message_model()?; LanguageModelRegistry::read_global(cx).commit_message_model(cx)?;
provider.is_authenticated(cx).then(|| model) provider.is_authenticated(cx).then(|| model)
}) })
@ -4526,15 +4174,6 @@ impl Render for GitPanel {
.when(has_write_access && has_co_authors, |git_panel| { .when(has_write_access && has_co_authors, |git_panel| {
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors)) git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
}) })
.on_hover(cx.listener(move |this, hovered, window, cx| {
if *hovered {
this.horizontal_scrollbar.show(cx);
this.vertical_scrollbar.show(cx);
cx.notify();
} else if !this.focus_handle.contains_focused(window, cx) {
this.hide_scrollbars(window, cx);
}
}))
.size_full() .size_full()
.overflow_hidden() .overflow_hidden()
.bg(cx.theme().colors().panel_background) .bg(cx.theme().colors().panel_background)

View file

@ -1,8 +1,9 @@
use editor::ShowScrollbar; use editor::EditorSettings;
use gpui::Pixels; use gpui::Pixels;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsSources}; use settings::{Settings, SettingsSources};
use ui::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar};
use workspace::dock::DockPosition; use workspace::dock::DockPosition;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@ -89,6 +90,22 @@ pub struct GitPanelSettings {
pub collapse_untracked_diff: bool, pub collapse_untracked_diff: bool,
} }
impl ScrollbarVisibilitySetting for GitPanelSettings {
fn scrollbar_visibility(&self, cx: &ui::App) -> ShowScrollbar {
// TODO: This PR should have defined Editor's `scrollbar.axis`
// as an Option<ScrollbarAxis>, not a ScrollbarAxes as it would allow you to
// `.unwrap_or(EditorSettings::get_global(cx).scrollbar.show)`.
//
// Once this is fixed we can extend the GitPanelSettings with a `scrollbar.axis`
// so we can show each axis based on the settings.
//
// We should fix this. PR: https://github.com/zed-industries/zed/pull/19495
self.scrollbar
.show
.unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
}
}
impl Settings for GitPanelSettings { impl Settings for GitPanelSettings {
const KEY: Option<&'static str> = Some("git_panel"); const KEY: Option<&'static str> = Some("git_panel");

View file

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

View file

@ -16,10 +16,10 @@
//! constructed by combining these two systems into an all-in-one element. //! constructed by combining these two systems into an all-in-one element.
use crate::{ use crate::{
Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase, AbsoluteLength, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent,
Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior, DispatchPhase, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox,
HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent,
KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton,
MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, ParentElement, Pixels, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, ParentElement, Pixels,
Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task,
TooltipId, Visibility, Window, WindowControlArea, point, px, size, TooltipId, Visibility, Window, WindowControlArea, point, px, size,
@ -1036,6 +1036,15 @@ pub trait StatefulInteractiveElement: InteractiveElement {
self self
} }
/// Set the space to be reserved for rendering the scrollbar.
///
/// This will only affect the layout of the element when overflow for this element is set to
/// `Overflow::Scroll`.
fn scrollbar_width(mut self, width: impl Into<AbsoluteLength>) -> Self {
self.interactivity().base_style.scrollbar_width = Some(width.into());
self
}
/// Track the scroll state of this element with the given handle. /// Track the scroll state of this element with the given handle.
fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
self.interactivity().tracked_scroll_handle = Some(scroll_handle.clone()); self.interactivity().tracked_scroll_handle = Some(scroll_handle.clone());

View file

@ -5,10 +5,10 @@
//! elements with uniform height. //! elements with uniform height.
use crate::{ use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId, AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, Entity,
Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,
ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled, IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size,
Window, point, size, StyleRefinement, Styled, Window, point, size,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@ -71,7 +71,7 @@ pub struct UniformList {
/// Frame state used by the [UniformList]. /// Frame state used by the [UniformList].
pub struct UniformListFrameState { pub struct UniformListFrameState {
items: SmallVec<[AnyElement; 32]>, items: SmallVec<[AnyElement; 32]>,
decorations: SmallVec<[AnyElement; 1]>, decorations: SmallVec<[AnyElement; 2]>,
} }
/// A handle for controlling the scroll position of a uniform list. /// A handle for controlling the scroll position of a uniform list.
@ -529,6 +529,31 @@ pub trait UniformListDecoration {
) -> AnyElement; ) -> AnyElement;
} }
impl<T: UniformListDecoration + 'static> UniformListDecoration for Entity<T> {
fn compute(
&self,
visible_range: Range<usize>,
bounds: Bounds<Pixels>,
scroll_offset: Point<Pixels>,
item_height: Pixels,
item_count: usize,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
self.update(cx, |inner, cx| {
inner.compute(
visible_range,
bounds,
scroll_offset,
item_height,
item_count,
window,
cx,
)
})
}
}
impl UniformList { impl UniformList {
/// Selects a specific list item for measurement. /// Selects a specific list item for measurement.
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self { pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -153,7 +153,7 @@ pub struct Style {
#[refineable] #[refineable]
pub overflow: Point<Overflow>, pub overflow: Point<Overflow>,
/// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes. /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
pub scrollbar_width: f32, pub scrollbar_width: AbsoluteLength,
/// Whether both x and y axis should be scrollable at the same time. /// Whether both x and y axis should be scrollable at the same time.
pub allow_concurrent_scroll: bool, pub allow_concurrent_scroll: bool,
/// Whether scrolling should be restricted to the axis indicated by the mouse wheel. /// Whether scrolling should be restricted to the axis indicated by the mouse wheel.
@ -745,7 +745,7 @@ impl Default for Style {
}, },
allow_concurrent_scroll: false, allow_concurrent_scroll: false,
restrict_scroll_to_axis: false, restrict_scroll_to_axis: false,
scrollbar_width: 0.0, scrollbar_width: AbsoluteLength::default(),
position: Position::Relative, position: Position::Relative,
inset: Edges::auto(), inset: Edges::auto(),
margin: Edges::<Length>::zero(), margin: Edges::<Length>::zero(),

View file

@ -277,7 +277,7 @@ impl ToTaffy<taffy::style::Style> for Style {
taffy::style::Style { taffy::style::Style {
display: self.display.into(), display: self.display.into(),
overflow: self.overflow.into(), overflow: self.overflow.into(),
scrollbar_width: self.scrollbar_width, scrollbar_width: self.scrollbar_width.to_taffy(rem_size),
position: self.position.into(), position: self.position.into(),
inset: self.inset.to_taffy(rem_size), inset: self.inset.to_taffy(rem_size),
size: self.size.to_taffy(rem_size), size: self.size.to_taffy(rem_size),
@ -314,6 +314,15 @@ impl ToTaffy<taffy::style::Style> for Style {
} }
} }
impl ToTaffy<f32> for AbsoluteLength {
fn to_taffy(&self, rem_size: Pixels) -> f32 {
match self {
AbsoluteLength::Pixels(pixels) => pixels.into(),
AbsoluteLength::Rems(rems) => (*rems * rem_size).into(),
}
}
}
impl ToTaffy<taffy::style::LengthPercentageAuto> for Length { impl ToTaffy<taffy::style::LengthPercentageAuto> for Length {
fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto { fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto {
match self { match self {

View file

@ -2504,7 +2504,7 @@ impl Window {
&mut self, &mut self,
key: impl Into<ElementId>, key: impl Into<ElementId>,
cx: &mut App, cx: &mut App,
init: impl FnOnce(&mut Self, &mut App) -> S, init: impl FnOnce(&mut Self, &mut Context<S>) -> S,
) -> Entity<S> { ) -> Entity<S> {
let current_view = self.current_view(); let current_view = self.current_view();
self.with_global_id(key.into(), |global_id, window| { self.with_global_id(key.into(), |global_id, window| {
@ -2537,7 +2537,7 @@ impl Window {
pub fn use_state<S: 'static>( pub fn use_state<S: 'static>(
&mut self, &mut self,
cx: &mut App, cx: &mut App,
init: impl FnOnce(&mut Self, &mut App) -> S, init: impl FnOnce(&mut Self, &mut Context<S>) -> S,
) -> Entity<S> { ) -> Entity<S> {
self.use_keyed_state( self.use_keyed_state(
ElementId::CodeLocation(*core::panic::Location::caller()), ElementId::CodeLocation(*core::panic::Location::caller()),
@ -4838,6 +4838,12 @@ impl<T: Into<SharedString>> From<(ElementId, T)> for ElementId {
} }
} }
impl From<&'static core::panic::Location<'static>> for ElementId {
fn from(location: &'static core::panic::Location<'static>) -> Self {
ElementId::CodeLocation(*location)
}
}
/// A rectangle to be rendered in the window at the given position and size. /// A rectangle to be rendered in the window at the given position and size.
/// Passed as an argument [`Window::paint_quad`]. /// Passed as an argument [`Window::paint_quad`].
#[derive(Clone)] #[derive(Clone)]

View file

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

View file

@ -401,19 +401,12 @@ pub fn init(cx: &mut App) {
mod persistence { mod persistence {
use std::path::PathBuf; use std::path::PathBuf;
use db::{ use db::{define_connection, query, sqlez_macros::sql};
query,
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
sqlez_macros::sql,
};
use workspace::{ItemId, WorkspaceDb, WorkspaceId}; use workspace::{ItemId, WorkspaceDb, WorkspaceId};
pub struct ImageViewerDb(ThreadSafeConnection); define_connection! {
pub static ref IMAGE_VIEWER: ImageViewerDb<WorkspaceDb> =
impl Domain for ImageViewerDb { &[sql!(
const NAME: &str = stringify!(ImageViewerDb);
const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE image_viewers ( CREATE TABLE image_viewers (
workspace_id INTEGER, workspace_id INTEGER,
item_id INTEGER UNIQUE, item_id INTEGER UNIQUE,
@ -424,11 +417,9 @@ mod persistence {
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE ON DELETE CASCADE
) STRICT; ) STRICT;
)]; )];
} }
db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]);
impl ImageViewerDb { impl ImageViewerDb {
query! { query! {
pub async fn save_image_path( pub async fn save_image_path(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1743,5 +1743,6 @@ pub enum Event {
} }
impl EventEmitter<Event> for LogStore {} impl EventEmitter<Event> for LogStore {}
impl EventEmitter<Event> for LspLogView {}
impl EventEmitter<EditorEvent> for LspLogView {} impl EventEmitter<EditorEvent> for LspLogView {}
impl EventEmitter<SearchEvent> for LspLogView {} impl EventEmitter<SearchEvent> for LspLogView {}

View file

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

View file

@ -850,19 +850,13 @@ impl workspace::SerializableItem for Onboarding {
} }
mod persistence { mod persistence {
use db::{ use db::{define_connection, query, sqlez_macros::sql};
query,
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
sqlez_macros::sql,
};
use workspace::WorkspaceDb; use workspace::WorkspaceDb;
pub struct OnboardingPagesDb(ThreadSafeConnection); define_connection! {
pub static ref ONBOARDING_PAGES: OnboardingPagesDb<WorkspaceDb> =
impl Domain for OnboardingPagesDb { &[
const NAME: &str = stringify!(OnboardingPagesDb); sql!(
const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE onboarding_pages ( CREATE TABLE onboarding_pages (
workspace_id INTEGER, workspace_id INTEGER,
item_id INTEGER UNIQUE, item_id INTEGER UNIQUE,
@ -872,11 +866,10 @@ mod persistence {
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE ON DELETE CASCADE
) STRICT; ) STRICT;
)]; ),
];
} }
db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
impl OnboardingPagesDb { impl OnboardingPagesDb {
query! { query! {
pub async fn save_onboarding_page( pub async fn save_onboarding_page(

View file

@ -414,19 +414,13 @@ impl workspace::SerializableItem for WelcomePage {
} }
mod persistence { mod persistence {
use db::{ use db::{define_connection, query, sqlez_macros::sql};
query,
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
sqlez_macros::sql,
};
use workspace::WorkspaceDb; use workspace::WorkspaceDb;
pub struct WelcomePagesDb(ThreadSafeConnection); define_connection! {
pub static ref WELCOME_PAGES: WelcomePagesDb<WorkspaceDb> =
impl Domain for WelcomePagesDb { &[
const NAME: &str = stringify!(WelcomePagesDb); sql!(
const MIGRATIONS: &[&str] = (&[sql!(
CREATE TABLE welcome_pages ( CREATE TABLE welcome_pages (
workspace_id INTEGER, workspace_id INTEGER,
item_id INTEGER UNIQUE, item_id INTEGER UNIQUE,
@ -436,11 +430,10 @@ mod persistence {
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE ON DELETE CASCADE
) STRICT; ) STRICT;
)]); ),
];
} }
db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]);
impl WelcomePagesDb { impl WelcomePagesDb {
query! { query! {
pub async fn save_welcome_page( pub async fn save_welcome_page(

View file

@ -4,11 +4,11 @@ use anyhow::Context as _;
use collections::{BTreeSet, HashMap, HashSet, hash_map}; use collections::{BTreeSet, HashMap, HashSet, hash_map};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::{ use editor::{
AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorSettings, ExcerptId, AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange,
ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects, ShowScrollbar, MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects,
display_map::ToDisplayPoint, display_map::ToDisplayPoint,
items::{entry_git_aware_label_color, entry_label_color}, items::{entry_git_aware_label_color, entry_label_color},
scroll::{Autoscroll, ScrollAnchor, ScrollbarAutoHide}, scroll::{Autoscroll, ScrollAnchor},
}; };
use file_icons::FileIcons; use file_icons::FileIcons;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
@ -45,19 +45,18 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use smol::channel; use smol::channel;
use theme::{SyntaxTheme, ThemeSettings}; use theme::{SyntaxTheme, ThemeSettings};
use ui::{DynamicSpacing, IndentGuideColors, IndentGuideLayout}; use ui::{
ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, DynamicSpacing, FluentBuilder,
HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, IndentGuideColors,
IndentGuideLayout, Label, LabelCommon, ListItem, ScrollAxes, Scrollbars, StyledExt,
StyledTypography, Toggleable, Tooltip, WithScrollbar, h_flex, v_flex,
};
use util::{RangeExt, ResultExt, TryFutureExt, debug_panic}; use util::{RangeExt, ResultExt, TryFutureExt, debug_panic};
use workspace::{ use workspace::{
OpenInTerminal, WeakItemHandle, Workspace, OpenInTerminal, WeakItemHandle, Workspace,
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
item::ItemHandle, item::ItemHandle,
searchable::{SearchEvent, SearchableItem}, searchable::{SearchEvent, SearchableItem},
ui::{
ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder, HighlightedLabel,
Icon, IconButton, IconButtonShape, IconName, IconSize, Label, LabelCommon, ListItem,
Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable, Tooltip, h_flex,
v_flex,
},
}; };
use worktree::{Entry, ProjectEntryId, WorktreeId}; use worktree::{Entry, ProjectEntryId, WorktreeId};
@ -125,10 +124,6 @@ pub struct OutlinePanel {
cached_entries: Vec<CachedEntry>, cached_entries: Vec<CachedEntry>,
filter_editor: Entity<Editor>, filter_editor: Entity<Editor>,
mode: ItemsDisplayMode, mode: ItemsDisplayMode,
show_scrollbar: bool,
vertical_scrollbar_state: ScrollbarState,
horizontal_scrollbar_state: ScrollbarState,
hide_scrollbar_task: Option<Task<()>>,
max_width_item_index: Option<usize>, max_width_item_index: Option<usize>,
preserve_selection_on_buffer_fold_toggles: HashSet<BufferId>, preserve_selection_on_buffer_fold_toggles: HashSet<BufferId>,
pending_default_expansion_depth: Option<usize>, pending_default_expansion_depth: Option<usize>,
@ -752,10 +747,6 @@ impl OutlinePanel {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in); let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in);
let focus_out_subscription =
cx.on_focus_out(&focus_handle, window, |outline_panel, _, window, cx| {
outline_panel.hide_scrollbar(window, cx);
});
let workspace_subscription = cx.subscribe_in( let workspace_subscription = cx.subscribe_in(
&workspace &workspace
.weak_handle() .weak_handle()
@ -868,12 +859,6 @@ impl OutlinePanel {
workspace: workspace_handle, workspace: workspace_handle,
project, project,
fs: workspace.app_state().fs.clone(), fs: workspace.app_state().fs.clone(),
show_scrollbar: !Self::should_autohide_scrollbar(cx),
hide_scrollbar_task: None,
vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
.parent_entity(&cx.entity()),
horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
.parent_entity(&cx.entity()),
max_width_item_index: None, max_width_item_index: None,
scroll_handle, scroll_handle,
focus_handle, focus_handle,
@ -903,7 +888,6 @@ impl OutlinePanel {
settings_subscription, settings_subscription,
icons_subscription, icons_subscription,
focus_subscription, focus_subscription,
focus_out_subscription,
workspace_subscription, workspace_subscription,
filter_update_subscription, filter_update_subscription,
], ],
@ -4491,150 +4475,6 @@ impl OutlinePanel {
cx.notify(); cx.notify();
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !Self::should_show_scrollbar(cx)
|| !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
{
return None;
}
Some(
div()
.occlude()
.id("project-panel-vertical-scroll")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|outline_panel, _, window, cx| {
if !outline_panel.vertical_scrollbar_state.is_dragging()
&& !outline_panel.focus_handle.contains_focused(window, cx)
{
outline_panel.hide_scrollbar(window, cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.vertical_scrollbar_state.clone())),
)
}
fn render_horizontal_scrollbar(
&self,
_: &mut Window,
cx: &mut Context<Self>,
) -> Option<Stateful<Div>> {
if !Self::should_show_scrollbar(cx)
|| !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
{
return None;
}
Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| {
div()
.occlude()
.id("project-panel-horizontal-scroll")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|outline_panel, _, window, cx| {
if !outline_panel.horizontal_scrollbar_state.is_dragging()
&& !outline_panel.focus_handle.contains_focused(window, cx)
{
outline_panel.hide_scrollbar(window, cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.w_full()
.absolute()
.right_1()
.left_1()
.bottom_0()
.h(px(12.))
.cursor_default()
.child(scrollbar)
})
}
fn should_show_scrollbar(cx: &App) -> bool {
let show = OutlinePanelSettings::get_global(cx)
.scrollbar
.show
.unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
match show {
ShowScrollbar::Auto => true,
ShowScrollbar::System => true,
ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
}
}
fn should_autohide_scrollbar(cx: &App) -> bool {
let show = OutlinePanelSettings::get_global(cx)
.scrollbar
.show
.unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
match show {
ShowScrollbar::Auto => true,
ShowScrollbar::System => cx
.try_global::<ScrollbarAutoHide>()
.map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
ShowScrollbar::Always => false,
ShowScrollbar::Never => true,
}
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
if !Self::should_autohide_scrollbar(cx) {
return;
}
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(cx, |panel, cx| {
panel.show_scrollbar = false;
cx.notify();
})
.log_err();
}))
}
fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 { fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
let item_text_chars = match entry { let item_text_chars = match entry {
PanelEntry::Fs(FsEntry::ExternalFile(external)) => self PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
@ -4690,7 +4530,7 @@ impl OutlinePanel {
indent_size: f32, indent_size: f32,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Div { ) -> impl IntoElement {
let contents = if self.cached_entries.is_empty() { let contents = if self.cached_entries.is_empty() {
let header = if self.updating_fs_entries || self.updating_cached_entries { let header = if self.updating_fs_entries || self.updating_cached_entries {
None None
@ -4844,17 +4684,20 @@ impl OutlinePanel {
}), }),
) )
}) })
.custom_scrollbars(
Scrollbars::for_settings::<OutlinePanelSettings>()
.tracked_scroll_handle(self.scroll_handle.clone())
.with_track_along(ScrollAxes::Horizontal)
.notify_content(),
window,
cx,
)
}; };
v_flex() v_flex()
.flex_shrink() .flex_shrink()
.size_full() .size_full()
.child(list_contents.size_full().flex_shrink()) .child(list_contents.size_full().flex_shrink())
.children(self.render_vertical_scrollbar(cx))
.when_some(
self.render_horizontal_scrollbar(window, cx),
|this, scrollbar| this.pb_4().child(scrollbar),
)
} }
.children(self.context_menu.as_ref().map(|(menu, position, _)| { .children(self.context_menu.as_ref().map(|(menu, position, _)| {
deferred( deferred(
@ -5121,15 +4964,6 @@ impl Render for OutlinePanel {
.size_full() .size_full()
.overflow_hidden() .overflow_hidden()
.relative() .relative()
.on_hover(cx.listener(|this, hovered, window, cx| {
if *hovered {
this.show_scrollbar = true;
this.hide_scrollbar_task.take();
cx.notify();
} else if !this.focus_handle.contains_focused(window, cx) {
this.hide_scrollbar(window, cx);
}
}))
.key_context(self.dispatch_context(window, cx)) .key_context(self.dispatch_context(window, cx))
.on_action(cx.listener(Self::open_selected_entry)) .on_action(cx.listener(Self::open_selected_entry))
.on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::cancel))

View file

@ -1,8 +1,9 @@
use editor::ShowScrollbar; use editor::EditorSettings;
use gpui::Pixels; use gpui::{App, Pixels};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources}; use settings::{Settings, SettingsSources};
use ui::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar};
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
@ -115,6 +116,14 @@ pub struct OutlinePanelSettingsContent {
pub expand_outlines_with_depth: Option<usize>, pub expand_outlines_with_depth: Option<usize>,
} }
impl ScrollbarVisibilitySetting for OutlinePanelSettings {
fn scrollbar_visibility(&self, cx: &App) -> ShowScrollbar {
self.scrollbar
.show
.unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
}
}
impl Settings for OutlinePanelSettings { impl Settings for OutlinePanelSettings {
const KEY: Option<&'static str> = Some("outline_panel"); const KEY: Option<&'static str> = Some("outline_panel");

View file

@ -11,17 +11,17 @@ use editor::{
use gpui::{ use gpui::{
Action, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Action, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
Focusable, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Render, Focusable, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Render,
ScrollStrategy, Stateful, Task, UniformListScrollHandle, Window, actions, div, list, ScrollStrategy, Task, UniformListScrollHandle, Window, actions, div, list, prelude::*,
prelude::*, uniform_list, uniform_list,
}; };
use head::Head; use head::Head;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use std::{ops::Range, sync::Arc, time::Duration}; use std::{ops::Range, sync::Arc, time::Duration};
use ui::{ use ui::{
Color, Divider, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing, ScrollAxes, Scrollbars, WithScrollbar,
prelude::*, v_flex,
}; };
use util::ResultExt;
use workspace::ModalView; use workspace::ModalView;
enum ElementContainer { enum ElementContainer {
@ -65,13 +65,8 @@ pub struct Picker<D: PickerDelegate> {
width: Option<Length>, width: Option<Length>,
widest_item: Option<usize>, widest_item: Option<usize>,
max_height: Option<Length>, max_height: Option<Length>,
focus_handle: FocusHandle,
/// An external control to display a scrollbar in the `Picker`. /// An external control to display a scrollbar in the `Picker`.
show_scrollbar: bool, show_scrollbar: bool,
/// An internal state that controls whether to show the scrollbar based on the user's focus.
scrollbar_visibility: bool,
scrollbar_state: ScrollbarState,
hide_scrollbar_task: Option<Task<()>>,
/// Whether the `Picker` is rendered as a self-contained modal. /// Whether the `Picker` is rendered as a self-contained modal.
/// ///
/// Set this to `false` when rendering the `Picker` as part of a larger modal. /// Set this to `false` when rendering the `Picker` as part of a larger modal.
@ -293,13 +288,6 @@ impl<D: PickerDelegate> Picker<D> {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let element_container = Self::create_element_container(container); let element_container = Self::create_element_container(container);
let scrollbar_state = match &element_container {
ElementContainer::UniformList(scroll_handle) => {
ScrollbarState::new(scroll_handle.clone())
}
ElementContainer::List(state) => ScrollbarState::new(state.clone()),
};
let focus_handle = cx.focus_handle();
let mut this = Self { let mut this = Self {
delegate, delegate,
head, head,
@ -309,12 +297,8 @@ impl<D: PickerDelegate> Picker<D> {
width: None, width: None,
widest_item: None, widest_item: None,
max_height: Some(rems(18.).into()), max_height: Some(rems(18.).into()),
focus_handle,
show_scrollbar: false, show_scrollbar: false,
scrollbar_visibility: true,
scrollbar_state,
is_modal: true, is_modal: true,
hide_scrollbar_task: None,
}; };
this.update_matches("".to_string(), window, cx); this.update_matches("".to_string(), window, cx);
// give the delegate 4ms to render the first set of suggestions. // give the delegate 4ms to render the first set of suggestions.
@ -790,67 +774,6 @@ impl<D: PickerDelegate> Picker<D> {
} }
} }
} }
fn hide_scrollbar(&mut self, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(cx, |panel, cx| {
panel.scrollbar_visibility = false;
cx.notify();
})
.log_err();
}))
}
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !self.show_scrollbar
|| !(self.scrollbar_visibility || self.scrollbar_state.is_dragging())
{
return None;
}
Some(
div()
.occlude()
.id("picker-scroll")
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.on_mouse_move(cx.listener(|_, _, _window, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _window, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _window, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|picker, _, window, cx| {
if !picker.scrollbar_state.is_dragging()
&& !picker.focus_handle.contains_focused(window, cx)
{
picker.hide_scrollbar(cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
}
} }
impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {} impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
@ -900,17 +823,12 @@ impl<D: PickerDelegate> Render for Picker<D> {
.overflow_hidden() .overflow_hidden()
.children(self.delegate.render_header(window, cx)) .children(self.delegate.render_header(window, cx))
.child(self.render_element_container(cx)) .child(self.render_element_container(cx))
.on_hover(cx.listener(|this, hovered, window, cx| { .when(self.show_scrollbar, |this| {
if *hovered { this.custom_scrollbars(
this.scrollbar_visibility = true; Scrollbars::new(ScrollAxes::Vertical).width_sm(),
this.hide_scrollbar_task.take(); window,
cx.notify(); cx,
} else if !this.focus_handle.contains_focused(window, cx) { )
this.hide_scrollbar(cx);
}
}))
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
div.child(scrollbar)
}), }),
) )
}) })

View file

@ -7,12 +7,11 @@ use collections::{BTreeSet, HashMap, hash_map};
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::{ use editor::{
Editor, EditorEvent, EditorSettings, ShowScrollbar, Editor, EditorEvent,
items::{ items::{
entry_diagnostic_aware_icon_decoration_and_color, entry_diagnostic_aware_icon_decoration_and_color,
entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color, entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color,
}, },
scroll::ScrollbarAutoHide,
}; };
use file_icons::FileIcons; use file_icons::FileIcons;
use git::status::GitSummary; use git::status::GitSummary;
@ -59,7 +58,8 @@ use theme::ThemeSettings;
use ui::{ use ui::{
Color, ContextMenu, DecoratedIcon, Divider, Icon, IconDecoration, IconDecorationKind, Color, ContextMenu, DecoratedIcon, Divider, Icon, IconDecoration, IconDecorationKind,
IndentGuideColors, IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, IndentGuideColors, IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing,
ScrollableHandle, Scrollbar, ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate, Tooltip, WithScrollbar, prelude::*,
v_flex,
}; };
use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths}; use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths};
use workspace::{ use workspace::{
@ -109,10 +109,6 @@ pub struct ProjectPanel {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
width: Option<Pixels>, width: Option<Pixels>,
pending_serialization: Task<Option<()>>, pending_serialization: Task<Option<()>>,
show_scrollbar: bool,
vertical_scrollbar_state: ScrollbarState,
horizontal_scrollbar_state: ScrollbarState,
hide_scrollbar_task: Option<Task<()>>,
diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>, diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>,
max_width_item_index: Option<usize>, max_width_item_index: Option<usize>,
diagnostic_summary_update: Task<()>, diagnostic_summary_update: Task<()>,
@ -428,7 +424,6 @@ impl ProjectPanel {
cx.on_focus(&focus_handle, window, Self::focus_in).detach(); cx.on_focus(&focus_handle, window, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.focus_out(window, cx); this.focus_out(window, cx);
this.hide_scrollbar(window, cx);
}) })
.detach(); .detach();
@ -619,12 +614,6 @@ impl ProjectPanel {
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
width: None, width: None,
pending_serialization: Task::ready(None), pending_serialization: Task::ready(None),
show_scrollbar: !Self::should_autohide_scrollbar(cx),
hide_scrollbar_task: None,
vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
.parent_entity(&cx.entity()),
horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
.parent_entity(&cx.entity()),
max_width_item_index: None, max_width_item_index: None,
diagnostics: Default::default(), diagnostics: Default::default(),
diagnostic_summary_update: Task::ready(()), diagnostic_summary_update: Task::ready(()),
@ -4089,7 +4078,6 @@ impl ProjectPanel {
.when(!is_sticky, |this| { .when(!is_sticky, |this| {
this this
.when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
.when(settings.drag_and_drop, |this| this
.on_drag_move::<ExternalPaths>(cx.listener( .on_drag_move::<ExternalPaths>(cx.listener(
move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| { move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
let is_current_target = this.drag_target_entry.as_ref() let is_current_target = this.drag_target_entry.as_ref()
@ -4223,7 +4211,7 @@ impl ProjectPanel {
} }
this.drag_onto(selections, entry_id, kind.is_file(), window, cx); this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
}), }),
)) )
}) })
.on_mouse_down( .on_mouse_down(
MouseButton::Left, MouseButton::Left,
@ -4434,7 +4422,6 @@ impl ProjectPanel {
div() div()
.when(!is_sticky, |div| { .when(!is_sticky, |div| {
div div
.when(settings.drag_and_drop, |div| div
.on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
this.hover_scroll_task.take(); this.hover_scroll_task.take();
this.drag_target_entry = None; this.drag_target_entry = None;
@ -4466,7 +4453,7 @@ impl ProjectPanel {
} }
}, },
))) ))
}) })
.child( .child(
Label::new(DELIMITER.clone()) Label::new(DELIMITER.clone())
@ -4486,7 +4473,6 @@ impl ProjectPanel {
.when(index != components_len - 1, |div|{ .when(index != components_len - 1, |div|{
let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
div div
.when(settings.drag_and_drop, |div| div
.on_drag_move(cx.listener( .on_drag_move(cx.listener(
move |this, event: &DragMoveEvent<DraggedSelection>, _, _| { move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
if event.bounds.contains(&event.event.position) { if event.bounds.contains(&event.event.position) {
@ -4524,7 +4510,7 @@ impl ProjectPanel {
target.index == index target.index == index
), |this| { ), |this| {
this.bg(item_colors.drag_over) this.bg(item_colors.drag_over)
})) })
}) })
}) })
.on_click(cx.listener(move |this, _, _, cx| { .on_click(cx.listener(move |this, _, _, cx| {
@ -4710,103 +4696,6 @@ impl ProjectPanel {
} }
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !Self::should_show_scrollbar(cx)
|| !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
{
return None;
}
Some(
div()
.occlude()
.id("project-panel-vertical-scroll")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
if !this.vertical_scrollbar_state.is_dragging()
&& !this.focus_handle.contains_focused(window, cx)
{
this.hide_scrollbar(window, cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_1()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(
// percentage as f32..end_offset as f32,
self.vertical_scrollbar_state.clone(),
)),
)
}
fn render_horizontal_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !Self::should_show_scrollbar(cx)
|| !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
{
return None;
}
Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| {
div()
.occlude()
.id("project-panel-horizontal-scroll")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
if !this.horizontal_scrollbar_state.is_dragging()
&& !this.focus_handle.contains_focused(window, cx)
{
this.hide_scrollbar(window, cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.w_full()
.absolute()
.right_1()
.left_1()
.bottom_1()
.h(px(12.))
.cursor_default()
.child(scrollbar)
})
}
fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext { fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
let mut dispatch_context = KeyContext::new_with_defaults(); let mut dispatch_context = KeyContext::new_with_defaults();
dispatch_context.add("ProjectPanel"); dispatch_context.add("ProjectPanel");
@ -4822,52 +4711,6 @@ impl ProjectPanel {
dispatch_context dispatch_context
} }
fn should_show_scrollbar(cx: &App) -> bool {
let show = ProjectPanelSettings::get_global(cx)
.scrollbar
.show
.unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
match show {
ShowScrollbar::Auto => true,
ShowScrollbar::System => true,
ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
}
}
fn should_autohide_scrollbar(cx: &App) -> bool {
let show = ProjectPanelSettings::get_global(cx)
.scrollbar
.show
.unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
match show {
ShowScrollbar::Auto => true,
ShowScrollbar::System => cx
.try_global::<ScrollbarAutoHide>()
.map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
ShowScrollbar::Always => false,
ShowScrollbar::Never => true,
}
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
if !Self::should_autohide_scrollbar(cx) {
return;
}
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(cx, |panel, cx| {
panel.show_scrollbar = false;
cx.notify();
})
.log_err();
}))
}
fn reveal_entry( fn reveal_entry(
&mut self, &mut self,
project: Entity<Project>, project: Entity<Project>,
@ -5032,8 +4875,7 @@ impl ProjectPanel {
sticky_parents.reverse(); sticky_parents.reverse();
let panel_settings = ProjectPanelSettings::get_global(cx); let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status;
let git_status_enabled = panel_settings.git_status;
let root_name = OsStr::new(worktree.root_name()); let root_name = OsStr::new(worktree.root_name());
let git_summaries_by_id = if git_status_enabled { let git_summaries_by_id = if git_status_enabled {
@ -5117,11 +4959,11 @@ impl Render for ProjectPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let has_worktree = !self.visible_entries.is_empty(); let has_worktree = !self.visible_entries.is_empty();
let project = self.project.read(cx); let project = self.project.read(cx);
let panel_settings = ProjectPanelSettings::get_global(cx); let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
let indent_size = panel_settings.indent_size; let show_indent_guides =
let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always; ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
let show_sticky_entries = { let show_sticky_entries = {
if panel_settings.sticky_scroll { if ProjectPanelSettings::get_global(cx).sticky_scroll {
let is_scrollable = self.scroll_handle.is_scrollable(); let is_scrollable = self.scroll_handle.is_scrollable();
let is_scrolled = self.scroll_handle.offset().y < px(0.); let is_scrolled = self.scroll_handle.offset().y < px(0.);
is_scrollable && is_scrolled is_scrollable && is_scrolled
@ -5209,10 +5051,8 @@ impl Render for ProjectPanel {
h_flex() h_flex()
.id("project-panel") .id("project-panel")
.group("project-panel") .group("project-panel")
.when(panel_settings.drag_and_drop, |this| { .on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>)) .on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
.on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
})
.size_full() .size_full()
.relative() .relative()
.on_modifiers_changed(cx.listener( .on_modifiers_changed(cx.listener(
@ -5220,15 +5060,6 @@ impl Render for ProjectPanel {
this.refresh_drag_cursor_style(&event.modifiers, window, cx); this.refresh_drag_cursor_style(&event.modifiers, window, cx);
}, },
)) ))
.on_hover(cx.listener(|this, hovered, window, cx| {
if *hovered {
this.show_scrollbar = true;
this.hide_scrollbar_task.take();
cx.notify();
} else if !this.focus_handle.contains_focused(window, cx) {
this.hide_scrollbar(window, cx);
}
}))
.on_click(cx.listener(|this, event, _, cx| { .on_click(cx.listener(|this, event, _, cx| {
if matches!(event, gpui::ClickEvent::Keyboard(_)) { if matches!(event, gpui::ClickEvent::Keyboard(_)) {
return; return;
@ -5489,10 +5320,14 @@ impl Render for ProjectPanel {
.with_width_from_item(self.max_width_item_index) .with_width_from_item(self.max_width_item_index)
.track_scroll(self.scroll_handle.clone()), .track_scroll(self.scroll_handle.clone()),
) )
.children(self.render_vertical_scrollbar(cx)) .custom_scrollbars(
.when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| { Scrollbars::for_settings::<ProjectPanelSettings>()
this.pb_4().child(scrollbar) .tracked_scroll_handle(self.scroll_handle.clone())
}) .with_track_along(ScrollAxes::Horizontal)
.notify_content(),
window,
cx,
)
.children(self.context_menu.as_ref().map(|(menu, position, _)| { .children(self.context_menu.as_ref().map(|(menu, position, _)| {
deferred( deferred(
anchored() anchored()
@ -5550,32 +5385,30 @@ impl Render for ProjectPanel {
})), })),
) )
.when(is_local, |div| { .when(is_local, |div| {
div.when(panel_settings.drag_and_drop, |div| { div.drag_over::<ExternalPaths>(|style, _, _, cx| {
div.drag_over::<ExternalPaths>(|style, _, _, cx| { style.bg(cx.theme().colors().drop_target_background)
style.bg(cx.theme().colors().drop_target_background)
})
.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();
},
))
}) })
} }
} }

View file

@ -1,8 +1,9 @@
use editor::ShowScrollbar; use editor::EditorSettings;
use gpui::Pixels; use gpui::Pixels;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsSources}; use settings::{Settings, SettingsSources};
use ui::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar};
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
@ -47,7 +48,6 @@ pub struct ProjectPanelSettings {
pub scrollbar: ScrollbarSettings, pub scrollbar: ScrollbarSettings,
pub show_diagnostics: ShowDiagnostics, pub show_diagnostics: ShowDiagnostics,
pub hide_root: bool, pub hide_root: bool,
pub drag_and_drop: bool,
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@ -161,10 +161,14 @@ pub struct ProjectPanelSettingsContent {
/// ///
/// Default: true /// Default: true
pub sticky_scroll: Option<bool>, pub sticky_scroll: Option<bool>,
/// Whether to enable drag-and-drop operations in the project panel. }
///
/// Default: true impl ScrollbarVisibilitySetting for ProjectPanelSettings {
pub drag_and_drop: Option<bool>, fn scrollbar_visibility(&self, cx: &ui::App) -> ShowScrollbar {
self.scrollbar
.show
.unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
}
} }
impl Settings for ProjectPanelSettings { impl Settings for ProjectPanelSettings {

View file

@ -1,4 +1,3 @@
use std::any::Any;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::path::PathBuf; use std::path::PathBuf;
@ -37,9 +36,10 @@ use settings::watch_config_file;
use smol::stream::StreamExt as _; use smol::stream::StreamExt as _;
use ui::Navigable; use ui::Navigable;
use ui::NavigableEntry; use ui::NavigableEntry;
use ui::WithScrollbar;
use ui::{ use ui::{
IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Section, Tooltip,
Section, Tooltip, prelude::*, prelude::*,
}; };
use util::{ use util::{
ResultExt, ResultExt,
@ -297,7 +297,7 @@ impl RemoteEntry {
#[derive(Clone)] #[derive(Clone)]
struct DefaultState { struct DefaultState {
scrollbar: ScrollbarState, scroll_handle: ScrollHandle,
add_new_server: NavigableEntry, add_new_server: NavigableEntry,
servers: Vec<RemoteEntry>, servers: Vec<RemoteEntry>,
} }
@ -305,7 +305,6 @@ struct DefaultState {
impl DefaultState { impl DefaultState {
fn new(ssh_config_servers: &BTreeSet<SharedString>, cx: &mut App) -> Self { fn new(ssh_config_servers: &BTreeSet<SharedString>, cx: &mut App) -> Self {
let handle = ScrollHandle::new(); let handle = ScrollHandle::new();
let scrollbar = ScrollbarState::new(handle.clone());
let add_new_server = NavigableEntry::new(&handle, cx); let add_new_server = NavigableEntry::new(&handle, cx);
let ssh_settings = SshSettings::get_global(cx); let ssh_settings = SshSettings::get_global(cx);
@ -346,7 +345,7 @@ impl DefaultState {
} }
Self { Self {
scrollbar, scroll_handle: handle,
add_new_server, add_new_server,
servers, servers,
} }
@ -1449,7 +1448,6 @@ impl RemoteServerProjects {
} }
} }
let scroll_state = state.scrollbar.parent_entity(&cx.entity());
let connect_button = div() let connect_button = div()
.id("ssh-connect-new-server-container") .id("ssh-connect-new-server-container")
.track_focus(&state.add_new_server.focus_handle) .track_focus(&state.add_new_server.focus_handle)
@ -1480,17 +1478,12 @@ impl RemoteServerProjects {
cx.notify(); cx.notify();
})); }));
let handle = &**scroll_state.scroll_handle() as &dyn Any;
let Some(scroll_handle) = handle.downcast_ref::<ScrollHandle>() else {
unreachable!()
};
let mut modal_section = Navigable::new( let mut modal_section = Navigable::new(
v_flex() v_flex()
.track_focus(&self.focus_handle(cx)) .track_focus(&self.focus_handle(cx))
.id("ssh-server-list") .id("ssh-server-list")
.overflow_y_scroll() .overflow_y_scroll()
.track_scroll(scroll_handle) .track_scroll(&state.scroll_handle)
.size_full() .size_full()
.child(connect_button) .child(connect_button)
.child( .child(
@ -1585,17 +1578,7 @@ impl RemoteServerProjects {
) )
.size_full(), .size_full(),
) )
.child( .vertical_scrollbar_for(state.scroll_handle.clone(), window, cx),
div()
.occlude()
.h_full()
.absolute()
.top_1()
.bottom_1()
.right_1()
.w(px(8.))
.children(Scrollbar::vertical(scroll_state)),
),
), ),
) )
.into_any_element() .into_any_element()

View file

@ -445,7 +445,7 @@ impl SshSocket {
} }
async fn platform(&self) -> Result<SshPlatform> { async fn platform(&self) -> Result<SshPlatform> {
let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?; let uname = self.run_command("sh", &["-c", "uname -sm"]).await?;
let Some((os, arch)) = uname.split_once(" ") else { let Some((os, arch)) = uname.split_once(" ") else {
anyhow::bail!("unknown uname: {uname:?}") anyhow::bail!("unknown uname: {uname:?}")
}; };
@ -476,7 +476,7 @@ impl SshSocket {
} }
async fn shell(&self) -> String { async fn shell(&self) -> String {
match self.run_command("sh", &["-lc", "echo $SHELL"]).await { match self.run_command("sh", &["-c", "echo $SHELL"]).await {
Ok(shell) => shell.trim().to_owned(), Ok(shell) => shell.trim().to_owned(),
Err(e) => { Err(e) => {
log::error!("Failed to get shell: {e}"); log::error!("Failed to get shell: {e}");
@ -1533,7 +1533,7 @@ impl RemoteConnection for SshRemoteConnection {
let ssh_proxy_process = match self let ssh_proxy_process = match self
.socket .socket
.ssh_command("sh", &["-lc", &start_proxy_command]) .ssh_command("sh", &["-c", &start_proxy_command])
// IMPORTANT: we kill this process when we drop the task that uses it. // IMPORTANT: we kill this process when we drop the task that uses it.
.kill_on_drop(true) .kill_on_drop(true)
.spawn() .spawn()
@ -1910,7 +1910,7 @@ impl SshRemoteConnection {
.run_command( .run_command(
"sh", "sh",
&[ &[
"-lc", "-c",
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
], ],
) )
@ -1988,7 +1988,7 @@ impl SshRemoteConnection {
.run_command( .run_command(
"sh", "sh",
&[ &[
"-lc", "-c",
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
], ],
) )
@ -2036,7 +2036,7 @@ impl SshRemoteConnection {
dst_path = &dst_path.to_string() dst_path = &dst_path.to_string()
) )
}; };
self.socket.run_command("sh", &["-lc", &script]).await?; self.socket.run_command("sh", &["-c", &script]).await?;
Ok(()) Ok(())
} }

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -11,12 +11,12 @@ use editor::{CompletionProvider, Editor, EditorEvent};
use fs::Fs; use fs::Fs;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter,
EventEmitter, FocusHandle, Focusable, Global, IsZero, FocusHandle, Focusable, Global, IsZero,
KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point, KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful,
ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred,
TextStyleRefinement, WeakEntity, actions, anchored, deferred, div, div,
}; };
use language::{Language, LanguageConfig, ToOffset as _}; use language::{Language, LanguageConfig, ToOffset as _};
use notifications::status_toast::{StatusToast, ToastIcon}; use notifications::status_toast::{StatusToast, ToastIcon};
@ -174,7 +174,7 @@ impl FilterState {
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
struct ActionMapping { struct ActionMapping {
keystrokes: Vec<KeybindingKeystroke>, keystrokes: Vec<Keystroke>,
context: Option<SharedString>, context: Option<SharedString>,
} }
@ -236,7 +236,7 @@ struct ConflictState {
} }
type ConflictKeybindMapping = HashMap< type ConflictKeybindMapping = HashMap<
Vec<KeybindingKeystroke>, Vec<Keystroke>,
Vec<( Vec<(
Option<gpui::KeyBindingContextPredicate>, Option<gpui::KeyBindingContextPredicate>,
Vec<ConflictOrigin>, Vec<ConflictOrigin>,
@ -414,21 +414,19 @@ impl Focusable for KeymapEditor {
} }
} }
/// Helper function to check if two keystroke sequences match exactly /// Helper function to check if two keystroke sequences match exactly
fn keystrokes_match_exactly( fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool {
keystrokes1: &[KeybindingKeystroke],
keystrokes2: &[KeybindingKeystroke],
) -> bool {
keystrokes1.len() == keystrokes2.len() keystrokes1.len() == keystrokes2.len()
&& keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| { && keystrokes1
k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers .iter()
}) .zip(keystrokes2)
.all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers)
} }
impl KeymapEditor { impl KeymapEditor {
fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let _keymap_subscription = let _keymap_subscription =
cx.observe_global_in::<KeymapEventChannel>(window, Self::on_keymap_changed); cx.observe_global_in::<KeymapEventChannel>(window, Self::on_keymap_changed);
let table_interaction_state = TableInteractionState::new(window, cx); let table_interaction_state = TableInteractionState::new(cx);
let keystroke_editor = cx.new(|cx| { let keystroke_editor = cx.new(|cx| {
let mut keystroke_editor = KeystrokeInput::new(None, window, cx); let mut keystroke_editor = KeystrokeInput::new(None, window, cx);
@ -511,7 +509,7 @@ impl KeymapEditor {
self.filter_editor.read(cx).text(cx) self.filter_editor.read(cx).text(cx)
} }
fn current_keystroke_query(&self, cx: &App) -> Vec<KeybindingKeystroke> { fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
match self.search_mode { match self.search_mode {
SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(), SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
SearchMode::Normal => Default::default(), SearchMode::Normal => Default::default(),
@ -532,7 +530,7 @@ impl KeymapEditor {
let keystroke_query = keystroke_query let keystroke_query = keystroke_query
.into_iter() .into_iter()
.map(|keystroke| keystroke.inner.unparse()) .map(|keystroke| keystroke.unparse())
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(" "); .join(" ");
@ -556,7 +554,7 @@ impl KeymapEditor {
async fn update_matches( async fn update_matches(
this: WeakEntity<Self>, this: WeakEntity<Self>,
action_query: String, action_query: String,
keystroke_query: Vec<KeybindingKeystroke>, keystroke_query: Vec<Keystroke>,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let action_query = command_palette::normalize_action_query(&action_query); let action_query = command_palette::normalize_action_query(&action_query);
@ -605,15 +603,13 @@ impl KeymapEditor {
{ {
let query = &keystroke_query[query_cursor]; let query = &keystroke_query[query_cursor];
let keystroke = &keystrokes[keystroke_cursor]; let keystroke = &keystrokes[keystroke_cursor];
let matches = query let matches =
.inner query.modifiers.is_subset_of(&keystroke.modifiers)
.modifiers && ((query.key.is_empty()
.is_subset_of(&keystroke.inner.modifiers) || query.key == keystroke.key)
&& ((query.inner.key.is_empty() && query.key_char.as_ref().is_none_or(
|| query.inner.key == keystroke.inner.key) |q_kc| q_kc == &keystroke.key,
&& query.inner.key_char.as_ref().is_none_or( ));
|q_kc| q_kc == &keystroke.inner.key,
));
if matches { if matches {
found_count += 1; found_count += 1;
query_cursor += 1; query_cursor += 1;
@ -682,7 +678,7 @@ impl KeymapEditor {
.map(KeybindSource::from_meta) .map(KeybindSource::from_meta)
.unwrap_or(KeybindSource::Unknown); .unwrap_or(KeybindSource::Unknown);
let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx); let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx) let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
.vim_mode(source == KeybindSource::Vim); .vim_mode(source == KeybindSource::Vim);
@ -784,9 +780,8 @@ impl KeymapEditor {
match previous_edit { match previous_edit {
// should remove scroll from process_query // should remove scroll from process_query
PreviousEdit::ScrollBarOffset(offset) => { PreviousEdit::ScrollBarOffset(offset) => {
this.table_interaction_state.update(cx, |table, _| { this.table_interaction_state
table.set_scrollbar_offset(Axis::Vertical, offset) .update(cx, |table, _| table.set_scroll_offset(offset))
})
// set selected index and scroll // set selected index and scroll
} }
PreviousEdit::Keybinding { PreviousEdit::Keybinding {
@ -815,9 +810,8 @@ impl KeymapEditor {
cx, cx,
); );
} else { } else {
this.table_interaction_state.update(cx, |table, _| { this.table_interaction_state
table.set_scrollbar_offset(Axis::Vertical, fallback) .update(cx, |table, _| table.set_scroll_offset(fallback));
});
} }
cx.notify(); cx.notify();
} }
@ -1202,15 +1196,10 @@ impl KeymapEditor {
}; };
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size(); let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
self.previous_edit = Some(PreviousEdit::ScrollBarOffset( self.previous_edit = Some(PreviousEdit::ScrollBarOffset(
self.table_interaction_state self.table_interaction_state.read(cx).scroll_offset(),
.read(cx)
.get_scrollbar_offset(Axis::Vertical),
)); ));
let keyboard_mapper = cx.keyboard_mapper().clone(); cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
cx.spawn(async move |_, _| { .detach_and_notify_err(window, cx);
remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await
})
.detach_and_notify_err(window, cx);
} }
fn copy_context_to_clipboard( fn copy_context_to_clipboard(
@ -1429,7 +1418,7 @@ impl ProcessedBinding {
.map(|keybind| keybind.get_action_mapping()) .map(|keybind| keybind.get_action_mapping())
} }
fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> { fn keystrokes(&self) -> Option<&[Keystroke]> {
self.ui_key_binding() self.ui_key_binding()
.map(|binding| binding.keystrokes.as_slice()) .map(|binding| binding.keystrokes.as_slice())
} }
@ -2227,7 +2216,7 @@ impl KeybindingEditorModal {
Ok(action_arguments) Ok(action_arguments)
} }
fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<KeybindingKeystroke>> { fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
let new_keystrokes = self let new_keystrokes = self
.keybind_editor .keybind_editor
.read_with(cx, |editor, _| editor.keystrokes().to_vec()); .read_with(cx, |editor, _| editor.keystrokes().to_vec());
@ -2323,7 +2312,6 @@ impl KeybindingEditorModal {
}).unwrap_or(Ok(()))?; }).unwrap_or(Ok(()))?;
let create = self.creating; let create = self.creating;
let keyboard_mapper = cx.keyboard_mapper().clone();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let action_name = existing_keybind.action().name; let action_name = existing_keybind.action().name;
@ -2336,7 +2324,6 @@ impl KeybindingEditorModal {
new_action_args.as_deref(), new_action_args.as_deref(),
&fs, &fs,
tab_size, tab_size,
keyboard_mapper.as_ref(),
) )
.await .await
{ {
@ -2346,10 +2333,7 @@ impl KeybindingEditorModal {
keymap.previous_edit = Some(PreviousEdit::Keybinding { keymap.previous_edit = Some(PreviousEdit::Keybinding {
action_mapping, action_mapping,
action_name, action_name,
fallback: keymap fallback: keymap.table_interaction_state.read(cx).scroll_offset(),
.table_interaction_state
.read(cx)
.get_scrollbar_offset(Axis::Vertical),
}); });
let status_toast = StatusToast::new( let status_toast = StatusToast::new(
format!("Saved edits to the {} action.", humanized_action_name), format!("Saved edits to the {} action.", humanized_action_name),
@ -2454,21 +2438,11 @@ impl KeybindingEditorModal {
} }
} }
fn remove_key_char( fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
KeybindingKeystroke { Keystroke {
inner, modifiers,
display_modifiers, key,
display_key, ..Default::default()
}: KeybindingKeystroke,
) -> KeybindingKeystroke {
KeybindingKeystroke {
inner: Keystroke {
modifiers: inner.modifiers,
key: inner.key,
key_char: None,
},
display_modifiers,
display_key,
} }
} }
@ -3011,7 +2985,6 @@ async fn save_keybinding_update(
new_args: Option<&str>, new_args: Option<&str>,
fs: &Arc<dyn Fs>, fs: &Arc<dyn Fs>,
tab_size: usize, tab_size: usize,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let keymap_contents = settings::KeymapFile::load_keymap_file(fs) let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
.await .await
@ -3054,13 +3027,9 @@ async fn save_keybinding_update(
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
let updated_keymap_contents = settings::KeymapFile::update_keybinding( let updated_keymap_contents =
operation, settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
keymap_contents, .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
tab_size,
keyboard_mapper,
)
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
fs.write( fs.write(
paths::keymap_file().as_path(), paths::keymap_file().as_path(),
updated_keymap_contents.as_bytes(), updated_keymap_contents.as_bytes(),
@ -3081,7 +3050,6 @@ async fn remove_keybinding(
existing: ProcessedBinding, existing: ProcessedBinding,
fs: &Arc<dyn Fs>, fs: &Arc<dyn Fs>,
tab_size: usize, tab_size: usize,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let Some(keystrokes) = existing.keystrokes() else { let Some(keystrokes) = existing.keystrokes() else {
anyhow::bail!("Cannot remove a keybinding that does not exist"); anyhow::bail!("Cannot remove a keybinding that does not exist");
@ -3105,13 +3073,9 @@ async fn remove_keybinding(
}; };
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
let updated_keymap_contents = settings::KeymapFile::update_keybinding( let updated_keymap_contents =
operation, settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
keymap_contents, .context("Failed to update keybinding")?;
tab_size,
keyboard_mapper,
)
.context("Failed to update keybinding")?;
fs.write( fs.write(
paths::keymap_file().as_path(), paths::keymap_file().as_path(),
updated_keymap_contents.as_bytes(), updated_keymap_contents.as_bytes(),
@ -3377,15 +3341,12 @@ impl SerializableItem for KeymapEditor {
} }
mod persistence { mod persistence {
use db::{query, sqlez::domain::Domain, sqlez_macros::sql}; use db::{define_connection, query, sqlez_macros::sql};
use workspace::WorkspaceDb; use workspace::WorkspaceDb;
pub struct KeybindingEditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); define_connection! {
pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
impl Domain for KeybindingEditorDb { &[sql!(
const NAME: &str = stringify!(KeybindingEditorDb);
const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE keybinding_editors ( CREATE TABLE keybinding_editors (
workspace_id INTEGER, workspace_id INTEGER,
item_id INTEGER UNIQUE, item_id INTEGER UNIQUE,
@ -3394,11 +3355,9 @@ mod persistence {
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE ON DELETE CASCADE
) STRICT; ) STRICT;
)]; )];
} }
db::static_connection!(KEYBINDING_EDITORS, KeybindingEditorDb, [WorkspaceDb]);
impl KeybindingEditorDb { impl KeybindingEditorDb {
query! { query! {
pub async fn save_keybinding_editor( pub async fn save_keybinding_editor(

View file

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

View file

@ -1,20 +1,20 @@
use std::{ops::Range, rc::Rc, time::Duration}; use std::{ops::Range, rc::Rc};
use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide}; use editor::EditorSettings;
use gpui::{ use gpui::{
AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId,
FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, Point, Stateful,
Stateful, Task, UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, UniformListScrollHandle, WeakEntity, transparent_black, uniform_list,
}; };
use itertools::intersperse_with; use itertools::intersperse_with;
use settings::Settings as _;
use ui::{ use ui::{
ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _,
StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex, StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, px,
single_example, v_flex,
}; };
const RESIZE_COLUMN_WIDTH: f32 = 8.0; const RESIZE_COLUMN_WIDTH: f32 = 8.0;
@ -56,136 +56,22 @@ impl<const COLS: usize> TableContents<COLS> {
pub struct TableInteractionState { pub struct TableInteractionState {
pub focus_handle: FocusHandle, pub focus_handle: FocusHandle,
pub scroll_handle: UniformListScrollHandle, pub scroll_handle: UniformListScrollHandle,
pub horizontal_scrollbar: ScrollbarProperties,
pub vertical_scrollbar: ScrollbarProperties,
} }
impl TableInteractionState { impl TableInteractionState {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> { pub fn new(cx: &mut App) -> Entity<Self> {
cx.new(|cx| { cx.new(|cx| Self {
let focus_handle = cx.focus_handle(); focus_handle: cx.focus_handle(),
scroll_handle: UniformListScrollHandle::new(),
cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, window, cx| {
this.hide_scrollbars(window, cx);
})
.detach();
let scroll_handle = UniformListScrollHandle::new();
let vertical_scrollbar = ScrollbarProperties {
axis: Axis::Vertical,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let horizontal_scrollbar = ScrollbarProperties {
axis: Axis::Horizontal,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let mut this = Self {
focus_handle,
scroll_handle,
horizontal_scrollbar,
vertical_scrollbar,
};
this.update_scrollbar_visibility(cx);
this
}) })
} }
pub fn get_scrollbar_offset(&self, axis: Axis) -> Point<Pixels> { pub fn scroll_offset(&self) -> Point<Pixels> {
match axis { self.scroll_handle.offset()
Axis::Vertical => self.vertical_scrollbar.state.scroll_handle().offset(),
Axis::Horizontal => self.horizontal_scrollbar.state.scroll_handle().offset(),
}
} }
pub fn set_scrollbar_offset(&self, axis: Axis, offset: Point<Pixels>) { pub fn set_scroll_offset(&self, offset: Point<Pixels>) {
match axis { self.scroll_handle.set_offset(offset);
Axis::Vertical => self
.vertical_scrollbar
.state
.scroll_handle()
.set_offset(offset),
Axis::Horizontal => self
.horizontal_scrollbar
.state
.scroll_handle()
.set_offset(offset),
}
}
fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) {
let show_setting = EditorSettings::get_global(cx).scrollbar.show;
let scroll_handle = self.scroll_handle.0.borrow();
let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
ShowScrollbar::Auto => true,
ShowScrollbar::System => cx
.try_global::<ScrollbarAutoHide>()
.map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
ShowScrollbar::Always => false,
ShowScrollbar::Never => false,
};
let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
(size.contents.width > size.item.width).then_some(size.contents.width)
});
// is there an item long enough that we should show a horizontal scrollbar?
let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
} else {
true
};
let show_scrollbar = match show_setting {
ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
};
let show_vertical = show_scrollbar;
let show_horizontal = item_wider_than_container && show_scrollbar;
let show_horizontal_track =
show_horizontal && matches!(show_setting, ShowScrollbar::Always);
// TODO: we probably should hide the scroll track when the list doesn't need to scroll
let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
self.vertical_scrollbar = ScrollbarProperties {
axis: self.vertical_scrollbar.axis,
state: self.vertical_scrollbar.state.clone(),
show_scrollbar: show_vertical,
show_track: show_vertical_track,
auto_hide: autohide(show_setting, cx),
hide_task: None,
};
self.horizontal_scrollbar = ScrollbarProperties {
axis: self.horizontal_scrollbar.axis,
state: self.horizontal_scrollbar.state.clone(),
show_scrollbar: show_horizontal,
show_track: show_horizontal_track,
auto_hide: autohide(show_setting, cx),
hide_task: None,
};
cx.notify();
}
fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.horizontal_scrollbar.hide(window, cx);
self.vertical_scrollbar.hide(window, cx);
} }
pub fn listener<E: ?Sized>( pub fn listener<E: ?Sized>(
@ -280,183 +166,6 @@ impl TableInteractionState {
.children(dividers) .children(dividers)
.into_any_element() .into_any_element()
} }
fn render_vertical_scrollbar_track(
this: &Entity<Self>,
parent: Div,
scroll_track_size: Pixels,
cx: &mut App,
) -> Div {
if !this.read(cx).vertical_scrollbar.show_track {
return parent;
}
let child = v_flex()
.h_full()
.flex_none()
.w(scroll_track_size)
.bg(cx.theme().colors().background)
.child(
div()
.size_full()
.flex_1()
.border_l_1()
.border_color(cx.theme().colors().border),
);
parent.child(child)
}
fn render_vertical_scrollbar(this: &Entity<Self>, parent: Div, cx: &mut App) -> Div {
if !this.read(cx).vertical_scrollbar.show_scrollbar {
return parent;
}
let child = div()
.id(("table-vertical-scrollbar", this.entity_id()))
.occlude()
.flex_none()
.h_full()
.cursor_default()
.absolute()
.right_0()
.top_0()
.bottom_0()
.w(px(12.))
.on_mouse_move(Self::listener(this, |_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
Self::listener(this, |this, _, window, cx| {
if !this.vertical_scrollbar.state.is_dragging()
&& !this.focus_handle.contains_focused(window, cx)
{
this.vertical_scrollbar.hide(window, cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_scroll_wheel(Self::listener(this, |_, _, _, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(
this.read(cx).vertical_scrollbar.state.clone(),
));
parent.child(child)
}
/// Renders the horizontal scrollbar.
///
/// The right offset is used to determine how far to the right the
/// scrollbar should extend to, useful for ensuring it doesn't collide
/// with the vertical scrollbar when visible.
fn render_horizontal_scrollbar(
this: &Entity<Self>,
parent: Div,
right_offset: Pixels,
cx: &mut App,
) -> Div {
if !this.read(cx).horizontal_scrollbar.show_scrollbar {
return parent;
}
let child = div()
.id(("table-horizontal-scrollbar", this.entity_id()))
.occlude()
.flex_none()
.w_full()
.cursor_default()
.absolute()
.bottom_neg_px()
.left_0()
.right_0()
.pr(right_offset)
.on_mouse_move(Self::listener(this, |_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
Self::listener(this, |this, _, window, cx| {
if !this.horizontal_scrollbar.state.is_dragging()
&& !this.focus_handle.contains_focused(window, cx)
{
this.horizontal_scrollbar.hide(window, cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(Self::listener(this, |_, _, _, cx| {
cx.notify();
}))
.children(Scrollbar::horizontal(
// percentage as f32..end_offset as f32,
this.read(cx).horizontal_scrollbar.state.clone(),
));
parent.child(child)
}
fn render_horizontal_scrollbar_track(
this: &Entity<Self>,
parent: Div,
scroll_track_size: Pixels,
cx: &mut App,
) -> Div {
if !this.read(cx).horizontal_scrollbar.show_track {
return parent;
}
let child = h_flex()
.w_full()
.h(scroll_track_size)
.flex_none()
.relative()
.child(
div()
.w_full()
.flex_1()
// for some reason the horizontal scrollbar is 1px
// taller than the vertical scrollbar??
.h(scroll_track_size - px(1.))
.bg(cx.theme().colors().background)
.border_t_1()
.border_color(cx.theme().colors().border),
)
.when(this.read(cx).vertical_scrollbar.show_track, |parent| {
parent
.child(
div()
.flex_none()
// -1px prevents a missing pixel between the two container borders
.w(scroll_track_size - px(1.))
.h_full(),
)
.child(
// HACK: Fill the missing 1px 🥲
div()
.absolute()
.right(scroll_track_size - px(1.))
.bottom(scroll_track_size - px(1.))
.size_px()
.bg(cx.theme().colors().border),
)
});
parent.child(child)
}
} }
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
@ -1054,17 +763,6 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
.and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial))) .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial)))
.map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial)); .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial));
let scroll_track_size = px(16.);
let h_scroll_offset = if interaction_state
.as_ref()
.is_some_and(|state| state.read(cx).vertical_scrollbar.show_scrollbar)
{
// magic number
px(3.)
} else {
px(0.)
};
let width = self.width; let width = self.width;
let no_rows_rendered = self.rows.is_empty(); let no_rows_rendered = self.rows.is_empty();
@ -1115,8 +813,8 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
}) })
} }
}) })
.child( .child({
div() let content = div()
.flex_grow() .flex_grow()
.w_full() .w_full()
.relative() .relative()
@ -1187,25 +885,21 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
) )
})) }))
}, },
) );
.when_some(interaction_state.as_ref(), |this, interaction_state| {
this.map(|this| { if let Some(state) = interaction_state.as_ref() {
TableInteractionState::render_vertical_scrollbar_track( content
interaction_state, .custom_scrollbars(
this, Scrollbars::for_settings::<EditorSettings>()
scroll_track_size, .tracked_scroll_handle(state.read(cx).scroll_handle.clone()),
cx, window,
) cx,
}) )
.map(|this| { .into_any_element()
TableInteractionState::render_vertical_scrollbar( } else {
interaction_state, content.into_any_element()
this, }
cx, })
)
})
}),
)
.when_some( .when_some(
no_rows_rendered no_rows_rendered
.then_some(self.empty_table_callback) .then_some(self.empty_table_callback)
@ -1220,52 +914,12 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
.child(callback(window, cx)), .child(callback(window, cx)),
) )
}, },
)
.when_some(
width.and(interaction_state.as_ref()),
|this, interaction_state| {
this.map(|this| {
TableInteractionState::render_horizontal_scrollbar_track(
interaction_state,
this,
scroll_track_size,
cx,
)
})
.map(|this| {
TableInteractionState::render_horizontal_scrollbar(
interaction_state,
this,
h_scroll_offset,
cx,
)
})
},
); );
if let Some(interaction_state) = interaction_state.as_ref() { if let Some(interaction_state) = interaction_state.as_ref() {
table table
.track_focus(&interaction_state.read(cx).focus_handle) .track_focus(&interaction_state.read(cx).focus_handle)
.id(("table", interaction_state.entity_id())) .id(("table", interaction_state.entity_id()))
.on_hover({
let interaction_state = interaction_state.downgrade();
move |hovered, window, cx| {
interaction_state
.update(cx, |interaction_state, cx| {
if *hovered {
interaction_state.horizontal_scrollbar.show(cx);
interaction_state.vertical_scrollbar.show(cx);
cx.notify();
} else if !interaction_state
.focus_handle
.contains_focused(window, cx)
{
interaction_state.hide_scrollbars(window, cx);
}
})
.ok();
}
})
.into_any_element() .into_any_element()
} else { } else {
table.into_any_element() table.into_any_element()
@ -1273,65 +927,6 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
} }
} }
// computed state related to how to render scrollbars
// one per axis
// on render we just read this off the keymap editor
// we update it when
// - settings change
// - on focus in, on focus out, on hover, etc.
#[derive(Debug)]
pub struct ScrollbarProperties {
axis: Axis,
show_scrollbar: bool,
show_track: bool,
auto_hide: bool,
hide_task: Option<Task<()>>,
state: ScrollbarState,
}
impl ScrollbarProperties {
// Shows the scrollbar and cancels any pending hide task
fn show(&mut self, cx: &mut Context<TableInteractionState>) {
if !self.auto_hide {
return;
}
self.show_scrollbar = true;
self.hide_task.take();
cx.notify();
}
fn hide(&mut self, window: &mut Window, cx: &mut Context<TableInteractionState>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
if !self.auto_hide {
return;
}
let axis = self.axis;
self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
if let Some(keymap_editor) = keymap_editor.upgrade() {
keymap_editor
.update(cx, |keymap_editor, cx| {
match axis {
Axis::Vertical => {
keymap_editor.vertical_scrollbar.show_scrollbar = false
}
Axis::Horizontal => {
keymap_editor.horizontal_scrollbar.show_scrollbar = false
}
}
cx.notify();
})
.ok();
}
}));
}
}
impl Component for Table<3> { impl Component for Table<3> {
fn scope() -> ComponentScope { fn scope() -> ComponentScope {
ComponentScope::Layout ComponentScope::Layout

View file

@ -1,12 +1,8 @@
use crate::connection::Connection; use crate::connection::Connection;
pub trait Domain: 'static { pub trait Domain: 'static {
const NAME: &str; fn name() -> &'static str;
const MIGRATIONS: &[&str]; fn migrations() -> &'static [&'static str];
fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool {
false
}
} }
pub trait Migrator: 'static { pub trait Migrator: 'static {
@ -21,11 +17,7 @@ impl Migrator for () {
impl<D: Domain> Migrator for D { impl<D: Domain> Migrator for D {
fn migrate(connection: &Connection) -> anyhow::Result<()> { fn migrate(connection: &Connection) -> anyhow::Result<()> {
connection.migrate( connection.migrate(Self::name(), Self::migrations())
Self::NAME,
Self::MIGRATIONS,
Self::should_allow_migration_change,
)
} }
} }

View file

@ -34,12 +34,7 @@ impl Connection {
/// Note: Unlike everything else in SQLez, migrations are run eagerly, without first /// 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 /// preparing the SQL statements. This makes it possible to do multi-statement schema
/// updates in a single string without running into prepare errors. /// updates in a single string without running into prepare errors.
pub fn migrate( pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> {
&self,
domain: &'static str,
migrations: &[&'static str],
mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool,
) -> Result<()> {
self.with_savepoint("migrating", || { self.with_savepoint("migrating", || {
// Setup the migrations table unconditionally // Setup the migrations table unconditionally
self.exec(indoc! {" self.exec(indoc! {"
@ -70,14 +65,9 @@ impl Connection {
&sqlformat::QueryParams::None, &sqlformat::QueryParams::None,
Default::default(), Default::default(),
); );
if completed_migration == migration if completed_migration == migration {
|| migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE")
{
// Migration already run. Continue // Migration already run. Continue
continue; continue;
} else if should_allow_migration_change(index, &completed_migration, &migration)
{
continue;
} else { } else {
anyhow::bail!(formatdoc! {" anyhow::bail!(formatdoc! {"
Migration changed for {domain} at step {index} Migration changed for {domain} at step {index}
@ -118,7 +108,6 @@ mod test {
a TEXT, a TEXT,
b TEXT b TEXT
)"}], )"}],
disallow_migration_change,
) )
.unwrap(); .unwrap();
@ -147,7 +136,6 @@ mod test {
d TEXT d TEXT
)"}, )"},
], ],
disallow_migration_change,
) )
.unwrap(); .unwrap();
@ -226,11 +214,7 @@ mod test {
// Run the migration verifying that the row got dropped // Run the migration verifying that the row got dropped
connection connection
.migrate( .migrate("test", &["DELETE FROM test_table"])
"test",
&["DELETE FROM test_table"],
disallow_migration_change,
)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
connection connection
@ -248,11 +232,7 @@ mod test {
// Run the same migration again and verify that the table was left unchanged // Run the same migration again and verify that the table was left unchanged
connection connection
.migrate( .migrate("test", &["DELETE FROM test_table"])
"test",
&["DELETE FROM test_table"],
disallow_migration_change,
)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
connection connection
@ -272,28 +252,27 @@ mod test {
.migrate( .migrate(
"test migration", "test migration",
&[ &[
"CREATE TABLE test (col INTEGER)", indoc! {"
"INSERT INTO test (col) VALUES (1)", CREATE TABLE test (
col INTEGER
)"},
indoc! {"
INSERT INTO test (col) VALUES (1)"},
], ],
disallow_migration_change,
) )
.unwrap(); .unwrap();
let mut migration_changed = false;
// Create another migration with the same domain but different steps // Create another migration with the same domain but different steps
let second_migration_result = connection.migrate( let second_migration_result = connection.migrate(
"test migration", "test migration",
&[ &[
"CREATE TABLE test (color INTEGER )", indoc! {"
"INSERT INTO test (color) VALUES (1)", CREATE TABLE test (
color INTEGER
)"},
indoc! {"
INSERT INTO test (color) VALUES (1)"},
], ],
|_, old, new| {
assert_eq!(old, "CREATE TABLE test (col INTEGER)");
assert_eq!(new, "CREATE TABLE test (color INTEGER)");
migration_changed = true;
false
},
); );
// Verify new migration returns error when run // Verify new migration returns error when run
@ -305,11 +284,7 @@ mod test {
let connection = Connection::open_memory(Some("test_create_alter_drop")); let connection = Connection::open_memory(Some("test_create_alter_drop"));
connection connection
.migrate( .migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"])
"first_migration",
&["CREATE TABLE table1(a TEXT) STRICT;"],
disallow_migration_change,
)
.unwrap(); .unwrap();
connection connection
@ -330,7 +305,6 @@ mod test {
ALTER TABLE table2 RENAME TO table1; ALTER TABLE table2 RENAME TO table1;
"}], "}],
disallow_migration_change,
) )
.unwrap(); .unwrap();
@ -338,8 +312,4 @@ mod test {
assert_eq!(res, "test text"); assert_eq!(res, "test text");
} }
fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool {
false
}
} }

View file

@ -278,8 +278,12 @@ mod test {
enum TestDomain {} enum TestDomain {}
impl Domain for TestDomain { impl Domain for TestDomain {
const NAME: &str = "test"; fn name() -> &'static str {
const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]; "test"
}
fn migrations() -> &'static [&'static str] {
&["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]
}
} }
for _ in 0..100 { for _ in 0..100 {
@ -308,9 +312,12 @@ mod test {
fn wild_zed_lost_failure() { fn wild_zed_lost_failure() {
enum TestWorkspace {} enum TestWorkspace {}
impl Domain for TestWorkspace { impl Domain for TestWorkspace {
const NAME: &str = "workspace"; fn name() -> &'static str {
"workspace"
}
const MIGRATIONS: &[&str] = &[" fn migrations() -> &'static [&'static str] {
&["
CREATE TABLE workspaces( CREATE TABLE workspaces(
workspace_id INTEGER PRIMARY KEY, workspace_id INTEGER PRIMARY KEY,
dock_visible INTEGER, -- Boolean dock_visible INTEGER, -- Boolean
@ -329,7 +336,8 @@ mod test {
ON DELETE CASCADE ON DELETE CASCADE
ON UPDATE CASCADE ON UPDATE CASCADE
) STRICT; ) STRICT;
"]; "]
}
} }
let builder = let builder =

View file

@ -9,11 +9,7 @@ use std::path::{Path, PathBuf};
use ui::{App, Context, Pixels, Window}; use ui::{App, Context, Pixels, Window};
use util::ResultExt as _; use util::ResultExt as _;
use db::{ use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
query,
sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection},
sqlez_macros::sql,
};
use workspace::{ use workspace::{
ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace, ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace,
WorkspaceDb, WorkspaceId, WorkspaceDb, WorkspaceId,
@ -379,13 +375,9 @@ impl<'de> Deserialize<'de> for SerializedAxis {
} }
} }
pub struct TerminalDb(ThreadSafeConnection); define_connection! {
pub static ref TERMINAL_DB: TerminalDb<WorkspaceDb> =
impl Domain for TerminalDb { &[sql!(
const NAME: &str = stringify!(TerminalDb);
const MIGRATIONS: &[&str] = &[
sql!(
CREATE TABLE terminals ( CREATE TABLE terminals (
workspace_id INTEGER, workspace_id INTEGER,
item_id INTEGER UNIQUE, item_id INTEGER UNIQUE,
@ -422,8 +414,6 @@ impl Domain for TerminalDb {
]; ];
} }
db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]);
impl TerminalDb { impl TerminalDb {
query! { query! {
pub async fn update_workspace_id( pub async fn update_workspace_id(

Some files were not shown because too many files have changed in this diff Show more