Compare commits

..

1 commit

Author SHA1 Message Date
MrSubidubi
807293ac2d Swap newline bindings for default linux keymap 2025-08-26 18:09:46 +02:00
94 changed files with 3825 additions and 6796 deletions

View file

@ -14,7 +14,7 @@ body:
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
- Any code must be sufficient to reproduce (include context!)
- 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.
-->

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",
"f11": "zed::ToggleFullScreen",
"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"
}
},
@ -120,7 +120,7 @@
"alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
"ctrl-alt-shift-e": "editor::ToggleEditPrediction",
"ctrl-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint"
}
@ -130,8 +130,8 @@
"bindings": {
"shift-enter": "editor::Newline",
"enter": "editor::Newline",
"ctrl-enter": "editor::NewlineAbove",
"ctrl-shift-enter": "editor::NewlineBelow",
"ctrl-enter": "editor::NewlineBelow",
"ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,12 +21,12 @@ use ui::prelude::*;
use util::ResultExt as _;
use workspace::{Item, Workspace};
actions!(dev, [OpenAcpLogs]);
actions!(acp, [OpenDebugTools]);
pub fn init(cx: &mut App) {
cx.observe_new(
|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 =
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), 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> {
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.clone()
@ -2097,7 +2097,7 @@ impl Thread {
}
pub fn summarize(&mut self, cx: &mut Context<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else {
println!("No thread summary model");
return;
};
@ -2416,7 +2416,7 @@ impl Thread {
}
let Some(ConfiguredModel { model, provider }) =
LanguageModelRegistry::read_global(cx).thread_summary_model()
LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
else {
return;
};
@ -5410,13 +5410,10 @@ fn main() {{
}),
cx,
);
registry.set_thread_summary_model(
Some(ConfiguredModel {
provider,
model: model.clone(),
}),
cx,
);
registry.set_thread_summary_model(Some(ConfiguredModel {
provider,
model: model.clone(),
}));
})
});

View file

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

View file

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

View file

@ -732,17 +732,7 @@ impl Thread {
stream.update_tool_call_fields(
&tool_use.id,
acp::ToolCallUpdateFields {
status: Some(
tool_result
.as_ref()
.map_or(acp::ToolCallStatus::Failed, |result| {
if result.is_error {
acp::ToolCallStatus::Failed
} else {
acp::ToolCallStatus::Completed
}
}),
),
status: Some(acp::ToolCallStatus::Completed),
raw_output: output,
..Default::default()
},
@ -1567,7 +1557,7 @@ impl Thread {
tool_name: tool_use.name,
is_error: true,
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> {
let event = self.0.next().await;
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))?;
event_stream.update_diff(diff.clone());
let _finalize_diff = util::defer({
let diff = diff.downgrade();
let mut cx = cx.clone();
move || {
diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
}
});
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
@ -396,6 +389,8 @@ impl AgentTool for EditFileTool {
})
.await;
diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
let input_path = input.path.display();
if unified_diff.is_empty() {
anyhow::ensure!(
@ -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) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);

View file

@ -162,34 +162,12 @@ impl AgentConnection for AcpConnection {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers = context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
let command = configuration.command();
Some(acp::McpServer {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
})
.collect()
} else {
vec![]
},
})
})
.collect();
cx.spawn(async move |cx| {
let response = conn
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
.new_session(acp::NewSessionRequest {
mcp_servers: vec![],
cwd,
})
.await
.map_err(|err| {
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 editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
TextStyleRefinement, WeakEntity, Window,
};
use language::language_settings::SoftWrap;
@ -154,22 +154,10 @@ impl EntryViewState {
});
}
}
AgentThreadEntry::AssistantMessage(message) => {
let entry = if let Some(Entry::AssistantMessage(entry)) =
self.entries.get_mut(index)
{
entry
} else {
self.set_entry(
index,
Entry::AssistantMessage(AssistantMessageEntry::default()),
);
let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
unreachable!()
};
entry
};
entry.sync(message);
AgentThreadEntry::AssistantMessage(_) => {
if index == self.entries.len() {
self.entries.push(Entry::empty())
}
}
};
}
@ -189,7 +177,7 @@ impl EntryViewState {
pub fn settings_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
Entry::UserMessage { .. } => {}
Entry::Content(response_views) => {
for view in response_views.values() {
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
@ -220,29 +208,9 @@ pub enum ViewEvent {
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
}
#[derive(Default, Debug)]
pub struct AssistantMessageEntry {
scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,
}
impl AssistantMessageEntry {
pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> {
self.scroll_handles_by_chunk_index.get(&ix).cloned()
}
pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
let ix = message.chunks.len() - 1;
let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
handle.scroll_to_bottom();
}
}
}
#[derive(Debug)]
pub enum Entry {
UserMessage(Entity<MessageEditor>),
AssistantMessage(AssistantMessageEntry),
Content(HashMap<EntityId, AnyEntity>),
}
@ -250,7 +218,7 @@ impl Entry {
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
match self {
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())
}
pub fn scroll_handle_for_assistant_message_chunk(
&self,
chunk_ix: usize,
) -> Option<ScrollHandle> {
match self {
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
Self::UserMessage(_) | Self::Content(_) => None,
}
}
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
match self {
Self::Content(map) => Some(map),
@ -296,7 +254,7 @@ impl Entry {
pub fn has_content(&self) -> bool {
match self {
Self::Content(map) => !map.is_empty(),
Self::UserMessage(_) | Self::AssistantMessage(_) => false,
Self::UserMessage(_) => false,
}
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

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)
)
}
/// 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 db::{
query,
sqlez::{
bindable::Column, domain::Domain, statement::Statement,
thread_safe_connection::ThreadSafeConnection,
},
define_connection, query,
sqlez::{bindable::Column, statement::Statement},
sqlez_macros::sql,
};
use serde::{Deserialize, Serialize};
@ -53,11 +50,8 @@ impl Column for SerializedCommandInvocation {
}
}
pub struct CommandPaletteDB(ThreadSafeConnection);
impl Domain for CommandPaletteDB {
const NAME: &str = stringify!(CommandPaletteDB);
const MIGRATIONS: &[&str] = &[sql!(
define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
&[sql!(
CREATE TABLE IF NOT EXISTS command_invocations(
id INTEGER PRIMARY KEY AUTOINCREMENT,
command_name TEXT NOT NULL,
@ -65,9 +59,7 @@ impl Domain for CommandPaletteDB {
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
) STRICT;
)];
}
db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
);
impl CommandPaletteDB {
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
///
/// Arguments:
/// - static variable name for connection
/// - type of connection wrapper
/// - dependencies, whose migrations should be run prior to this domain's migrations
#[macro_export]
macro_rules! static_connection {
($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => {
macro_rules! define_connection {
(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 {
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 {
#[cfg(any(test, feature = "test-support"))]
pub async fn open_test_db(name: &'static str) -> Self {
@ -135,8 +142,7 @@ macro_rules! static_connection {
#[cfg(any(test, feature = "test-support"))]
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
#[allow(unused_parens)]
$t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id))))
$t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id))))
});
#[cfg(not(any(test, feature = "test-support")))]
@ -147,10 +153,46 @@ macro_rules! static_connection {
} else {
$crate::RELEASE_CHANNEL.dev_name()
};
#[allow(unused_parens)]
$t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope)))
$t($crate::smol::block_on($crate::open_db::<$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)
@ -177,12 +219,17 @@ mod tests {
enum BadDB {}
impl Domain for BadDB {
const NAME: &str = "db_tests";
const MIGRATIONS: &[&str] = &[
sql!(CREATE TABLE test(value);),
// failure because test already exists
sql!(CREATE TABLE test(value);),
];
fn name() -> &'static str {
"db_tests"
}
fn migrations() -> &'static [&'static str] {
&[
sql!(CREATE TABLE test(value);),
// failure because test already exists
sql!(CREATE TABLE test(value);),
]
}
}
let tempdir = tempfile::Builder::new()
@ -204,15 +251,25 @@ mod tests {
enum CorruptedDB {}
impl Domain for CorruptedDB {
const NAME: &str = "db_tests";
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
fn name() -> &'static str {
"db_tests"
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);)]
}
}
enum GoodDB {}
impl Domain for GoodDB {
const NAME: &str = "db_tests"; //Notice same name
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)];
fn name() -> &'static str {
"db_tests" //Notice same name
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
}
let tempdir = tempfile::Builder::new()
@ -248,16 +305,25 @@ mod tests {
enum 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 {}
impl Domain for GoodDB {
const NAME: &str = "db_tests"; //Notice same name
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration
fn name() -> &'static str {
"db_tests" //Notice same name
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
}
let tempdir = tempfile::Builder::new()

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,15 +1,14 @@
use std::rc::Rc;
use crate::{
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
};
use collections::HashMap;
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
use smallvec::SmallVec;
/// A keybinding and its associated metadata, from the keymap.
pub struct KeyBinding {
pub(crate) action: Box<dyn Action>,
pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
pub(crate) meta: Option<KeyBindingMetaIndex>,
/// 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 {
let context_predicate =
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
Self::load(
keystrokes,
Box::new(action),
context_predicate,
false,
None,
&DummyKeyboardMapper,
)
.unwrap()
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
}
/// Load a keybinding from the given raw data.
@ -49,22 +40,24 @@ impl KeyBinding {
keystrokes: &str,
action: Box<dyn Action>,
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
use_key_equivalents: bool,
key_equivalents: Option<&HashMap<char, char>>,
action_input: Option<SharedString>,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> std::result::Result<Self, InvalidKeystrokeError> {
let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
.split_whitespace()
.map(|source| {
let keystroke = Keystroke::parse(source)?;
Ok(KeybindingKeystroke::new(
keystroke,
use_key_equivalents,
keyboard_mapper,
))
})
.map(Keystroke::parse)
.collect::<std::result::Result<_, _>>()?;
if let Some(equivalents) = key_equivalents {
for keystroke in keystrokes.iter_mut() {
if keystroke.key.chars().count() == 1
&& let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
{
keystroke.key = key.to_string();
}
}
}
Ok(Self {
keystrokes,
action,
@ -86,13 +79,13 @@ impl KeyBinding {
}
/// 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() {
return None;
}
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
if !typed.as_keystroke().should_match(target) {
if !typed.should_match(target) {
return None;
}
}
@ -101,7 +94,7 @@ impl KeyBinding {
}
/// Get the keystrokes associated with this binding
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
pub fn keystrokes(&self) -> &[Keystroke] {
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_reopen(&self, callback: Box<dyn FnMut()>);
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
@ -250,6 +251,7 @@ pub(crate) trait Platform: 'static {
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn compositor_name(&self) -> &'static str {
""
@ -270,10 +272,6 @@ pub(crate) trait Platform: 'static {
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
}
/// A handle to a platform's display, e.g. a monitor or laptop screen.

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -1,31 +1,22 @@
use anyhow::Result;
use collections::HashMap;
use windows::Win32::UI::{
Input::KeyboardAndMouse::{
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
},
WindowsAndMessaging::KL_NAMELENGTH,
};
use windows_core::HSTRING;
use crate::{
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
};
use crate::{Modifiers, PlatformKeyboardLayout};
pub(crate) struct WindowsKeyboardLayout {
id: String,
name: String,
}
pub(crate) struct WindowsKeyboardMapper {
key_to_vkey: HashMap<String, (u16, bool)>,
vkey_to_key: HashMap<u16, String>,
vkey_to_shifted: HashMap<u16, String>,
}
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
fn id(&self) -> &str {
&self.id
@ -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 {
pub(crate) fn new() -> Result<Self> {
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(
vkey: VIRTUAL_KEY,
scan_code: u32,
@ -243,134 +140,3 @@ pub(crate) fn generate_key_char(
_ => None,
}
}
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
match key {
// ` => VK_OEM_3
"`" => Some((VK_OEM_3.0, false)),
"~" => Some((VK_OEM_3.0, true)),
"1" => Some((VK_1.0, false)),
"!" => Some((VK_1.0, true)),
"2" => Some((VK_2.0, false)),
"@" => Some((VK_2.0, true)),
"3" => Some((VK_3.0, false)),
"#" => Some((VK_3.0, true)),
"4" => Some((VK_4.0, false)),
"$" => Some((VK_4.0, true)),
"5" => Some((VK_5.0, false)),
"%" => Some((VK_5.0, true)),
"6" => Some((VK_6.0, false)),
"^" => Some((VK_6.0, true)),
"7" => Some((VK_7.0, false)),
"&" => Some((VK_7.0, true)),
"8" => Some((VK_8.0, false)),
"*" => Some((VK_8.0, true)),
"9" => Some((VK_9.0, false)),
"(" => Some((VK_9.0, true)),
"0" => Some((VK_0.0, false)),
")" => Some((VK_0.0, true)),
"-" => Some((VK_OEM_MINUS.0, false)),
"_" => Some((VK_OEM_MINUS.0, true)),
"=" => Some((VK_OEM_PLUS.0, false)),
"+" => Some((VK_OEM_PLUS.0, true)),
"[" => Some((VK_OEM_4.0, false)),
"{" => Some((VK_OEM_4.0, true)),
"]" => Some((VK_OEM_6.0, false)),
"}" => Some((VK_OEM_6.0, true)),
"\\" => Some((VK_OEM_5.0, false)),
"|" => Some((VK_OEM_5.0, true)),
";" => Some((VK_OEM_1.0, false)),
":" => Some((VK_OEM_1.0, true)),
"'" => Some((VK_OEM_7.0, false)),
"\"" => Some((VK_OEM_7.0, true)),
"," => Some((VK_OEM_COMMA.0, false)),
"<" => Some((VK_OEM_COMMA.0, true)),
"." => Some((VK_OEM_PERIOD.0, false)),
">" => Some((VK_OEM_PERIOD.0, true)),
"/" => Some((VK_OEM_2.0, false)),
"?" => Some((VK_OEM_2.0, true)),
_ => None,
}
}
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
VK_OEM_3,
VK_OEM_MINUS,
VK_OEM_PLUS,
VK_OEM_4,
VK_OEM_5,
VK_OEM_6,
VK_OEM_1,
VK_OEM_7,
VK_OEM_COMMA,
VK_OEM_PERIOD,
VK_OEM_2,
VK_OEM_102,
VK_OEM_8,
VK_ABNT_C1,
VK_0,
VK_1,
VK_2,
VK_3,
VK_4,
VK_5,
VK_6,
VK_7,
VK_8,
VK_9,
];
#[cfg(test)]
mod tests {
use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
#[test]
fn test_keyboard_mapper() {
let mapper = WindowsKeyboardMapper::new();
// Normal case
let keystroke = Keystroke {
modifiers: Modifiers::control(),
key: "a".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(mapped.inner, keystroke);
assert_eq!(mapped.display_key, "a");
assert_eq!(mapped.display_modifiers, Modifiers::control());
// Shifted case, ctrl-$
let keystroke = Keystroke {
modifiers: Modifiers::control(),
key: "$".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
assert_eq!(mapped.inner, keystroke);
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
// Shifted case, but shift is true
let keystroke = Keystroke {
modifiers: Modifiers::control_shift(),
key: "$".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke, true);
assert_eq!(mapped.inner.modifiers, Modifiers::control());
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
// Windows style
let keystroke = Keystroke {
modifiers: Modifiers::control_shift(),
key: "4".to_string(),
key_char: None,
};
let mapped = mapper.map_key_equivalent(keystroke, true);
assert_eq!(mapped.inner.modifiers, Modifiers::control());
assert_eq!(mapped.inner.key, "$");
assert_eq!(mapped.display_key, "4");
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
}
}

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()>) {
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
#[derive(Default, Clone, Debug, PartialEq)]
pub struct ZedDotDevSettings {
@ -146,7 +146,7 @@ impl State {
default_fast_model: None,
recommended_models: Vec::new(),
_fetch_models_task: cx.spawn(async move |this, cx| {
maybe!(async move {
maybe!(async {
let (client, llm_api_token) = this
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,8 +23,6 @@ actions!(
HelixInsert,
/// Appends at the end of the selection.
HelixAppend,
/// Goes to the location of the last modification.
HelixGotoLastModification,
]
);
@ -33,7 +31,6 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, Vim::helix_insert);
Vim::action(editor, cx, Vim::helix_append);
Vim::action(editor, cx, Vim::helix_yank);
Vim::action(editor, cx, Vim::helix_goto_last_modification);
}
impl Vim {
@ -433,15 +430,6 @@ impl Vim {
});
self.switch_mode(Mode::HelixNormal, true, window, cx);
}
pub fn helix_goto_last_modification(
&mut self,
_: &HelixGotoLastModification,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.jump(".".into(), false, false, window, cx);
}
}
#[cfg(test)]
@ -453,7 +441,6 @@ mod test {
#[gpui::test]
async fn test_word_motions(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
// «
// ˇ
// »
@ -515,7 +502,6 @@ mod test {
#[gpui::test]
async fn test_delete(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
// test delete a selection
cx.set_state(
@ -596,7 +582,6 @@ mod test {
#[gpui::test]
async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
cx.set_state(
indoc! {"
@ -650,7 +635,6 @@ mod test {
#[gpui::test]
async fn test_newline_char(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
@ -668,7 +652,6 @@ mod test {
#[gpui::test]
async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
cx.set_state(
indoc! {"
«The ˇ»quick brown
@ -691,7 +674,6 @@ mod test {
#[gpui::test]
async fn test_append(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
// test from the end of the selection
cx.set_state(
indoc! {"
@ -734,7 +716,6 @@ mod test {
#[gpui::test]
async fn test_replace(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
// No selection (single character)
cx.set_state("ˇaa", Mode::HelixNormal);
@ -782,72 +763,4 @@ mod test {
cx.shared_clipboard().assert_eq("worl");
cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
}
#[gpui::test]
async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
// First copy some text to clipboard
cx.set_state("«hello worldˇ»", Mode::HelixNormal);
cx.simulate_keystrokes("y");
// Test paste with shift-r on single cursor
cx.set_state("foo ˇbar", Mode::HelixNormal);
cx.simulate_keystrokes("shift-r");
cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
// Test paste with shift-r on selection
cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
cx.simulate_keystrokes("shift-r");
cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
}
#[gpui::test]
async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
// Make a modification at a specific location
cx.set_state("ˇhello", Mode::HelixNormal);
assert_eq!(cx.mode(), Mode::HelixNormal);
cx.simulate_keystrokes("i");
assert_eq!(cx.mode(), Mode::Insert);
cx.simulate_keystrokes("escape");
assert_eq!(cx.mode(), Mode::HelixNormal);
}
#[gpui::test]
async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
// Make a modification at a specific location
cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
cx.simulate_keystrokes("i");
cx.simulate_keystrokes("escape");
cx.simulate_keystrokes("i");
cx.simulate_keystrokes("m o d i f i e d space");
cx.simulate_keystrokes("escape");
// TODO: this fails, because state is no longer helix
cx.assert_state(
"line one\nline modified ˇtwo\nline three",
Mode::HelixNormal,
);
// Move cursor away from the modification
cx.simulate_keystrokes("up");
// Use "g ." to go back to last modification
cx.simulate_keystrokes("g .");
// Verify we're back at the modification location and still in HelixNormal mode
cx.assert_state(
"line one\nline modifiedˇ two\nline three",
Mode::HelixNormal,
);
}
}

View file

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

View file

@ -7,10 +7,8 @@ use crate::{motion::Motion, object::Object};
use anyhow::Result;
use collections::HashMap;
use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
use db::{
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
sqlez_macros::sql,
};
use db::define_connection;
use db::sqlez_macros::sql;
use editor::display_map::{is_invisible, replacement};
use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint};
use gpui::{
@ -1670,12 +1668,8 @@ impl MarksView {
}
}
pub struct VimDb(ThreadSafeConnection);
impl Domain for VimDb {
const NAME: &str = stringify!(VimDb);
const MIGRATIONS: &[&str] = &[
define_connection! (
pub static ref DB: VimDb<WorkspaceDb> = &[
sql! (
CREATE TABLE vim_marks (
workspace_id INTEGER,
@ -1695,9 +1689,7 @@ impl Domain for VimDb {
ON vim_global_marks_paths(workspace_id, mark_name);
),
];
}
db::static_connection!(DB, VimDb, [WorkspaceDb]);
);
struct SerializedMark {
path: Arc<Path>,

View file

@ -1,4 +1,4 @@
use std::{path::Path, sync::Arc};
use std::{path::PathBuf, sync::Arc};
use gpui::{EventEmitter, FocusHandle, Focusable};
use ui::{
@ -12,7 +12,7 @@ use crate::Item;
/// A view to display when a certain buffer fails to open.
pub struct InvalidBufferView {
/// Which path was attempted to open.
pub abs_path: Arc<Path>,
pub abs_path: Arc<PathBuf>,
/// An error message, happened when opening the buffer.
pub error: SharedString,
is_local: bool,
@ -21,7 +21,7 @@ pub struct InvalidBufferView {
impl InvalidBufferView {
pub fn new(
abs_path: &Path,
abs_path: PathBuf,
is_local: bool,
e: &anyhow::Error,
_: &mut Window,
@ -29,7 +29,7 @@ impl InvalidBufferView {
) -> Self {
Self {
is_local,
abs_path: Arc::from(abs_path),
abs_path: Arc::new(abs_path),
error: format!("{e}").into(),
focus_handle: cx.focus_handle(),
}
@ -43,7 +43,7 @@ impl Item for InvalidBufferView {
// Ensure we always render at least the filename.
detail += 1;
let path = self.abs_path.as_ref();
let path = self.abs_path.as_path();
let mut prefix = path;
while detail > 0 {

View file

@ -23,7 +23,7 @@ use std::{
any::{Any, TypeId},
cell::RefCell,
ops::Range,
path::Path,
path::PathBuf,
rc::Rc,
sync::Arc,
time::Duration,
@ -1168,7 +1168,7 @@ pub trait ProjectItem: Item {
/// with the error from that failure as an argument.
/// Allows to open an item that can gracefully display and handle errors.
fn for_broken_project_item(
_abs_path: &Path,
_abs_path: PathBuf,
_is_local: bool,
_e: &anyhow::Error,
_window: &mut Window,

View file

@ -58,7 +58,11 @@ impl PathList {
let mut paths: Vec<PathBuf> = if serialized.paths.is_empty() {
Vec::new()
} else {
serialized.paths.split('\n').map(PathBuf::from).collect()
serde_json::from_str::<Vec<PathBuf>>(&serialized.paths)
.unwrap_or(Vec::new())
.into_iter()
.map(|s| SanitizedPath::from(s).into())
.collect()
};
let mut order: Vec<usize> = serialized
@ -81,13 +85,7 @@ impl PathList {
pub fn serialize(&self) -> SerializedPathList {
use std::fmt::Write as _;
let mut paths = String::new();
for path in self.paths.iter() {
if !paths.is_empty() {
paths.push('\n');
}
paths.push_str(&path.to_string_lossy());
}
let paths = serde_json::to_string(&self.paths).unwrap_or_default();
let mut order = String::new();
for ix in self.order.iter() {

View file

@ -10,11 +10,7 @@ use std::{
use anyhow::{Context as _, Result, bail};
use collections::HashMap;
use db::{
query,
sqlez::{connection::Connection, domain::Domain},
sqlez_macros::sql,
};
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
@ -279,189 +275,186 @@ impl sqlez::bindable::Bind for SerializedPixels {
}
}
pub struct WorkspaceDb(ThreadSafeConnection);
define_connection! {
pub static ref DB: WorkspaceDb<()> =
&[
sql!(
CREATE TABLE workspaces(
workspace_id INTEGER PRIMARY KEY,
workspace_location BLOB UNIQUE,
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
left_sidebar_open INTEGER, // Boolean
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
) STRICT;
impl Domain for WorkspaceDb {
const NAME: &str = stringify!(WorkspaceDb);
CREATE TABLE pane_groups(
group_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
parent_group_id INTEGER, // NULL indicates that this is a root node
position INTEGER, // NULL indicates that this is a root node
axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
const MIGRATIONS: &[&str] = &[
sql!(
CREATE TABLE workspaces(
workspace_id INTEGER PRIMARY KEY,
workspace_location BLOB UNIQUE,
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
left_sidebar_open INTEGER, // Boolean
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
) STRICT;
CREATE TABLE panes(
pane_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
active INTEGER NOT NULL, // Boolean
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) STRICT;
CREATE TABLE pane_groups(
group_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
parent_group_id INTEGER, // NULL indicates that this is a root node
position INTEGER, // NULL indicates that this is a root node
axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
CREATE TABLE center_panes(
pane_id INTEGER PRIMARY KEY,
parent_group_id INTEGER, // NULL means that this is a root pane
position INTEGER, // NULL means that this is a root pane
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
ON DELETE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
CREATE TABLE panes(
pane_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
active INTEGER NOT NULL, // Boolean
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) STRICT;
CREATE TABLE center_panes(
pane_id INTEGER PRIMARY KEY,
parent_group_id INTEGER, // NULL means that this is a root pane
position INTEGER, // NULL means that this is a root pane
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
ON DELETE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
CREATE TABLE items(
item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
workspace_id INTEGER NOT NULL,
pane_id INTEGER NOT NULL,
kind TEXT NOT NULL,
position INTEGER NOT NULL,
active INTEGER NOT NULL,
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
ON DELETE CASCADE,
PRIMARY KEY(item_id, workspace_id)
) STRICT;
),
sql!(
ALTER TABLE workspaces ADD COLUMN window_state TEXT;
ALTER TABLE workspaces ADD COLUMN window_x REAL;
ALTER TABLE workspaces ADD COLUMN window_y REAL;
ALTER TABLE workspaces ADD COLUMN window_width REAL;
ALTER TABLE workspaces ADD COLUMN window_height REAL;
ALTER TABLE workspaces ADD COLUMN display BLOB;
),
// Drop foreign key constraint from workspaces.dock_pane to panes table.
sql!(
CREATE TABLE workspaces_2(
workspace_id INTEGER PRIMARY KEY,
workspace_location BLOB UNIQUE,
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
left_sidebar_open INTEGER, // Boolean
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
window_state TEXT,
window_x REAL,
window_y REAL,
window_width REAL,
window_height REAL,
display BLOB
) STRICT;
INSERT INTO workspaces_2 SELECT * FROM workspaces;
DROP TABLE workspaces;
ALTER TABLE workspaces_2 RENAME TO workspaces;
),
// Add panels related information
sql!(
ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
),
// Add panel zoom persistence
sql!(
ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
),
// Add pane group flex data
sql!(
ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
),
// Add fullscreen field to workspace
// Deprecated, `WindowBounds` holds the fullscreen state now.
// Preserving so users can downgrade Zed.
sql!(
ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
),
// Add preview field to items
sql!(
ALTER TABLE items ADD COLUMN preview INTEGER; //bool
),
// Add centered_layout field to workspace
sql!(
ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
),
sql!(
CREATE TABLE remote_projects (
remote_project_id INTEGER NOT NULL UNIQUE,
path TEXT,
dev_server_name TEXT
);
ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
),
sql!(
DROP TABLE remote_projects;
CREATE TABLE dev_server_projects (
id INTEGER NOT NULL UNIQUE,
path TEXT,
dev_server_name TEXT
);
ALTER TABLE workspaces DROP COLUMN remote_project_id;
ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
),
sql!(
ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
),
sql!(
ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
),
sql!(
ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
),
sql!(
ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
),
sql!(
CREATE TABLE ssh_projects (
id INTEGER PRIMARY KEY,
host TEXT NOT NULL,
port INTEGER,
path TEXT NOT NULL,
user TEXT
);
ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
),
sql!(
ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
),
sql!(
CREATE TABLE toolchains (
workspace_id INTEGER,
worktree_id INTEGER,
language_name TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
PRIMARY KEY (workspace_id, worktree_id, language_name)
);
),
sql!(
ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
),
sql!(
CREATE TABLE items(
item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
workspace_id INTEGER NOT NULL,
pane_id INTEGER NOT NULL,
kind TEXT NOT NULL,
position INTEGER NOT NULL,
active INTEGER NOT NULL,
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
ON DELETE CASCADE,
PRIMARY KEY(item_id, workspace_id)
) STRICT;
),
sql!(
ALTER TABLE workspaces ADD COLUMN window_state TEXT;
ALTER TABLE workspaces ADD COLUMN window_x REAL;
ALTER TABLE workspaces ADD COLUMN window_y REAL;
ALTER TABLE workspaces ADD COLUMN window_width REAL;
ALTER TABLE workspaces ADD COLUMN window_height REAL;
ALTER TABLE workspaces ADD COLUMN display BLOB;
),
// Drop foreign key constraint from workspaces.dock_pane to panes table.
sql!(
CREATE TABLE workspaces_2(
workspace_id INTEGER PRIMARY KEY,
workspace_location BLOB UNIQUE,
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
left_sidebar_open INTEGER, // Boolean
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
window_state TEXT,
window_x REAL,
window_y REAL,
window_width REAL,
window_height REAL,
display BLOB
) STRICT;
INSERT INTO workspaces_2 SELECT * FROM workspaces;
DROP TABLE workspaces;
ALTER TABLE workspaces_2 RENAME TO workspaces;
),
// Add panels related information
sql!(
ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
),
// Add panel zoom persistence
sql!(
ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
),
// Add pane group flex data
sql!(
ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
),
// Add fullscreen field to workspace
// Deprecated, `WindowBounds` holds the fullscreen state now.
// Preserving so users can downgrade Zed.
sql!(
ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
),
// Add preview field to items
sql!(
ALTER TABLE items ADD COLUMN preview INTEGER; //bool
),
// Add centered_layout field to workspace
sql!(
ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
),
sql!(
CREATE TABLE remote_projects (
remote_project_id INTEGER NOT NULL UNIQUE,
path TEXT,
dev_server_name TEXT
);
ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
),
sql!(
DROP TABLE remote_projects;
CREATE TABLE dev_server_projects (
id INTEGER NOT NULL UNIQUE,
path TEXT,
dev_server_name TEXT
);
ALTER TABLE workspaces DROP COLUMN remote_project_id;
ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
),
sql!(
ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
),
sql!(
ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
),
sql!(
ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
),
sql!(
ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
),
sql!(
CREATE TABLE ssh_projects (
id INTEGER PRIMARY KEY,
host TEXT NOT NULL,
port INTEGER,
path TEXT NOT NULL,
user TEXT
);
ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
),
sql!(
ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
),
sql!(
CREATE TABLE toolchains (
workspace_id INTEGER,
worktree_id INTEGER,
language_name TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
PRIMARY KEY (workspace_id, worktree_id, language_name)
);
),
sql!(
ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
),
sql!(
CREATE TABLE breakpoints (
workspace_id INTEGER NOT NULL,
path TEXT NOT NULL,
@ -473,172 +466,141 @@ impl Domain for WorkspaceDb {
ON UPDATE CASCADE
);
),
sql!(
ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
),
sql!(
ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
),
sql!(
ALTER TABLE breakpoints DROP COLUMN kind
),
sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
sql!(
ALTER TABLE breakpoints ADD COLUMN condition TEXT;
ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
),
sql!(CREATE TABLE toolchains2 (
workspace_id INTEGER,
worktree_id INTEGER,
language_name TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
raw_json TEXT NOT NULL,
relative_worktree_path TEXT NOT NULL,
PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
INSERT INTO toolchains2
SELECT * FROM toolchains;
DROP TABLE toolchains;
ALTER TABLE toolchains2 RENAME TO toolchains;
),
sql!(
CREATE TABLE ssh_connections (
id INTEGER PRIMARY KEY,
host TEXT NOT NULL,
port INTEGER,
user TEXT
);
sql!(
ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
),
sql!(
ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
),
sql!(
ALTER TABLE breakpoints DROP COLUMN kind
),
sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
sql!(
ALTER TABLE breakpoints ADD COLUMN condition TEXT;
ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
),
sql!(CREATE TABLE toolchains2 (
workspace_id INTEGER,
worktree_id INTEGER,
language_name TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
raw_json TEXT NOT NULL,
relative_worktree_path TEXT NOT NULL,
PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
INSERT INTO toolchains2
SELECT * FROM toolchains;
DROP TABLE toolchains;
ALTER TABLE toolchains2 RENAME TO toolchains;
),
sql!(
CREATE TABLE ssh_connections (
id INTEGER PRIMARY KEY,
host TEXT NOT NULL,
port INTEGER,
user TEXT
);
INSERT INTO ssh_connections (host, port, user)
SELECT DISTINCT host, port, user
FROM ssh_projects;
INSERT INTO ssh_connections (host, port, user)
SELECT DISTINCT host, port, user
FROM ssh_projects;
CREATE TABLE workspaces_2(
workspace_id INTEGER PRIMARY KEY,
paths TEXT,
paths_order TEXT,
ssh_connection_id INTEGER REFERENCES ssh_connections(id),
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
window_state TEXT,
window_x REAL,
window_y REAL,
window_width REAL,
window_height REAL,
display BLOB,
left_dock_visible INTEGER,
left_dock_active_panel TEXT,
right_dock_visible INTEGER,
right_dock_active_panel TEXT,
bottom_dock_visible INTEGER,
bottom_dock_active_panel TEXT,
left_dock_zoom INTEGER,
right_dock_zoom INTEGER,
bottom_dock_zoom INTEGER,
fullscreen INTEGER,
centered_layout INTEGER,
session_id TEXT,
window_id INTEGER
) STRICT;
CREATE TABLE workspaces_2(
workspace_id INTEGER PRIMARY KEY,
paths TEXT,
paths_order TEXT,
ssh_connection_id INTEGER REFERENCES ssh_connections(id),
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
window_state TEXT,
window_x REAL,
window_y REAL,
window_width REAL,
window_height REAL,
display BLOB,
left_dock_visible INTEGER,
left_dock_active_panel TEXT,
right_dock_visible INTEGER,
right_dock_active_panel TEXT,
bottom_dock_visible INTEGER,
bottom_dock_active_panel TEXT,
left_dock_zoom INTEGER,
right_dock_zoom INTEGER,
bottom_dock_zoom INTEGER,
fullscreen INTEGER,
centered_layout INTEGER,
session_id TEXT,
window_id INTEGER
) STRICT;
INSERT
INTO workspaces_2
SELECT
workspaces.workspace_id,
CASE
WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
ELSE
CASE
WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
NULL
ELSE
replace(workspaces.local_paths_array, ',', CHAR(10))
END
END as paths,
CASE
WHEN ssh_projects.id IS NOT NULL THEN ""
ELSE workspaces.local_paths_order_array
END as paths_order,
CASE
WHEN ssh_projects.id IS NOT NULL THEN (
SELECT ssh_connections.id
FROM ssh_connections
WHERE
ssh_connections.host IS ssh_projects.host AND
ssh_connections.port IS ssh_projects.port AND
ssh_connections.user IS ssh_projects.user
)
ELSE NULL
END as ssh_connection_id,
workspaces.timestamp,
workspaces.window_state,
workspaces.window_x,
workspaces.window_y,
workspaces.window_width,
workspaces.window_height,
workspaces.display,
workspaces.left_dock_visible,
workspaces.left_dock_active_panel,
workspaces.right_dock_visible,
workspaces.right_dock_active_panel,
workspaces.bottom_dock_visible,
workspaces.bottom_dock_active_panel,
workspaces.left_dock_zoom,
workspaces.right_dock_zoom,
workspaces.bottom_dock_zoom,
workspaces.fullscreen,
workspaces.centered_layout,
workspaces.session_id,
workspaces.window_id
FROM
workspaces LEFT JOIN
ssh_projects ON
workspaces.ssh_project_id = ssh_projects.id;
DELETE FROM workspaces_2
WHERE workspace_id NOT IN (
SELECT MAX(workspace_id)
FROM workspaces_2
GROUP BY ssh_connection_id, paths
);
DROP TABLE ssh_projects;
DROP TABLE workspaces;
ALTER TABLE workspaces_2 RENAME TO workspaces;
CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
),
// Fix any data from when workspaces.paths were briefly encoded as JSON arrays
sql!(
UPDATE workspaces
SET paths = CASE
WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN
replace(
substr(paths, 3, length(paths) - 4),
'"' || ',' || '"',
CHAR(10)
)
INSERT
INTO workspaces_2
SELECT
workspaces.workspace_id,
CASE
WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
ELSE
replace(paths, ',', CHAR(10))
END
WHERE paths IS NOT NULL
),
CASE
WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
NULL
ELSE
json('[' || '"' || replace(workspaces.local_paths_array, ',', '"' || "," || '"') || '"' || ']')
END
END as paths,
CASE
WHEN ssh_projects.id IS NOT NULL THEN ""
ELSE workspaces.local_paths_order_array
END as paths_order,
CASE
WHEN ssh_projects.id IS NOT NULL THEN (
SELECT ssh_connections.id
FROM ssh_connections
WHERE
ssh_connections.host IS ssh_projects.host AND
ssh_connections.port IS ssh_projects.port AND
ssh_connections.user IS ssh_projects.user
)
ELSE NULL
END as ssh_connection_id,
workspaces.timestamp,
workspaces.window_state,
workspaces.window_x,
workspaces.window_y,
workspaces.window_width,
workspaces.window_height,
workspaces.display,
workspaces.left_dock_visible,
workspaces.left_dock_active_panel,
workspaces.right_dock_visible,
workspaces.right_dock_active_panel,
workspaces.bottom_dock_visible,
workspaces.bottom_dock_active_panel,
workspaces.left_dock_zoom,
workspaces.right_dock_zoom,
workspaces.bottom_dock_zoom,
workspaces.fullscreen,
workspaces.centered_layout,
workspaces.session_id,
workspaces.window_id
FROM
workspaces LEFT JOIN
ssh_projects ON
workspaces.ssh_project_id = ssh_projects.id;
DROP TABLE ssh_projects;
DROP TABLE workspaces;
ALTER TABLE workspaces_2 RENAME TO workspaces;
CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
),
];
// Allow recovering from bad migration that was initially shipped to nightly
// when introducing the ssh_connections table.
fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool {
old.starts_with("CREATE TABLE ssh_connections")
&& new.starts_with("CREATE TABLE ssh_connections")
}
}
db::static_connection!(DB, WorkspaceDb, []);
impl WorkspaceDb {
/// Returns a serialized workspace for the given worktree_roots. If the passed array
/// is empty, the most recent workspace is returned instead. If no workspace for the
@ -1841,7 +1803,6 @@ mod tests {
ON DELETE CASCADE
) STRICT;
)],
|_, _, _| false,
)
.unwrap();
})
@ -1890,7 +1851,6 @@ mod tests {
REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;)],
|_, _, _| false,
)
})
.await

View file

@ -613,59 +613,48 @@ impl ProjectItemRegistry {
self.build_project_item_for_path_fns
.push(|project, project_path, window, cx| {
let project_path = project_path.clone();
let is_file = project
.read(cx)
.entry_for_path(&project_path, cx)
.is_some_and(|entry| entry.is_file());
let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
let abs_path = project.read(cx).absolute_path(&project_path, cx);
let is_local = project.read(cx).is_local();
let project_item =
<T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
let project = project.clone();
Some(window.spawn(cx, async move |cx| {
match project_item.await.with_context(|| {
format!(
"opening project path {:?}",
entry_abs_path.as_deref().unwrap_or(&project_path.path)
)
}) {
Ok(project_item) => {
let project_item = project_item;
let project_entry_id: Option<ProjectEntryId> =
project_item.read_with(cx, project::ProjectItem::entry_id)?;
let build_workspace_item = Box::new(
|pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
Box::new(cx.new(|cx| {
T::for_project_item(
project,
Some(pane),
project_item,
window,
cx,
)
})) as Box<dyn ItemHandle>
},
) as Box<_>;
Ok((project_entry_id, build_workspace_item))
}
Err(e) => match entry_abs_path.as_deref().filter(|_| is_file) {
Some(abs_path) => match cx.update(|window, cx| {
T::for_broken_project_item(abs_path, is_local, &e, window, cx)
})? {
Some(broken_project_item_view) => {
let build_workspace_item = Box::new(
Some(window.spawn(cx, async move |cx| match project_item.await {
Ok(project_item) => {
let project_item = project_item;
let project_entry_id: Option<ProjectEntryId> =
project_item.read_with(cx, project::ProjectItem::entry_id)?;
let build_workspace_item = Box::new(
|pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
Box::new(cx.new(|cx| {
T::for_project_item(
project,
Some(pane),
project_item,
window,
cx,
)
})) as Box<dyn ItemHandle>
},
) as Box<_>;
Ok((project_entry_id, build_workspace_item))
}
Err(e) => match abs_path {
Some(abs_path) => match cx.update(|window, cx| {
T::for_broken_project_item(abs_path, is_local, &e, window, cx)
})? {
Some(broken_project_item_view) => {
let build_workspace_item = Box::new(
move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
cx.new(|_| broken_project_item_view).boxed_clone()
},
)
as Box<_>;
Ok((None, build_workspace_item))
}
None => Err(e)?,
},
Ok((None, build_workspace_item))
}
None => Err(e)?,
},
}
None => Err(e)?,
},
}))
});
}
@ -4022,6 +4011,52 @@ impl Workspace {
maybe_pane_handle
}
pub fn split_pane_with_item(
&mut self,
pane_to_split: WeakEntity<Pane>,
split_direction: SplitDirection,
from: WeakEntity<Pane>,
item_id_to_move: EntityId,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(pane_to_split) = pane_to_split.upgrade() else {
return;
};
let Some(from) = from.upgrade() else {
return;
};
let new_pane = self.add_pane(window, cx);
move_item(&from, &new_pane, item_id_to_move, 0, true, window, cx);
self.center
.split(&pane_to_split, &new_pane, split_direction)
.unwrap();
cx.notify();
}
pub fn split_pane_with_project_entry(
&mut self,
pane_to_split: WeakEntity<Pane>,
split_direction: SplitDirection,
project_entry: ProjectEntryId,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
let pane_to_split = pane_to_split.upgrade()?;
let new_pane = self.add_pane(window, cx);
self.center
.split(&pane_to_split, &new_pane, split_direction)
.unwrap();
let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx);
Some(cx.foreground_executor().spawn(async move {
task.await?;
Ok(())
}))
}
pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let active_item = self.active_pane.read(cx).active_item();
for pane in &self.panes {

View file

@ -1308,11 +1308,11 @@ pub fn handle_keymap_file_changes(
})
.detach();
let mut current_layout_id = cx.keyboard_layout().id().to_string();
let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
cx.on_keyboard_layout_change(move |cx| {
let next_layout_id = cx.keyboard_layout().id();
if next_layout_id != current_layout_id {
current_layout_id = next_layout_id.to_string();
let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
if next_mapping != current_mapping {
current_mapping = next_mapping;
keyboard_layout_tx.unbounded_send(()).ok();
}
})
@ -4434,6 +4434,7 @@ mod tests {
assert_eq!(actions_without_namespace, Vec::<&str>::new());
let expected_namespaces = vec![
"acp",
"activity_indicator",
"agent",
#[cfg(not(target_os = "macos"))]
@ -4729,7 +4730,7 @@ mod tests {
// and key strokes contain the given key
bindings
.into_iter()
.any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)),
.any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
"On {} Failed to find {} with key binding {}",
line,
action.name(),

View file

@ -1,17 +1,10 @@
use anyhow::Result;
use db::{
query,
sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection},
sqlez_macros::sql,
};
use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
pub struct ComponentPreviewDb(ThreadSafeConnection);
impl Domain for ComponentPreviewDb {
const NAME: &str = stringify!(ComponentPreviewDb);
const MIGRATIONS: &[&str] = &[sql!(
define_connection! {
pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb<WorkspaceDb> =
&[sql!(
CREATE TABLE component_previews (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
@ -20,11 +13,9 @@ impl Domain for ComponentPreviewDb {
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
)];
)];
}
db::static_connection!(COMPONENT_PREVIEW_DB, ComponentPreviewDb, [WorkspaceDb]);
impl ComponentPreviewDb {
pub async fn save_active_page(
&self,

View file

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

View file

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

View file

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

View file

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

View file

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