Compare commits
10 commits
main
...
fix-none-r
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fa2e24fcc7 | ||
![]() |
11a5d86999 | ||
![]() |
31076619b3 | ||
![]() |
ae6b79a398 | ||
![]() |
e047398756 | ||
![]() |
6f69765172 | ||
![]() |
3410652c71 | ||
![]() |
0617098175 | ||
![]() |
7684bc265b | ||
![]() |
cdb4db55da |
143 changed files with 4887 additions and 8715 deletions
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
|
@ -14,7 +14,7 @@ body:
|
||||||
### Description
|
### Description
|
||||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
|
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
|
||||||
- Any code must be sufficient to reproduce (include context!)
|
- Any code must be sufficient to reproduce (include context!)
|
||||||
- Include code as text, not just as a screenshot.
|
- Code must as text, not just as a screenshot.
|
||||||
- Issues with insufficient detail may be summarily closed.
|
- Issues with insufficient detail may be summarily closed.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -8468,7 +8468,6 @@ dependencies = [
|
||||||
"theme",
|
"theme",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
"util_macros",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
"zed_actions",
|
"zed_actions",
|
||||||
|
@ -13521,7 +13520,6 @@ dependencies = [
|
||||||
"smol",
|
"smol",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"telemetry_events",
|
"telemetry_events",
|
||||||
"thiserror 2.0.12",
|
|
||||||
"toml 0.8.20",
|
"toml 0.8.20",
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
|
|
|
@ -1,4 +1 @@
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12.286 6H7.048C6.469 6 6 6.469 6 7.048v5.238c0 .578.469 1.047 1.048 1.047h5.238c.578 0 1.047-.469 1.047-1.047V7.048c0-.579-.469-1.048-1.047-1.048Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.714 10a1.05 1.05 0 0 1-1.047-1.048V3.714a1.05 1.05 0 0 1 1.047-1.047h5.238A1.05 1.05 0 0 1 10 3.714"/></svg>
|
||||||
<path d="M12.486 6.2H7.24795C6.66895 6.2 6.19995 6.669 6.19995 7.248V12.486C6.19995 13.064 6.66895 13.533 7.24795 13.533H12.486C13.064 13.533 13.533 13.064 13.533 12.486V7.248C13.533 6.669 13.064 6.2 12.486 6.2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M3.91712 10.203C3.63951 10.2022 3.37351 10.0915 3.1773 9.89511C2.98109 9.69872 2.87064 9.43261 2.87012 9.155V3.917C2.87091 3.63956 2.98147 3.37371 3.17765 3.17753C3.37383 2.98135 3.63968 2.87079 3.91712 2.87H9.15512C9.43273 2.87053 9.69883 2.98097 9.89523 3.17718C10.0916 3.37339 10.2023 3.63939 10.2031 3.917" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
|
Before Width: | Height: | Size: 802 B After Width: | Height: | Size: 515 B |
|
@ -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 |
|
@ -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 |
|
@ -40,7 +40,7 @@
|
||||||
"shift-f11": "debugger::StepOut",
|
"shift-f11": "debugger::StepOut",
|
||||||
"f11": "zed::ToggleFullScreen",
|
"f11": "zed::ToggleFullScreen",
|
||||||
"ctrl-alt-z": "edit_prediction::RateCompletions",
|
"ctrl-alt-z": "edit_prediction::RateCompletions",
|
||||||
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
||||||
"ctrl-alt-l": "lsp_tool::ToggleMenu"
|
"ctrl-alt-l": "lsp_tool::ToggleMenu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -120,7 +120,7 @@
|
||||||
"alt-g m": "git::OpenModifiedFiles",
|
"alt-g m": "git::OpenModifiedFiles",
|
||||||
"menu": "editor::OpenContextMenu",
|
"menu": "editor::OpenContextMenu",
|
||||||
"shift-f10": "editor::OpenContextMenu",
|
"shift-f10": "editor::OpenContextMenu",
|
||||||
"ctrl-alt-shift-e": "editor::ToggleEditPrediction",
|
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||||
"f9": "editor::ToggleBreakpoint",
|
"f9": "editor::ToggleBreakpoint",
|
||||||
"shift-f9": "editor::EditLogBreakpoint"
|
"shift-f9": "editor::EditLogBreakpoint"
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -38,7 +38,6 @@
|
||||||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
|
||||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||||
"ctrl-d": "editor::Delete", // delete-char
|
"ctrl-d": "editor::Delete", // delete-char
|
||||||
|
|
|
@ -38,7 +38,6 @@
|
||||||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
|
||||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||||
"ctrl-d": "editor::Delete", // delete-char
|
"ctrl-d": "editor::Delete", // delete-char
|
||||||
|
|
|
@ -428,13 +428,11 @@
|
||||||
"g h": "vim::StartOfLine",
|
"g h": "vim::StartOfLine",
|
||||||
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
||||||
"g e": "vim::EndOfDocument",
|
"g e": "vim::EndOfDocument",
|
||||||
"g .": "vim::HelixGotoLastModification", // go to last modification
|
|
||||||
"g r": "editor::FindAllReferences", // zed specific
|
"g r": "editor::FindAllReferences", // zed specific
|
||||||
"g t": "vim::WindowTop",
|
"g t": "vim::WindowTop",
|
||||||
"g c": "vim::WindowMiddle",
|
"g c": "vim::WindowMiddle",
|
||||||
"g b": "vim::WindowBottom",
|
"g b": "vim::WindowBottom",
|
||||||
|
|
||||||
"shift-r": "editor::Paste",
|
|
||||||
"x": "editor::SelectLine",
|
"x": "editor::SelectLine",
|
||||||
"shift-x": "editor::SelectLine",
|
"shift-x": "editor::SelectLine",
|
||||||
"%": "editor::SelectAll",
|
"%": "editor::SelectAll",
|
||||||
|
|
|
@ -162,12 +162,6 @@
|
||||||
// 2. Always quit the application
|
// 2. Always quit the application
|
||||||
// "on_last_window_closed": "quit_app",
|
// "on_last_window_closed": "quit_app",
|
||||||
"on_last_window_closed": "platform_default",
|
"on_last_window_closed": "platform_default",
|
||||||
// Whether to show padding for zoomed panels.
|
|
||||||
// When enabled, zoomed center panels (e.g. code editor) will have padding all around,
|
|
||||||
// while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
|
|
||||||
//
|
|
||||||
// Default: true
|
|
||||||
"zoomed_padding": true,
|
|
||||||
// Whether to use the system provided dialogs for Open and Save As.
|
// Whether to use the system provided dialogs for Open and Save As.
|
||||||
// When set to false, Zed will use the built-in keyboard-first pickers.
|
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||||
"use_system_path_prompts": true,
|
"use_system_path_prompts": true,
|
||||||
|
@ -653,8 +647,6 @@
|
||||||
// "never"
|
// "never"
|
||||||
"show": "always"
|
"show": "always"
|
||||||
},
|
},
|
||||||
// Whether to enable drag-and-drop operations in the project panel.
|
|
||||||
"drag_and_drop": true,
|
|
||||||
// Whether to hide the root entry when only one folder is open in the window.
|
// Whether to hide the root entry when only one folder is open in the window.
|
||||||
"hide_root": false
|
"hide_root": false
|
||||||
},
|
},
|
||||||
|
@ -1637,9 +1629,6 @@
|
||||||
"allowed": true
|
"allowed": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Kotlin": {
|
|
||||||
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
|
|
||||||
},
|
|
||||||
"LaTeX": {
|
"LaTeX": {
|
||||||
"formatter": "language_server",
|
"formatter": "language_server",
|
||||||
"language_servers": ["texlab", "..."],
|
"language_servers": ["texlab", "..."],
|
||||||
|
|
|
@ -43,8 +43,8 @@
|
||||||
// "args": ["--login"]
|
// "args": ["--login"]
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
"shell": "system"
|
"shell": "system",
|
||||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||||
// "tags": []
|
"tags": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -183,15 +183,16 @@ impl ToolCall {
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
|
|
||||||
first_line.to_owned() + "…"
|
|
||||||
} else {
|
|
||||||
tool_call.title
|
|
||||||
};
|
|
||||||
Self {
|
Self {
|
||||||
id: tool_call.id,
|
id: tool_call.id,
|
||||||
label: cx
|
label: cx.new(|cx| {
|
||||||
.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
|
Markdown::new(
|
||||||
|
tool_call.title.into(),
|
||||||
|
Some(language_registry.clone()),
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}),
|
||||||
kind: tool_call.kind,
|
kind: tool_call.kind,
|
||||||
content: tool_call
|
content: tool_call
|
||||||
.content
|
.content
|
||||||
|
@ -232,11 +233,7 @@ impl ToolCall {
|
||||||
|
|
||||||
if let Some(title) = title {
|
if let Some(title) = title {
|
||||||
self.label.update(cx, |label, cx| {
|
self.label.update(cx, |label, cx| {
|
||||||
if let Some((first_line, _)) = title.split_once("\n") {
|
label.replace(title, cx);
|
||||||
label.replace(first_line.to_owned() + "…", cx)
|
|
||||||
} else {
|
|
||||||
label.replace(title, cx);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -512,7 +509,7 @@ impl ContentBlock {
|
||||||
"`Image`".into()
|
"`Image`".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
|
fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
|
||||||
match self {
|
match self {
|
||||||
ContentBlock::Empty => "",
|
ContentBlock::Empty => "",
|
||||||
ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
|
ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
|
||||||
|
@ -759,8 +756,6 @@ pub struct AcpThread {
|
||||||
connection: Rc<dyn AgentConnection>,
|
connection: Rc<dyn AgentConnection>,
|
||||||
session_id: acp::SessionId,
|
session_id: acp::SessionId,
|
||||||
token_usage: Option<TokenUsage>,
|
token_usage: Option<TokenUsage>,
|
||||||
prompt_capabilities: acp::PromptCapabilities,
|
|
||||||
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -775,12 +770,11 @@ pub enum AcpThreadEvent {
|
||||||
Stopped,
|
Stopped,
|
||||||
Error,
|
Error,
|
||||||
LoadError(LoadError),
|
LoadError(LoadError),
|
||||||
PromptCapabilitiesUpdated,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq)]
|
||||||
pub enum ThreadStatus {
|
pub enum ThreadStatus {
|
||||||
Idle,
|
Idle,
|
||||||
WaitingForToolConfirmation,
|
WaitingForToolConfirmation,
|
||||||
|
@ -827,20 +821,7 @@ impl AcpThread {
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
action_log: Entity<ActionLog>,
|
action_log: Entity<ActionLog>,
|
||||||
session_id: acp::SessionId,
|
session_id: acp::SessionId,
|
||||||
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let prompt_capabilities = *prompt_capabilities_rx.borrow();
|
|
||||||
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
|
|
||||||
loop {
|
|
||||||
let caps = prompt_capabilities_rx.recv().await?;
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.prompt_capabilities = caps;
|
|
||||||
cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
action_log,
|
action_log,
|
||||||
shared_buffers: Default::default(),
|
shared_buffers: Default::default(),
|
||||||
|
@ -852,15 +833,9 @@ impl AcpThread {
|
||||||
connection,
|
connection,
|
||||||
session_id,
|
session_id,
|
||||||
token_usage: None,
|
token_usage: None,
|
||||||
prompt_capabilities,
|
|
||||||
_observe_prompt_capabilities: task,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
self.prompt_capabilities
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
||||||
&self.connection
|
&self.connection
|
||||||
}
|
}
|
||||||
|
@ -1398,10 +1373,6 @@ impl AcpThread {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_resume(&self, cx: &App) -> bool {
|
|
||||||
self.connection.resume(&self.session_id, cx).is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resume(&mut self, cx: &mut Context<Self>) -> BoxFuture<'static, Result<()>> {
|
pub fn resume(&mut self, cx: &mut Context<Self>) -> BoxFuture<'static, Result<()>> {
|
||||||
self.run_turn(cx, async move |this, cx| {
|
self.run_turn(cx, async move |this, cx| {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
@ -2624,19 +2595,13 @@ mod tests {
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
let thread = cx.new(|cx| {
|
let thread = cx.new(|_cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
"Test",
|
"Test",
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
watch::Receiver::constant(acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: true,
|
|
||||||
embedded_context: true,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
self.sessions.lock().insert(session_id, thread.downgrade());
|
self.sessions.lock().insert(session_id, thread.downgrade());
|
||||||
|
@ -2670,6 +2635,14 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||||
|
acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: true,
|
||||||
|
embedded_context: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||||
let sessions = self.sessions.lock();
|
let sessions = self.sessions.lock();
|
||||||
let thread = sessions.get(session_id).unwrap().clone();
|
let thread = sessions.get(session_id).unwrap().clone();
|
||||||
|
@ -2686,7 +2659,7 @@ mod tests {
|
||||||
fn truncate(
|
fn truncate(
|
||||||
&self,
|
&self,
|
||||||
session_id: &acp::SessionId,
|
session_id: &acp::SessionId,
|
||||||
_cx: &App,
|
_cx: &mut App,
|
||||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||||
Some(Rc::new(FakeAgentSessionEditor {
|
Some(Rc::new(FakeAgentSessionEditor {
|
||||||
_session_id: session_id.clone(),
|
_session_id: session_id.clone(),
|
||||||
|
|
|
@ -38,10 +38,12 @@ pub trait AgentConnection {
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<acp::PromptResponse>>;
|
) -> Task<Result<acp::PromptResponse>>;
|
||||||
|
|
||||||
|
fn prompt_capabilities(&self) -> acp::PromptCapabilities;
|
||||||
|
|
||||||
fn resume(
|
fn resume(
|
||||||
&self,
|
&self,
|
||||||
_session_id: &acp::SessionId,
|
_session_id: &acp::SessionId,
|
||||||
_cx: &App,
|
_cx: &mut App,
|
||||||
) -> Option<Rc<dyn AgentSessionResume>> {
|
) -> Option<Rc<dyn AgentSessionResume>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -51,7 +53,7 @@ pub trait AgentConnection {
|
||||||
fn truncate(
|
fn truncate(
|
||||||
&self,
|
&self,
|
||||||
_session_id: &acp::SessionId,
|
_session_id: &acp::SessionId,
|
||||||
_cx: &App,
|
_cx: &mut App,
|
||||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -59,7 +61,7 @@ pub trait AgentConnection {
|
||||||
fn set_title(
|
fn set_title(
|
||||||
&self,
|
&self,
|
||||||
_session_id: &acp::SessionId,
|
_session_id: &acp::SessionId,
|
||||||
_cx: &App,
|
_cx: &mut App,
|
||||||
) -> Option<Rc<dyn AgentSessionSetTitle>> {
|
) -> Option<Rc<dyn AgentSessionSetTitle>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -327,19 +329,13 @@ mod test_support {
|
||||||
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||||
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
|
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
let thread = cx.new(|cx| {
|
let thread = cx.new(|_cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
"Test",
|
"Test",
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
watch::Receiver::constant(acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: true,
|
|
||||||
embedded_context: true,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
self.sessions.lock().insert(
|
self.sessions.lock().insert(
|
||||||
|
@ -352,6 +348,14 @@ mod test_support {
|
||||||
Task::ready(Ok(thread))
|
Task::ready(Ok(thread))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||||
|
acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: true,
|
||||||
|
embedded_context: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn authenticate(
|
fn authenticate(
|
||||||
&self,
|
&self,
|
||||||
_method_id: acp::AuthMethodId,
|
_method_id: acp::AuthMethodId,
|
||||||
|
@ -435,7 +439,7 @@ mod test_support {
|
||||||
fn truncate(
|
fn truncate(
|
||||||
&self,
|
&self,
|
||||||
_session_id: &agent_client_protocol::SessionId,
|
_session_id: &agent_client_protocol::SessionId,
|
||||||
_cx: &App,
|
_cx: &mut App,
|
||||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||||
Some(Rc::new(StubAgentSessionEditor))
|
Some(Rc::new(StubAgentSessionEditor))
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,12 @@ use ui::prelude::*;
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::{Item, Workspace};
|
use workspace::{Item, Workspace};
|
||||||
|
|
||||||
actions!(dev, [OpenAcpLogs]);
|
actions!(acp, [OpenDebugTools]);
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
cx.observe_new(
|
cx.observe_new(
|
||||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||||
workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
|
workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| {
|
||||||
let acp_tools =
|
let acp_tools =
|
||||||
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
|
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
|
||||||
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
|
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
|
||||||
|
|
|
@ -664,7 +664,7 @@ impl Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
|
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
|
||||||
if self.configured_model.is_none() {
|
if self.configured_model.is_none() || self.messages.is_empty() {
|
||||||
self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
||||||
}
|
}
|
||||||
self.configured_model.clone()
|
self.configured_model.clone()
|
||||||
|
@ -2097,7 +2097,7 @@ impl Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn summarize(&mut self, cx: &mut Context<Self>) {
|
pub fn summarize(&mut self, cx: &mut Context<Self>) {
|
||||||
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
|
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else {
|
||||||
println!("No thread summary model");
|
println!("No thread summary model");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -2416,7 +2416,7 @@ impl Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(ConfiguredModel { model, provider }) =
|
let Some(ConfiguredModel { model, provider }) =
|
||||||
LanguageModelRegistry::read_global(cx).thread_summary_model()
|
LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -5410,13 +5410,10 @@ fn main() {{
|
||||||
}),
|
}),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
registry.set_thread_summary_model(
|
registry.set_thread_summary_model(Some(ConfiguredModel {
|
||||||
Some(ConfiguredModel {
|
provider,
|
||||||
provider,
|
model: model.clone(),
|
||||||
model: model.clone(),
|
}));
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -180,7 +180,7 @@ impl NativeAgent {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<Entity<NativeAgent>> {
|
) -> Result<Entity<NativeAgent>> {
|
||||||
log::debug!("Creating new NativeAgent");
|
log::info!("Creating new NativeAgent");
|
||||||
|
|
||||||
let project_context = cx
|
let project_context = cx
|
||||||
.update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?
|
.update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?
|
||||||
|
@ -228,7 +228,7 @@ impl NativeAgent {
|
||||||
) -> Entity<AcpThread> {
|
) -> Entity<AcpThread> {
|
||||||
let connection = Rc::new(NativeAgentConnection(cx.entity()));
|
let connection = Rc::new(NativeAgentConnection(cx.entity()));
|
||||||
let registry = LanguageModelRegistry::read_global(cx);
|
let registry = LanguageModelRegistry::read_global(cx);
|
||||||
let summarization_model = registry.thread_summary_model().map(|c| c.model);
|
let summarization_model = registry.thread_summary_model(cx).map(|c| c.model);
|
||||||
|
|
||||||
thread_handle.update(cx, |thread, cx| {
|
thread_handle.update(cx, |thread, cx| {
|
||||||
thread.set_summarization_model(summarization_model, cx);
|
thread.set_summarization_model(summarization_model, cx);
|
||||||
|
@ -240,16 +240,13 @@ impl NativeAgent {
|
||||||
let title = thread.title();
|
let title = thread.title();
|
||||||
let project = thread.project.clone();
|
let project = thread.project.clone();
|
||||||
let action_log = thread.action_log.clone();
|
let action_log = thread.action_log.clone();
|
||||||
let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone();
|
let acp_thread = cx.new(|_cx| {
|
||||||
let acp_thread = cx.new(|cx| {
|
|
||||||
acp_thread::AcpThread::new(
|
acp_thread::AcpThread::new(
|
||||||
title,
|
title,
|
||||||
connection,
|
connection,
|
||||||
project.clone(),
|
project.clone(),
|
||||||
action_log.clone(),
|
action_log.clone(),
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
prompt_capabilities_rx,
|
|
||||||
cx,
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let subscriptions = vec![
|
let subscriptions = vec![
|
||||||
|
@ -524,7 +521,7 @@ impl NativeAgent {
|
||||||
|
|
||||||
let registry = LanguageModelRegistry::read_global(cx);
|
let registry = LanguageModelRegistry::read_global(cx);
|
||||||
let default_model = registry.default_model().map(|m| m.model);
|
let default_model = registry.default_model().map(|m| m.model);
|
||||||
let summarization_model = registry.thread_summary_model().map(|m| m.model);
|
let summarization_model = registry.thread_summary_model(cx).map(|m| m.model);
|
||||||
|
|
||||||
for session in self.sessions.values_mut() {
|
for session in self.sessions.values_mut() {
|
||||||
session.thread.update(cx, |thread, cx| {
|
session.thread.update(cx, |thread, cx| {
|
||||||
|
@ -759,7 +756,7 @@ impl NativeAgentConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Response stream completed");
|
log::info!("Response stream completed");
|
||||||
anyhow::Ok(acp::PromptResponse {
|
anyhow::Ok(acp::PromptResponse {
|
||||||
stop_reason: acp::StopReason::EndTurn,
|
stop_reason: acp::StopReason::EndTurn,
|
||||||
})
|
})
|
||||||
|
@ -784,7 +781,7 @@ impl AgentModelSelector for NativeAgentConnection {
|
||||||
model_id: acp_thread::AgentModelId,
|
model_id: acp_thread::AgentModelId,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
log::debug!("Setting model for session {}: {}", session_id, model_id);
|
log::info!("Setting model for session {}: {}", session_id, model_id);
|
||||||
let Some(thread) = self
|
let Some(thread) = self
|
||||||
.0
|
.0
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -855,7 +852,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<Entity<acp_thread::AcpThread>>> {
|
) -> Task<Result<Entity<acp_thread::AcpThread>>> {
|
||||||
let agent = self.0.clone();
|
let agent = self.0.clone();
|
||||||
log::debug!("Creating new thread for project at: {:?}", cwd);
|
log::info!("Creating new thread for project at: {:?}", cwd);
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
log::debug!("Starting thread creation in async context");
|
log::debug!("Starting thread creation in async context");
|
||||||
|
@ -920,7 +917,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
log::debug!("Converted prompt to message: {} chars", content.len());
|
log::info!("Converted prompt to message: {} chars", content.len());
|
||||||
log::debug!("Message id: {:?}", id);
|
log::debug!("Message id: {:?}", id);
|
||||||
log::debug!("Message content: {:?}", content);
|
log::debug!("Message content: {:?}", content);
|
||||||
|
|
||||||
|
@ -928,10 +925,18 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||||
|
acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: false,
|
||||||
|
embedded_context: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn resume(
|
fn resume(
|
||||||
&self,
|
&self,
|
||||||
session_id: &acp::SessionId,
|
session_id: &acp::SessionId,
|
||||||
_cx: &App,
|
_cx: &mut App,
|
||||||
) -> Option<Rc<dyn acp_thread::AgentSessionResume>> {
|
) -> Option<Rc<dyn acp_thread::AgentSessionResume>> {
|
||||||
Some(Rc::new(NativeAgentSessionResume {
|
Some(Rc::new(NativeAgentSessionResume {
|
||||||
connection: self.clone(),
|
connection: self.clone(),
|
||||||
|
@ -951,9 +956,9 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
fn truncate(
|
fn truncate(
|
||||||
&self,
|
&self,
|
||||||
session_id: &agent_client_protocol::SessionId,
|
session_id: &agent_client_protocol::SessionId,
|
||||||
cx: &App,
|
cx: &mut App,
|
||||||
) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
|
) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
|
||||||
self.0.read_with(cx, |agent, _cx| {
|
self.0.update(cx, |agent, _cx| {
|
||||||
agent.sessions.get(session_id).map(|session| {
|
agent.sessions.get(session_id).map(|session| {
|
||||||
Rc::new(NativeAgentSessionEditor {
|
Rc::new(NativeAgentSessionEditor {
|
||||||
thread: session.thread.clone(),
|
thread: session.thread.clone(),
|
||||||
|
@ -966,7 +971,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
fn set_title(
|
fn set_title(
|
||||||
&self,
|
&self,
|
||||||
session_id: &acp::SessionId,
|
session_id: &acp::SessionId,
|
||||||
_cx: &App,
|
_cx: &mut App,
|
||||||
) -> Option<Rc<dyn acp_thread::AgentSessionSetTitle>> {
|
) -> Option<Rc<dyn acp_thread::AgentSessionSetTitle>> {
|
||||||
Some(Rc::new(NativeAgentSessionSetTitle {
|
Some(Rc::new(NativeAgentSessionSetTitle {
|
||||||
connection: self.clone(),
|
connection: self.clone(),
|
||||||
|
|
|
@ -22,10 +22,6 @@ impl NativeAgentServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentServer for NativeAgentServer {
|
impl AgentServer for NativeAgentServer {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
|
||||||
"zed"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Zed Agent".into()
|
"Zed Agent".into()
|
||||||
}
|
}
|
||||||
|
@ -48,7 +44,7 @@ impl AgentServer for NativeAgentServer {
|
||||||
project: &Entity<Project>,
|
project: &Entity<Project>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
|
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
|
||||||
log::debug!(
|
log::info!(
|
||||||
"NativeAgentServer::connect called for path: {:?}",
|
"NativeAgentServer::connect called for path: {:?}",
|
||||||
_root_dir
|
_root_dir
|
||||||
);
|
);
|
||||||
|
@ -67,7 +63,7 @@ impl AgentServer for NativeAgentServer {
|
||||||
|
|
||||||
// Create the connection wrapper
|
// Create the connection wrapper
|
||||||
let connection = NativeAgentConnection(agent);
|
let connection = NativeAgentConnection(agent);
|
||||||
log::debug!("NativeAgentServer connection established successfully");
|
log::info!("NativeAgentServer connection established successfully");
|
||||||
|
|
||||||
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
|
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,8 +4,6 @@ use agent_client_protocol::{self as acp};
|
||||||
use agent_settings::AgentProfileId;
|
use agent_settings::AgentProfileId;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
use cloud_llm_client::CompletionIntent;
|
|
||||||
use collections::IndexMap;
|
|
||||||
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
|
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
|
||||||
use fs::{FakeFs, Fs};
|
use fs::{FakeFs, Fs};
|
||||||
use futures::{
|
use futures::{
|
||||||
|
@ -72,7 +70,6 @@ async fn test_echo(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
|
||||||
async fn test_thinking(cx: &mut TestAppContext) {
|
async fn test_thinking(cx: &mut TestAppContext) {
|
||||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||||
let fake_model = model.as_fake();
|
let fake_model = model.as_fake();
|
||||||
|
@ -472,7 +469,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||||
tool_name: ToolRequiringPermission::name().into(),
|
tool_name: ToolRequiringPermission::name().into(),
|
||||||
is_error: true,
|
is_error: true,
|
||||||
content: "Permission to run tool denied by user".into(),
|
content: "Permission to run tool denied by user".into(),
|
||||||
output: Some("Permission to run tool denied by user".into())
|
output: None
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -675,6 +672,15 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
|
||||||
"}
|
"}
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure we error if calling resume when tool use limit was *not* reached.
|
||||||
|
let error = thread
|
||||||
|
.update(cx, |thread, cx| thread.resume(cx))
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
error.to_string(),
|
||||||
|
"can only resume after tool use limit is reached"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -1348,7 +1354,6 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
|
||||||
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
||||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||||
let fake_model = model.as_fake();
|
let fake_model = model.as_fake();
|
||||||
|
@ -1687,7 +1692,6 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
|
||||||
async fn test_title_generation(cx: &mut TestAppContext) {
|
async fn test_title_generation(cx: &mut TestAppContext) {
|
||||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||||
let fake_model = model.as_fake();
|
let fake_model = model.as_fake();
|
||||||
|
@ -1733,81 +1737,6 @@ async fn test_title_generation(cx: &mut TestAppContext) {
|
||||||
thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world"));
|
thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
|
||||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
|
||||||
let fake_model = model.as_fake();
|
|
||||||
|
|
||||||
let _events = thread
|
|
||||||
.update(cx, |thread, cx| {
|
|
||||||
thread.add_tool(ToolRequiringPermission);
|
|
||||||
thread.add_tool(EchoTool);
|
|
||||||
thread.send(UserMessageId::new(), ["Hey!"], cx)
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
let permission_tool_use = LanguageModelToolUse {
|
|
||||||
id: "tool_id_1".into(),
|
|
||||||
name: ToolRequiringPermission::name().into(),
|
|
||||||
raw_input: "{}".into(),
|
|
||||||
input: json!({}),
|
|
||||||
is_input_complete: true,
|
|
||||||
};
|
|
||||||
let echo_tool_use = LanguageModelToolUse {
|
|
||||||
id: "tool_id_2".into(),
|
|
||||||
name: EchoTool::name().into(),
|
|
||||||
raw_input: json!({"text": "test"}).to_string(),
|
|
||||||
input: json!({"text": "test"}),
|
|
||||||
is_input_complete: true,
|
|
||||||
};
|
|
||||||
fake_model.send_last_completion_stream_text_chunk("Hi!");
|
|
||||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
|
||||||
permission_tool_use,
|
|
||||||
));
|
|
||||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
|
||||||
echo_tool_use.clone(),
|
|
||||||
));
|
|
||||||
fake_model.end_last_completion_stream();
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
// Ensure pending tools are skipped when building a request.
|
|
||||||
let request = thread
|
|
||||||
.read_with(cx, |thread, cx| {
|
|
||||||
thread.build_completion_request(CompletionIntent::EditFile, cx)
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
request.messages[1..],
|
|
||||||
vec![
|
|
||||||
LanguageModelRequestMessage {
|
|
||||||
role: Role::User,
|
|
||||||
content: vec!["Hey!".into()],
|
|
||||||
cache: true
|
|
||||||
},
|
|
||||||
LanguageModelRequestMessage {
|
|
||||||
role: Role::Assistant,
|
|
||||||
content: vec![
|
|
||||||
MessageContent::Text("Hi!".into()),
|
|
||||||
MessageContent::ToolUse(echo_tool_use.clone())
|
|
||||||
],
|
|
||||||
cache: false
|
|
||||||
},
|
|
||||||
LanguageModelRequestMessage {
|
|
||||||
role: Role::User,
|
|
||||||
content: vec![MessageContent::ToolResult(LanguageModelToolResult {
|
|
||||||
tool_use_id: echo_tool_use.id.clone(),
|
|
||||||
tool_name: echo_tool_use.name,
|
|
||||||
is_error: false,
|
|
||||||
content: "test".into(),
|
|
||||||
output: Some("test".into())
|
|
||||||
})],
|
|
||||||
cache: false
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_agent_connection(cx: &mut TestAppContext) {
|
async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||||
cx.update(settings::init);
|
cx.update(settings::init);
|
||||||
|
@ -1822,11 +1751,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||||
let clock = Arc::new(clock::FakeSystemClock::new());
|
let clock = Arc::new(clock::FakeSystemClock::new());
|
||||||
let client = Client::new(clock, http_client, cx);
|
let client = Client::new(clock, http_client, cx);
|
||||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||||
|
Project::init_settings(cx);
|
||||||
|
agent_settings::init(cx);
|
||||||
language_model::init(client.clone(), cx);
|
language_model::init(client.clone(), cx);
|
||||||
language_models::init(user_store, client.clone(), cx);
|
language_models::init(user_store, client.clone(), cx);
|
||||||
Project::init_settings(cx);
|
|
||||||
LanguageModelRegistry::test(cx);
|
LanguageModelRegistry::test(cx);
|
||||||
agent_settings::init(cx);
|
|
||||||
});
|
});
|
||||||
cx.executor().forbid_parking();
|
cx.executor().forbid_parking();
|
||||||
|
|
||||||
|
@ -2100,7 +2029,6 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
|
||||||
fake_model.send_last_completion_stream_text_chunk("Hey,");
|
|
||||||
fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
|
fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
|
||||||
provider: LanguageModelProviderName::new("Anthropic"),
|
provider: LanguageModelProviderName::new("Anthropic"),
|
||||||
retry_after: Some(Duration::from_secs(3)),
|
retry_after: Some(Duration::from_secs(3)),
|
||||||
|
@ -2110,9 +2038,8 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
||||||
cx.executor().advance_clock(Duration::from_secs(3));
|
cx.executor().advance_clock(Duration::from_secs(3));
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
|
||||||
fake_model.send_last_completion_stream_text_chunk("there!");
|
fake_model.send_last_completion_stream_text_chunk("Hey!");
|
||||||
fake_model.end_last_completion_stream();
|
fake_model.end_last_completion_stream();
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
let mut retry_events = Vec::new();
|
let mut retry_events = Vec::new();
|
||||||
while let Some(Ok(event)) = events.next().await {
|
while let Some(Ok(event)) = events.next().await {
|
||||||
|
@ -2140,94 +2067,12 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
## Assistant
|
## Assistant
|
||||||
|
|
||||||
Hey,
|
Hey!
|
||||||
|
|
||||||
[resume]
|
|
||||||
|
|
||||||
## Assistant
|
|
||||||
|
|
||||||
there!
|
|
||||||
"}
|
"}
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
|
||||||
let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
|
|
||||||
let fake_model = model.as_fake();
|
|
||||||
|
|
||||||
let events = thread
|
|
||||||
.update(cx, |thread, cx| {
|
|
||||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
|
|
||||||
thread.add_tool(EchoTool);
|
|
||||||
thread.send(UserMessageId::new(), ["Call the echo tool!"], cx)
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
let tool_use_1 = LanguageModelToolUse {
|
|
||||||
id: "tool_1".into(),
|
|
||||||
name: EchoTool::name().into(),
|
|
||||||
raw_input: json!({"text": "test"}).to_string(),
|
|
||||||
input: json!({"text": "test"}),
|
|
||||||
is_input_complete: true,
|
|
||||||
};
|
|
||||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
|
||||||
tool_use_1.clone(),
|
|
||||||
));
|
|
||||||
fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
|
|
||||||
provider: LanguageModelProviderName::new("Anthropic"),
|
|
||||||
retry_after: Some(Duration::from_secs(3)),
|
|
||||||
});
|
|
||||||
fake_model.end_last_completion_stream();
|
|
||||||
|
|
||||||
cx.executor().advance_clock(Duration::from_secs(3));
|
|
||||||
let completion = fake_model.pending_completions().pop().unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
completion.messages[1..],
|
|
||||||
vec![
|
|
||||||
LanguageModelRequestMessage {
|
|
||||||
role: Role::User,
|
|
||||||
content: vec!["Call the echo tool!".into()],
|
|
||||||
cache: false
|
|
||||||
},
|
|
||||||
LanguageModelRequestMessage {
|
|
||||||
role: Role::Assistant,
|
|
||||||
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
|
|
||||||
cache: false
|
|
||||||
},
|
|
||||||
LanguageModelRequestMessage {
|
|
||||||
role: Role::User,
|
|
||||||
content: vec![language_model::MessageContent::ToolResult(
|
|
||||||
LanguageModelToolResult {
|
|
||||||
tool_use_id: tool_use_1.id.clone(),
|
|
||||||
tool_name: tool_use_1.name.clone(),
|
|
||||||
is_error: false,
|
|
||||||
content: "test".into(),
|
|
||||||
output: Some("test".into())
|
|
||||||
}
|
|
||||||
)],
|
|
||||||
cache: true
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
fake_model.send_last_completion_stream_text_chunk("Done");
|
|
||||||
fake_model.end_last_completion_stream();
|
|
||||||
cx.run_until_parked();
|
|
||||||
events.collect::<Vec<_>>().await;
|
|
||||||
thread.read_with(cx, |thread, _cx| {
|
|
||||||
assert_eq!(
|
|
||||||
thread.last_message(),
|
|
||||||
Some(Message::Agent(AgentMessage {
|
|
||||||
content: vec![AgentMessageContent::Text("Done".into())],
|
|
||||||
tool_results: IndexMap::default()
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) {
|
async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) {
|
||||||
let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
|
let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
|
||||||
|
|
|
@ -123,7 +123,7 @@ impl Message {
|
||||||
match self {
|
match self {
|
||||||
Message::User(message) => message.to_markdown(),
|
Message::User(message) => message.to_markdown(),
|
||||||
Message::Agent(message) => message.to_markdown(),
|
Message::Agent(message) => message.to_markdown(),
|
||||||
Message::Resume => "[resume]\n".into(),
|
Message::Resume => "[resumed after tool use limit was reached]".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -448,33 +448,24 @@ impl AgentMessage {
|
||||||
cache: false,
|
cache: false,
|
||||||
};
|
};
|
||||||
for chunk in &self.content {
|
for chunk in &self.content {
|
||||||
match chunk {
|
let chunk = match chunk {
|
||||||
AgentMessageContent::Text(text) => {
|
AgentMessageContent::Text(text) => {
|
||||||
assistant_message
|
language_model::MessageContent::Text(text.clone())
|
||||||
.content
|
|
||||||
.push(language_model::MessageContent::Text(text.clone()));
|
|
||||||
}
|
}
|
||||||
AgentMessageContent::Thinking { text, signature } => {
|
AgentMessageContent::Thinking { text, signature } => {
|
||||||
assistant_message
|
language_model::MessageContent::Thinking {
|
||||||
.content
|
text: text.clone(),
|
||||||
.push(language_model::MessageContent::Thinking {
|
signature: signature.clone(),
|
||||||
text: text.clone(),
|
|
||||||
signature: signature.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
AgentMessageContent::RedactedThinking(value) => {
|
|
||||||
assistant_message.content.push(
|
|
||||||
language_model::MessageContent::RedactedThinking(value.clone()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
AgentMessageContent::ToolUse(tool_use) => {
|
|
||||||
if self.tool_results.contains_key(&tool_use.id) {
|
|
||||||
assistant_message
|
|
||||||
.content
|
|
||||||
.push(language_model::MessageContent::ToolUse(tool_use.clone()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AgentMessageContent::RedactedThinking(value) => {
|
||||||
|
language_model::MessageContent::RedactedThinking(value.clone())
|
||||||
|
}
|
||||||
|
AgentMessageContent::ToolUse(value) => {
|
||||||
|
language_model::MessageContent::ToolUse(value.clone())
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
assistant_message.content.push(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut user_message = LanguageModelRequestMessage {
|
let mut user_message = LanguageModelRequestMessage {
|
||||||
|
@ -575,22 +566,11 @@ pub struct Thread {
|
||||||
templates: Arc<Templates>,
|
templates: Arc<Templates>,
|
||||||
model: Option<Arc<dyn LanguageModel>>,
|
model: Option<Arc<dyn LanguageModel>>,
|
||||||
summarization_model: Option<Arc<dyn LanguageModel>>,
|
summarization_model: Option<Arc<dyn LanguageModel>>,
|
||||||
prompt_capabilities_tx: watch::Sender<acp::PromptCapabilities>,
|
|
||||||
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
|
||||||
pub(crate) project: Entity<Project>,
|
pub(crate) project: Entity<Project>,
|
||||||
pub(crate) action_log: Entity<ActionLog>,
|
pub(crate) action_log: Entity<ActionLog>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Thread {
|
impl Thread {
|
||||||
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
|
|
||||||
let image = model.map_or(true, |model| model.supports_images());
|
|
||||||
acp::PromptCapabilities {
|
|
||||||
image,
|
|
||||||
audio: false,
|
|
||||||
embedded_context: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
project_context: Entity<ProjectContext>,
|
project_context: Entity<ProjectContext>,
|
||||||
|
@ -601,8 +581,6 @@ impl Thread {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
||||||
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
||||||
let (prompt_capabilities_tx, prompt_capabilities_rx) =
|
|
||||||
watch::channel(Self::prompt_capabilities(model.as_deref()));
|
|
||||||
Self {
|
Self {
|
||||||
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
|
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
|
||||||
prompt_id: PromptId::new(),
|
prompt_id: PromptId::new(),
|
||||||
|
@ -630,8 +608,6 @@ impl Thread {
|
||||||
templates,
|
templates,
|
||||||
model,
|
model,
|
||||||
summarization_model: None,
|
summarization_model: None,
|
||||||
prompt_capabilities_tx,
|
|
||||||
prompt_capabilities_rx,
|
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
}
|
}
|
||||||
|
@ -732,17 +708,7 @@ impl Thread {
|
||||||
stream.update_tool_call_fields(
|
stream.update_tool_call_fields(
|
||||||
&tool_use.id,
|
&tool_use.id,
|
||||||
acp::ToolCallUpdateFields {
|
acp::ToolCallUpdateFields {
|
||||||
status: Some(
|
status: Some(acp::ToolCallStatus::Completed),
|
||||||
tool_result
|
|
||||||
.as_ref()
|
|
||||||
.map_or(acp::ToolCallStatus::Failed, |result| {
|
|
||||||
if result.is_error {
|
|
||||||
acp::ToolCallStatus::Failed
|
|
||||||
} else {
|
|
||||||
acp::ToolCallStatus::Completed
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
raw_output: output,
|
raw_output: output,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -775,8 +741,6 @@ impl Thread {
|
||||||
.or_else(|| registry.default_model())
|
.or_else(|| registry.default_model())
|
||||||
.map(|model| model.model)
|
.map(|model| model.model)
|
||||||
});
|
});
|
||||||
let (prompt_capabilities_tx, prompt_capabilities_rx) =
|
|
||||||
watch::channel(Self::prompt_capabilities(model.as_deref()));
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
|
@ -806,8 +770,6 @@ impl Thread {
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
updated_at: db_thread.updated_at,
|
updated_at: db_thread.updated_at,
|
||||||
prompt_capabilities_tx,
|
|
||||||
prompt_capabilities_rx,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -975,12 +937,10 @@ impl Thread {
|
||||||
pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
||||||
let old_usage = self.latest_token_usage();
|
let old_usage = self.latest_token_usage();
|
||||||
self.model = Some(model);
|
self.model = Some(model);
|
||||||
let new_caps = Self::prompt_capabilities(self.model.as_deref());
|
|
||||||
let new_usage = self.latest_token_usage();
|
let new_usage = self.latest_token_usage();
|
||||||
if old_usage != new_usage {
|
if old_usage != new_usage {
|
||||||
cx.emit(TokenUsageUpdated(new_usage));
|
cx.emit(TokenUsageUpdated(new_usage));
|
||||||
}
|
}
|
||||||
self.prompt_capabilities_tx.send(new_caps).log_err();
|
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1116,10 +1076,15 @@ impl Thread {
|
||||||
&mut self,
|
&mut self,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
|
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
|
||||||
|
anyhow::ensure!(
|
||||||
|
self.tool_use_limit_reached,
|
||||||
|
"can only resume after tool use limit is reached"
|
||||||
|
);
|
||||||
|
|
||||||
self.messages.push(Message::Resume);
|
self.messages.push(Message::Resume);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
log::debug!("Total messages in thread: {}", self.messages.len());
|
log::info!("Total messages in thread: {}", self.messages.len());
|
||||||
self.run_turn(cx)
|
self.run_turn(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1137,7 +1102,7 @@ impl Thread {
|
||||||
{
|
{
|
||||||
let model = self.model().context("No language model configured")?;
|
let model = self.model().context("No language model configured")?;
|
||||||
|
|
||||||
log::info!("Thread::send called with model: {}", model.name().0);
|
log::info!("Thread::send called with model: {:?}", model.name());
|
||||||
self.advance_prompt_id();
|
self.advance_prompt_id();
|
||||||
|
|
||||||
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||||
|
@ -1147,7 +1112,7 @@ impl Thread {
|
||||||
.push(Message::User(UserMessage { id, content }));
|
.push(Message::User(UserMessage { id, content }));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
log::debug!("Total messages in thread: {}", self.messages.len());
|
log::info!("Total messages in thread: {}", self.messages.len());
|
||||||
self.run_turn(cx)
|
self.run_turn(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1171,14 +1136,44 @@ impl Thread {
|
||||||
event_stream: event_stream.clone(),
|
event_stream: event_stream.clone(),
|
||||||
tools: self.enabled_tools(profile, &model, cx),
|
tools: self.enabled_tools(profile, &model, cx),
|
||||||
_task: cx.spawn(async move |this, cx| {
|
_task: cx.spawn(async move |this, cx| {
|
||||||
log::debug!("Starting agent turn execution");
|
log::info!("Starting agent turn execution");
|
||||||
|
|
||||||
let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await;
|
let turn_result: Result<()> = async {
|
||||||
|
let mut intent = CompletionIntent::UserPrompt;
|
||||||
|
loop {
|
||||||
|
Self::stream_completion(&this, &model, intent, &event_stream, cx).await?;
|
||||||
|
|
||||||
|
let mut end_turn = true;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
// Generate title if needed.
|
||||||
|
if this.title.is_none() && this.pending_title_generation.is_none() {
|
||||||
|
this.generate_title(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// End the turn if the model didn't use tools.
|
||||||
|
let message = this.pending_message.as_ref();
|
||||||
|
end_turn =
|
||||||
|
message.map_or(true, |message| message.tool_results.is_empty());
|
||||||
|
this.flush_pending_message(cx);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
|
||||||
|
log::info!("Tool use limit reached, completing turn");
|
||||||
|
return Err(language_model::ToolUseLimitReachedError.into());
|
||||||
|
} else if end_turn {
|
||||||
|
log::info!("No tool uses found, completing turn");
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
intent = CompletionIntent::ToolResults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.await;
|
||||||
_ = this.update(cx, |this, cx| this.flush_pending_message(cx));
|
_ = this.update(cx, |this, cx| this.flush_pending_message(cx));
|
||||||
|
|
||||||
match turn_result {
|
match turn_result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log::debug!("Turn execution completed");
|
log::info!("Turn execution completed");
|
||||||
event_stream.send_stop(acp::StopReason::EndTurn);
|
event_stream.send_stop(acp::StopReason::EndTurn);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
@ -1204,18 +1199,20 @@ impl Thread {
|
||||||
Ok(events_rx)
|
Ok(events_rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_turn_internal(
|
async fn stream_completion(
|
||||||
this: &WeakEntity<Self>,
|
this: &WeakEntity<Self>,
|
||||||
model: Arc<dyn LanguageModel>,
|
model: &Arc<dyn LanguageModel>,
|
||||||
|
completion_intent: CompletionIntent,
|
||||||
event_stream: &ThreadEventStream,
|
event_stream: &ThreadEventStream,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut attempt = 0;
|
log::debug!("Stream completion started successfully");
|
||||||
let mut intent = CompletionIntent::UserPrompt;
|
let request = this.update(cx, |this, cx| {
|
||||||
loop {
|
this.build_completion_request(completion_intent, cx)
|
||||||
let request =
|
})??;
|
||||||
this.update(cx, |this, cx| this.build_completion_request(intent, cx))??;
|
|
||||||
|
|
||||||
|
let mut attempt = None;
|
||||||
|
'retry: loop {
|
||||||
telemetry::event!(
|
telemetry::event!(
|
||||||
"Agent Thread Completion",
|
"Agent Thread Completion",
|
||||||
thread_id = this.read_with(cx, |this, _| this.id.to_string())?,
|
thread_id = this.read_with(cx, |this, _| this.id.to_string())?,
|
||||||
|
@ -1225,31 +1222,75 @@ impl Thread {
|
||||||
attempt
|
attempt
|
||||||
);
|
);
|
||||||
|
|
||||||
log::debug!("Calling model.stream_completion, attempt {}", attempt);
|
log::info!(
|
||||||
|
"Calling model.stream_completion, attempt {}",
|
||||||
|
attempt.unwrap_or(0)
|
||||||
|
);
|
||||||
let mut events = model
|
let mut events = model
|
||||||
.stream_completion(request, cx)
|
.stream_completion(request.clone(), cx)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| anyhow!(error))?;
|
.map_err(|error| anyhow!(error))?;
|
||||||
let mut tool_results = FuturesUnordered::new();
|
let mut tool_results = FuturesUnordered::new();
|
||||||
let mut error = None;
|
|
||||||
while let Some(event) = events.next().await {
|
while let Some(event) = events.next().await {
|
||||||
log::trace!("Received completion event: {:?}", event);
|
|
||||||
match event {
|
match event {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
|
log::trace!("Received completion event: {:?}", event);
|
||||||
tool_results.extend(this.update(cx, |this, cx| {
|
tool_results.extend(this.update(cx, |this, cx| {
|
||||||
this.handle_completion_event(event, event_stream, cx)
|
this.handle_streamed_completion_event(event, event_stream, cx)
|
||||||
})??);
|
})??);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(error) => {
|
||||||
error = Some(err);
|
let completion_mode =
|
||||||
break;
|
this.read_with(cx, |thread, _cx| thread.completion_mode())?;
|
||||||
|
if completion_mode == CompletionMode::Normal {
|
||||||
|
return Err(anyhow!(error))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(strategy) = Self::retry_strategy_for(&error) else {
|
||||||
|
return Err(anyhow!(error))?;
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_attempts = match &strategy {
|
||||||
|
RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
|
||||||
|
RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
|
||||||
|
};
|
||||||
|
|
||||||
|
let attempt = attempt.get_or_insert(0u8);
|
||||||
|
|
||||||
|
*attempt += 1;
|
||||||
|
|
||||||
|
let attempt = *attempt;
|
||||||
|
if attempt > max_attempts {
|
||||||
|
return Err(anyhow!(error))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let delay = match &strategy {
|
||||||
|
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
|
||||||
|
let delay_secs =
|
||||||
|
initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
|
||||||
|
Duration::from_secs(delay_secs)
|
||||||
|
}
|
||||||
|
RetryStrategy::Fixed { delay, .. } => *delay,
|
||||||
|
};
|
||||||
|
log::debug!("Retry attempt {attempt} with delay {delay:?}");
|
||||||
|
|
||||||
|
event_stream.send_retry(acp_thread::RetryStatus {
|
||||||
|
last_error: error.to_string().into(),
|
||||||
|
attempt: attempt as usize,
|
||||||
|
max_attempts: max_attempts as usize,
|
||||||
|
started_at: Instant::now(),
|
||||||
|
duration: delay,
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.background_executor().timer(delay).await;
|
||||||
|
continue 'retry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let end_turn = tool_results.is_empty();
|
|
||||||
while let Some(tool_result) = tool_results.next().await {
|
while let Some(tool_result) = tool_results.next().await {
|
||||||
log::debug!("Tool finished {:?}", tool_result);
|
log::info!("Tool finished {:?}", tool_result);
|
||||||
|
|
||||||
event_stream.update_tool_call_fields(
|
event_stream.update_tool_call_fields(
|
||||||
&tool_result.tool_use_id,
|
&tool_result.tool_use_id,
|
||||||
|
@ -1270,83 +1311,31 @@ impl Thread {
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
return Ok(());
|
||||||
this.flush_pending_message(cx);
|
|
||||||
if this.title.is_none() && this.pending_title_generation.is_none() {
|
|
||||||
this.generate_title(cx);
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if let Some(error) = error {
|
|
||||||
attempt += 1;
|
|
||||||
let retry =
|
|
||||||
this.update(cx, |this, _| this.handle_completion_error(error, attempt))??;
|
|
||||||
let timer = cx.background_executor().timer(retry.duration);
|
|
||||||
event_stream.send_retry(retry);
|
|
||||||
timer.await;
|
|
||||||
this.update(cx, |this, _cx| {
|
|
||||||
if let Some(Message::Agent(message)) = this.messages.last() {
|
|
||||||
if message.tool_results.is_empty() {
|
|
||||||
intent = CompletionIntent::UserPrompt;
|
|
||||||
this.messages.push(Message::Resume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
} else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
|
|
||||||
return Err(language_model::ToolUseLimitReachedError.into());
|
|
||||||
} else if end_turn {
|
|
||||||
return Ok(());
|
|
||||||
} else {
|
|
||||||
intent = CompletionIntent::ToolResults;
|
|
||||||
attempt = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_completion_error(
|
pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage {
|
||||||
&mut self,
|
log::debug!("Building system message");
|
||||||
error: LanguageModelCompletionError,
|
let prompt = SystemPromptTemplate {
|
||||||
attempt: u8,
|
project: self.project_context.read(cx),
|
||||||
) -> Result<acp_thread::RetryStatus> {
|
available_tools: self.tools.keys().cloned().collect(),
|
||||||
if self.completion_mode == CompletionMode::Normal {
|
|
||||||
return Err(anyhow!(error));
|
|
||||||
}
|
}
|
||||||
|
.render(&self.templates)
|
||||||
let Some(strategy) = Self::retry_strategy_for(&error) else {
|
.context("failed to build system prompt")
|
||||||
return Err(anyhow!(error));
|
.expect("Invalid template");
|
||||||
};
|
log::debug!("System message built");
|
||||||
|
LanguageModelRequestMessage {
|
||||||
let max_attempts = match &strategy {
|
role: Role::System,
|
||||||
RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
|
content: vec![prompt.into()],
|
||||||
RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
|
cache: true,
|
||||||
};
|
|
||||||
|
|
||||||
if attempt > max_attempts {
|
|
||||||
return Err(anyhow!(error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let delay = match &strategy {
|
|
||||||
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
|
|
||||||
let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
|
|
||||||
Duration::from_secs(delay_secs)
|
|
||||||
}
|
|
||||||
RetryStrategy::Fixed { delay, .. } => *delay,
|
|
||||||
};
|
|
||||||
log::debug!("Retry attempt {attempt} with delay {delay:?}");
|
|
||||||
|
|
||||||
Ok(acp_thread::RetryStatus {
|
|
||||||
last_error: error.to_string().into(),
|
|
||||||
attempt: attempt as usize,
|
|
||||||
max_attempts: max_attempts as usize,
|
|
||||||
started_at: Instant::now(),
|
|
||||||
duration: delay,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A helper method that's called on every streamed completion event.
|
/// A helper method that's called on every streamed completion event.
|
||||||
/// Returns an optional tool result task, which the main agentic loop will
|
/// Returns an optional tool result task, which the main agentic loop will
|
||||||
/// send back to the model when it resolves.
|
/// send back to the model when it resolves.
|
||||||
fn handle_completion_event(
|
fn handle_streamed_completion_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: LanguageModelCompletionEvent,
|
event: LanguageModelCompletionEvent,
|
||||||
event_stream: &ThreadEventStream,
|
event_stream: &ThreadEventStream,
|
||||||
|
@ -1541,7 +1530,7 @@ impl Thread {
|
||||||
});
|
});
|
||||||
let supports_images = self.model().is_some_and(|model| model.supports_images());
|
let supports_images = self.model().is_some_and(|model| model.supports_images());
|
||||||
let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
|
let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
|
||||||
log::debug!("Running tool {}", tool_use.name);
|
log::info!("Running tool {}", tool_use.name);
|
||||||
Some(cx.foreground_executor().spawn(async move {
|
Some(cx.foreground_executor().spawn(async move {
|
||||||
let tool_result = tool_result.await.and_then(|output| {
|
let tool_result = tool_result.await.and_then(|output| {
|
||||||
if let LanguageModelToolResultContent::Image(_) = &output.llm_output
|
if let LanguageModelToolResultContent::Image(_) = &output.llm_output
|
||||||
|
@ -1567,7 +1556,7 @@ impl Thread {
|
||||||
tool_name: tool_use.name,
|
tool_name: tool_use.name,
|
||||||
is_error: true,
|
is_error: true,
|
||||||
content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())),
|
content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())),
|
||||||
output: Some(error.to_string().into()),
|
output: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
@ -1653,7 +1642,7 @@ impl Thread {
|
||||||
summary.extend(lines.next());
|
summary.extend(lines.next());
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Setting summary: {}", summary);
|
log::info!("Setting summary: {}", summary);
|
||||||
let summary = SharedString::from(summary);
|
let summary = SharedString::from(summary);
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
@ -1670,7 +1659,7 @@ impl Thread {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
log::debug!(
|
log::info!(
|
||||||
"Generating title with model: {:?}",
|
"Generating title with model: {:?}",
|
||||||
self.summarization_model.as_ref().map(|model| model.name())
|
self.summarization_model.as_ref().map(|model| model.name())
|
||||||
);
|
);
|
||||||
|
@ -1756,10 +1745,6 @@ impl Thread {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if message.content.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for content in &message.content {
|
for content in &message.content {
|
||||||
let AgentMessageContent::ToolUse(tool_use) = content else {
|
let AgentMessageContent::ToolUse(tool_use) = content else {
|
||||||
continue;
|
continue;
|
||||||
|
@ -1788,7 +1773,7 @@ impl Thread {
|
||||||
pub(crate) fn build_completion_request(
|
pub(crate) fn build_completion_request(
|
||||||
&self,
|
&self,
|
||||||
completion_intent: CompletionIntent,
|
completion_intent: CompletionIntent,
|
||||||
cx: &App,
|
cx: &mut App,
|
||||||
) -> Result<LanguageModelRequest> {
|
) -> Result<LanguageModelRequest> {
|
||||||
let model = self.model().context("No language model configured")?;
|
let model = self.model().context("No language model configured")?;
|
||||||
let tools = if let Some(turn) = self.running_turn.as_ref() {
|
let tools = if let Some(turn) = self.running_turn.as_ref() {
|
||||||
|
@ -1812,8 +1797,8 @@ impl Thread {
|
||||||
log::debug!("Completion mode: {:?}", self.completion_mode);
|
log::debug!("Completion mode: {:?}", self.completion_mode);
|
||||||
|
|
||||||
let messages = self.build_request_messages(cx);
|
let messages = self.build_request_messages(cx);
|
||||||
log::debug!("Request will include {} messages", messages.len());
|
log::info!("Request will include {} messages", messages.len());
|
||||||
log::debug!("Request includes {} tools", tools.len());
|
log::info!("Request includes {} tools", tools.len());
|
||||||
|
|
||||||
let request = LanguageModelRequest {
|
let request = LanguageModelRequest {
|
||||||
thread_id: Some(self.id.to_string()),
|
thread_id: Some(self.id.to_string()),
|
||||||
|
@ -1909,31 +1894,23 @@ impl Thread {
|
||||||
"Building request messages from {} thread messages",
|
"Building request messages from {} thread messages",
|
||||||
self.messages.len()
|
self.messages.len()
|
||||||
);
|
);
|
||||||
|
let mut messages = vec![self.build_system_message(cx)];
|
||||||
let system_prompt = SystemPromptTemplate {
|
|
||||||
project: self.project_context.read(cx),
|
|
||||||
available_tools: self.tools.keys().cloned().collect(),
|
|
||||||
}
|
|
||||||
.render(&self.templates)
|
|
||||||
.context("failed to build system prompt")
|
|
||||||
.expect("Invalid template");
|
|
||||||
let mut messages = vec![LanguageModelRequestMessage {
|
|
||||||
role: Role::System,
|
|
||||||
content: vec![system_prompt.into()],
|
|
||||||
cache: false,
|
|
||||||
}];
|
|
||||||
for message in &self.messages {
|
for message in &self.messages {
|
||||||
messages.extend(message.to_request());
|
messages.extend(message.to_request());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(last_message) = messages.last_mut() {
|
|
||||||
last_message.cache = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(message) = self.pending_message.as_ref() {
|
if let Some(message) = self.pending_message.as_ref() {
|
||||||
messages.extend(message.to_request());
|
messages.extend(message.to_request());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(last_user_message) = messages
|
||||||
|
.iter_mut()
|
||||||
|
.rev()
|
||||||
|
.find(|message| message.role == Role::User)
|
||||||
|
{
|
||||||
|
last_user_message.cache = true;
|
||||||
|
}
|
||||||
|
|
||||||
messages
|
messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2469,30 +2446,6 @@ impl ToolCallEventStreamReceiver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields {
|
|
||||||
let event = self.0.next().await;
|
|
||||||
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
|
|
||||||
update,
|
|
||||||
)))) = event
|
|
||||||
{
|
|
||||||
update.fields
|
|
||||||
} else {
|
|
||||||
panic!("Expected update fields but got: {:?}", event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn expect_diff(&mut self) -> Entity<acp_thread::Diff> {
|
|
||||||
let event = self.0.next().await;
|
|
||||||
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff(
|
|
||||||
update,
|
|
||||||
)))) = event
|
|
||||||
{
|
|
||||||
update.diff
|
|
||||||
} else {
|
|
||||||
panic!("Expected diff but got: {:?}", event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> {
|
pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> {
|
||||||
let event = self.0.next().await;
|
let event = self.0.next().await;
|
||||||
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(
|
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(
|
||||||
|
|
|
@ -273,13 +273,6 @@ impl AgentTool for EditFileTool {
|
||||||
|
|
||||||
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
||||||
event_stream.update_diff(diff.clone());
|
event_stream.update_diff(diff.clone());
|
||||||
let _finalize_diff = util::defer({
|
|
||||||
let diff = diff.downgrade();
|
|
||||||
let mut cx = cx.clone();
|
|
||||||
move || {
|
|
||||||
diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||||
let old_text = cx
|
let old_text = cx
|
||||||
|
@ -396,6 +389,8 @@ impl AgentTool for EditFileTool {
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
|
||||||
|
|
||||||
let input_path = input.path.display();
|
let input_path = input.path.display();
|
||||||
if unified_diff.is_empty() {
|
if unified_diff.is_empty() {
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
|
@ -1550,100 +1545,6 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_diff_finalization(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
let fs = project::FakeFs::new(cx.executor());
|
|
||||||
fs.insert_tree("/", json!({"main.rs": ""})).await;
|
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
|
|
||||||
let languages = project.read_with(cx, |project, _cx| project.languages().clone());
|
|
||||||
let context_server_registry =
|
|
||||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
|
||||||
let model = Arc::new(FakeLanguageModel::default());
|
|
||||||
let thread = cx.new(|cx| {
|
|
||||||
Thread::new(
|
|
||||||
project.clone(),
|
|
||||||
cx.new(|_cx| ProjectContext::default()),
|
|
||||||
context_server_registry.clone(),
|
|
||||||
Templates::new(),
|
|
||||||
Some(model.clone()),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure the diff is finalized after the edit completes.
|
|
||||||
{
|
|
||||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
|
||||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
|
||||||
let edit = cx.update(|cx| {
|
|
||||||
tool.run(
|
|
||||||
EditFileToolInput {
|
|
||||||
display_description: "Edit file".into(),
|
|
||||||
path: path!("/main.rs").into(),
|
|
||||||
mode: EditFileMode::Edit,
|
|
||||||
},
|
|
||||||
stream_tx,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
stream_rx.expect_update_fields().await;
|
|
||||||
let diff = stream_rx.expect_diff().await;
|
|
||||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
|
||||||
cx.run_until_parked();
|
|
||||||
model.end_last_completion_stream();
|
|
||||||
edit.await.unwrap();
|
|
||||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the diff is finalized if an error occurs while editing.
|
|
||||||
{
|
|
||||||
model.forbid_requests();
|
|
||||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
|
||||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
|
||||||
let edit = cx.update(|cx| {
|
|
||||||
tool.run(
|
|
||||||
EditFileToolInput {
|
|
||||||
display_description: "Edit file".into(),
|
|
||||||
path: path!("/main.rs").into(),
|
|
||||||
mode: EditFileMode::Edit,
|
|
||||||
},
|
|
||||||
stream_tx,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
stream_rx.expect_update_fields().await;
|
|
||||||
let diff = stream_rx.expect_diff().await;
|
|
||||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
|
||||||
edit.await.unwrap_err();
|
|
||||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
|
||||||
model.allow_requests();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the diff is finalized if the tool call gets dropped.
|
|
||||||
{
|
|
||||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
|
||||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
|
||||||
let edit = cx.update(|cx| {
|
|
||||||
tool.run(
|
|
||||||
EditFileToolInput {
|
|
||||||
display_description: "Edit file".into(),
|
|
||||||
path: path!("/main.rs").into(),
|
|
||||||
mode: EditFileMode::Edit,
|
|
||||||
},
|
|
||||||
stream_tx,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
stream_rx.expect_update_fields().await;
|
|
||||||
let diff = stream_rx.expect_diff().await;
|
|
||||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
|
||||||
drop(edit);
|
|
||||||
cx.run_until_parked();
|
|
||||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_test(cx: &mut TestAppContext) {
|
fn init_test(cx: &mut TestAppContext) {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
|
|
@ -136,17 +136,12 @@ impl AgentTool for FetchTool {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
input: Self::Input,
|
input: Self::Input,
|
||||||
event_stream: ToolCallEventStream,
|
_event_stream: ToolCallEventStream,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<Self::Output>> {
|
) -> Task<Result<Self::Output>> {
|
||||||
let authorize = event_stream.authorize(input.url.clone(), cx);
|
|
||||||
|
|
||||||
let text = cx.background_spawn({
|
let text = cx.background_spawn({
|
||||||
let http_client = self.http_client.clone();
|
let http_client = self.http_client.clone();
|
||||||
async move {
|
async move { Self::build_message(http_client, &input.url).await }
|
||||||
authorize.await?;
|
|
||||||
Self::build_message(http_client, &input.url).await
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.foreground_executor().spawn(async move {
|
cx.foreground_executor().spawn(async move {
|
||||||
|
|
|
@ -165,17 +165,16 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let mut results = Vec::new();
|
Ok(snapshots
|
||||||
for snapshot in snapshots {
|
.iter()
|
||||||
for entry in snapshot.entries(false, 0) {
|
.flat_map(|snapshot| {
|
||||||
let root_name = PathBuf::from(snapshot.root_name());
|
let root_name = PathBuf::from(snapshot.root_name());
|
||||||
if path_matcher.is_match(root_name.join(&entry.path)) {
|
snapshot
|
||||||
results.push(snapshot.abs_path().join(entry.path.as_ref()));
|
.entries(false, 0)
|
||||||
}
|
.map(move |entry| root_name.join(&entry.path))
|
||||||
}
|
.filter(|path| path_matcher.is_match(&path))
|
||||||
}
|
})
|
||||||
|
.collect())
|
||||||
Ok(results)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,8 +215,8 @@ mod test {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
matches,
|
matches,
|
||||||
&[
|
&[
|
||||||
PathBuf::from(path!("/root/apple/banana/carrot")),
|
PathBuf::from("root/apple/banana/carrot"),
|
||||||
PathBuf::from(path!("/root/apple/bandana/carbonara"))
|
PathBuf::from("root/apple/bandana/carbonara")
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -228,8 +227,8 @@ mod test {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
matches,
|
matches,
|
||||||
&[
|
&[
|
||||||
PathBuf::from(path!("/root/apple/banana/carrot")),
|
PathBuf::from("root/apple/banana/carrot"),
|
||||||
PathBuf::from(path!("/root/apple/bandana/carbonara"))
|
PathBuf::from("root/apple/bandana/carbonara")
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{path::Path, sync::Arc};
|
use std::sync::Arc;
|
||||||
use util::markdown::MarkdownCodeBlock;
|
|
||||||
|
|
||||||
use crate::{AgentTool, ToolCallEventStream};
|
use crate::{AgentTool, ToolCallEventStream};
|
||||||
|
|
||||||
|
@ -69,12 +68,27 @@ impl AgentTool for ReadFileTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||||
input
|
if let Ok(input) = input {
|
||||||
.ok()
|
let path = &input.path;
|
||||||
.as_ref()
|
match (input.start_line, input.end_line) {
|
||||||
.and_then(|input| Path::new(&input.path).file_name())
|
(Some(start), Some(end)) => {
|
||||||
.map(|file_name| file_name.to_string_lossy().to_string().into())
|
format!(
|
||||||
.unwrap_or_default()
|
"[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
|
||||||
|
path, start, end, path, start, end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
(Some(start), None) => {
|
||||||
|
format!(
|
||||||
|
"[Read file `{}` (from line {})](@selection:{}:({}-{}))",
|
||||||
|
path, start, path, start, start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => format!("[Read file `{}`](@file:{})", path, path),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
"Read file".into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
|
@ -244,19 +258,6 @@ impl AgentTool for ReadFileTool {
|
||||||
}]),
|
}]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
|
|
||||||
let markdown = MarkdownCodeBlock {
|
|
||||||
tag: &input.path,
|
|
||||||
text,
|
|
||||||
}
|
|
||||||
.to_string();
|
|
||||||
event_stream.update_fields(ToolCallUpdateFields {
|
|
||||||
content: Some(vec![acp::ToolCallContent::Content {
|
|
||||||
content: markdown.into(),
|
|
||||||
}]),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
|
@ -162,34 +162,12 @@ impl AgentConnection for AcpConnection {
|
||||||
let conn = self.connection.clone();
|
let conn = self.connection.clone();
|
||||||
let sessions = self.sessions.clone();
|
let sessions = self.sessions.clone();
|
||||||
let cwd = cwd.to_path_buf();
|
let cwd = cwd.to_path_buf();
|
||||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
|
||||||
let mcp_servers = context_server_store
|
|
||||||
.configured_server_ids()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|id| {
|
|
||||||
let configuration = context_server_store.configuration_for_server(id)?;
|
|
||||||
let command = configuration.command();
|
|
||||||
Some(acp::McpServer {
|
|
||||||
name: id.0.to_string(),
|
|
||||||
command: command.path.clone(),
|
|
||||||
args: command.args.clone(),
|
|
||||||
env: if let Some(env) = command.env.as_ref() {
|
|
||||||
env.iter()
|
|
||||||
.map(|(name, value)| acp::EnvVariable {
|
|
||||||
name: name.clone(),
|
|
||||||
value: value.clone(),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let response = conn
|
let response = conn
|
||||||
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
|
.new_session(acp::NewSessionRequest {
|
||||||
|
mcp_servers: vec![],
|
||||||
|
cwd,
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||||
|
@ -207,16 +185,13 @@ impl AgentConnection for AcpConnection {
|
||||||
|
|
||||||
let session_id = response.session_id;
|
let session_id = response.session_id;
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||||
let thread = cx.new(|cx| {
|
let thread = cx.new(|_cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
self.server_name.clone(),
|
self.server_name.clone(),
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
|
|
||||||
watch::Receiver::constant(self.prompt_capabilities),
|
|
||||||
cx,
|
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -288,9 +263,7 @@ impl AgentConnection for AcpConnection {
|
||||||
|
|
||||||
match serde_json::from_value(data.clone()) {
|
match serde_json::from_value(data.clone()) {
|
||||||
Ok(ErrorDetails { details }) => {
|
Ok(ErrorDetails { details }) => {
|
||||||
if suppress_abort_err
|
if suppress_abort_err && details.contains("This operation was aborted")
|
||||||
&& (details.contains("This operation was aborted")
|
|
||||||
|| details.contains("The user aborted a request"))
|
|
||||||
{
|
{
|
||||||
Ok(acp::PromptResponse {
|
Ok(acp::PromptResponse {
|
||||||
stop_reason: acp::StopReason::Cancelled,
|
stop_reason: acp::StopReason::Cancelled,
|
||||||
|
@ -306,6 +279,10 @@ impl AgentConnection for AcpConnection {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||||
|
self.prompt_capabilities
|
||||||
|
}
|
||||||
|
|
||||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||||
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
||||||
session.suppress_abort_err = true;
|
session.suppress_abort_err = true;
|
||||||
|
|
524
crates/agent_servers/src/acp/v0.rs
Normal file
524
crates/agent_servers/src/acp/v0.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
376
crates/agent_servers/src/acp/v1.rs
Normal file
376
crates/agent_servers/src/acp/v1.rs
Normal 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(¬ification.session_id)
|
||||||
|
.context("Failed to get session")?;
|
||||||
|
|
||||||
|
session.thread.update(cx, |thread, cx| {
|
||||||
|
thread.handle_session_update(notification.update, cx)
|
||||||
|
})??;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,7 +36,6 @@ pub trait AgentServer: Send {
|
||||||
fn name(&self) -> SharedString;
|
fn name(&self) -> SharedString;
|
||||||
fn empty_state_headline(&self) -> SharedString;
|
fn empty_state_headline(&self) -> SharedString;
|
||||||
fn empty_state_message(&self) -> SharedString;
|
fn empty_state_message(&self) -> SharedString;
|
||||||
fn telemetry_id(&self) -> &'static str;
|
|
||||||
|
|
||||||
fn connect(
|
fn connect(
|
||||||
&self,
|
&self,
|
||||||
|
@ -98,7 +97,7 @@ pub struct AgentServerCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentServerCommand {
|
impl AgentServerCommand {
|
||||||
pub async fn resolve(
|
pub(crate) async fn resolve(
|
||||||
path_bin_name: &'static str,
|
path_bin_name: &'static str,
|
||||||
extra_args: &[&'static str],
|
extra_args: &[&'static str],
|
||||||
fallback_path: Option<&Path>,
|
fallback_path: Option<&Path>,
|
||||||
|
|
|
@ -43,10 +43,6 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
|
||||||
pub struct ClaudeCode;
|
pub struct ClaudeCode;
|
||||||
|
|
||||||
impl AgentServer for ClaudeCode {
|
impl AgentServer for ClaudeCode {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
|
||||||
"claude-code"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Claude Code".into()
|
"Claude Code".into()
|
||||||
}
|
}
|
||||||
|
@ -253,19 +249,13 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||||
});
|
});
|
||||||
|
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||||
let thread = cx.new(|cx| {
|
let thread = cx.new(|_cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
"Claude Code",
|
"Claude Code",
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
watch::Receiver::constant(acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: false,
|
|
||||||
embedded_context: true,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -329,6 +319,14 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||||
cx.foreground_executor().spawn(async move { end_rx.await? })
|
cx.foreground_executor().spawn(async move { end_rx.await? })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||||
|
acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: false,
|
||||||
|
embedded_context: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
|
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
|
||||||
let sessions = self.sessions.borrow();
|
let sessions = self.sessions.borrow();
|
||||||
let Some(session) = sessions.get(session_id) else {
|
let Some(session) = sessions.get(session_id) else {
|
||||||
|
|
|
@ -22,10 +22,6 @@ impl CustomAgentServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::AgentServer for CustomAgentServer {
|
impl crate::AgentServer for CustomAgentServer {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
|
||||||
"custom"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
self.name.clone()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,6 @@ pub struct Gemini;
|
||||||
const ACP_ARG: &str = "--experimental-acp";
|
const ACP_ARG: &str = "--experimental-acp";
|
||||||
|
|
||||||
impl AgentServer for Gemini {
|
impl AgentServer for Gemini {
|
||||||
fn telemetry_id(&self) -> &'static str {
|
|
||||||
"gemini-cli"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Gemini CLI".into()
|
"Gemini CLI".into()
|
||||||
}
|
}
|
||||||
|
@ -57,7 +53,7 @@ impl AgentServer for Gemini {
|
||||||
return Err(LoadError::NotInstalled {
|
return Err(LoadError::NotInstalled {
|
||||||
error_message: "Failed to find Gemini CLI binary".into(),
|
error_message: "Failed to find Gemini CLI binary".into(),
|
||||||
install_message: "Install Gemini CLI".into(),
|
install_message: "Install Gemini CLI".into(),
|
||||||
install_command: Self::install_command().into(),
|
install_command: "npm install -g @google/gemini-cli@preview".into()
|
||||||
}.into());
|
}.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -92,7 +88,7 @@ impl AgentServer for Gemini {
|
||||||
current_version
|
current_version
|
||||||
).into(),
|
).into(),
|
||||||
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
||||||
upgrade_command: Self::upgrade_command().into(),
|
upgrade_command: "npm install -g @google/gemini-cli@preview".into(),
|
||||||
}.into())
|
}.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,20 +101,6 @@ impl AgentServer for Gemini {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gemini {
|
|
||||||
pub fn binary_name() -> &'static str {
|
|
||||||
"gemini"
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn install_command() -> &'static str {
|
|
||||||
"npm install -g @google/gemini-cli@preview"
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn upgrade_command() -> &'static str {
|
|
||||||
"npm install -g @google/gemini-cli@preview"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -6,7 +6,7 @@ use agent2::HistoryStore;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
|
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
|
||||||
TextStyleRefinement, WeakEntity, Window,
|
TextStyleRefinement, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use language::language_settings::SoftWrap;
|
use language::language_settings::SoftWrap;
|
||||||
|
@ -154,22 +154,10 @@ impl EntryViewState {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AgentThreadEntry::AssistantMessage(message) => {
|
AgentThreadEntry::AssistantMessage(_) => {
|
||||||
let entry = if let Some(Entry::AssistantMessage(entry)) =
|
if index == self.entries.len() {
|
||||||
self.entries.get_mut(index)
|
self.entries.push(Entry::empty())
|
||||||
{
|
}
|
||||||
entry
|
|
||||||
} else {
|
|
||||||
self.set_entry(
|
|
||||||
index,
|
|
||||||
Entry::AssistantMessage(AssistantMessageEntry::default()),
|
|
||||||
);
|
|
||||||
let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
entry
|
|
||||||
};
|
|
||||||
entry.sync(message);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -189,7 +177,7 @@ impl EntryViewState {
|
||||||
pub fn settings_changed(&mut self, cx: &mut App) {
|
pub fn settings_changed(&mut self, cx: &mut App) {
|
||||||
for entry in self.entries.iter() {
|
for entry in self.entries.iter() {
|
||||||
match entry {
|
match entry {
|
||||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
Entry::UserMessage { .. } => {}
|
||||||
Entry::Content(response_views) => {
|
Entry::Content(response_views) => {
|
||||||
for view in response_views.values() {
|
for view in response_views.values() {
|
||||||
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
|
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
|
||||||
|
@ -220,29 +208,9 @@ pub enum ViewEvent {
|
||||||
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
|
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct AssistantMessageEntry {
|
|
||||||
scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AssistantMessageEntry {
|
|
||||||
pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> {
|
|
||||||
self.scroll_handles_by_chunk_index.get(&ix).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
|
|
||||||
if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
|
|
||||||
let ix = message.chunks.len() - 1;
|
|
||||||
let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
|
|
||||||
handle.scroll_to_bottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Entry {
|
pub enum Entry {
|
||||||
UserMessage(Entity<MessageEditor>),
|
UserMessage(Entity<MessageEditor>),
|
||||||
AssistantMessage(AssistantMessageEntry),
|
|
||||||
Content(HashMap<EntityId, AnyEntity>),
|
Content(HashMap<EntityId, AnyEntity>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,7 +218,7 @@ impl Entry {
|
||||||
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
||||||
match self {
|
match self {
|
||||||
Self::UserMessage(editor) => Some(editor),
|
Self::UserMessage(editor) => Some(editor),
|
||||||
Self::AssistantMessage(_) | Self::Content(_) => None,
|
Entry::Content(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,16 +239,6 @@ impl Entry {
|
||||||
.map(|entity| entity.downcast::<TerminalView>().unwrap())
|
.map(|entity| entity.downcast::<TerminalView>().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_handle_for_assistant_message_chunk(
|
|
||||||
&self,
|
|
||||||
chunk_ix: usize,
|
|
||||||
) -> Option<ScrollHandle> {
|
|
||||||
match self {
|
|
||||||
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
|
|
||||||
Self::UserMessage(_) | Self::Content(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
|
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Content(map) => Some(map),
|
Self::Content(map) => Some(map),
|
||||||
|
@ -296,7 +254,7 @@ impl Entry {
|
||||||
pub fn has_content(&self) -> bool {
|
pub fn has_content(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Content(map) => !map.is_empty(),
|
Self::Content(map) => !map.is_empty(),
|
||||||
Self::UserMessage(_) | Self::AssistantMessage(_) => false,
|
Self::UserMessage(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,6 @@ pub enum MessageEditorEvent {
|
||||||
Send,
|
Send,
|
||||||
Cancel,
|
Cancel,
|
||||||
Focus,
|
Focus,
|
||||||
LostFocus,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||||
|
@ -132,14 +131,10 @@ impl MessageEditor {
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
|
cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| {
|
||||||
cx.emit(MessageEditorEvent::Focus)
|
cx.emit(MessageEditorEvent::Focus)
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
|
|
||||||
cx.emit(MessageEditorEvent::LostFocus)
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
let mut subscriptions = Vec::new();
|
let mut subscriptions = Vec::new();
|
||||||
subscriptions.push(cx.subscribe_in(&editor, window, {
|
subscriptions.push(cx.subscribe_in(&editor, window, {
|
||||||
|
@ -373,7 +368,7 @@ impl MessageEditor {
|
||||||
|
|
||||||
if Img::extensions().contains(&extension) && !extension.contains("svg") {
|
if Img::extensions().contains(&extension) && !extension.contains("svg") {
|
||||||
if !self.prompt_capabilities.get().image {
|
if !self.prompt_capabilities.get().image {
|
||||||
return Task::ready(Err(anyhow!("This model does not support images yet")));
|
return Task::ready(Err(anyhow!("This agent does not support images yet")));
|
||||||
}
|
}
|
||||||
let task = self
|
let task = self
|
||||||
.project
|
.project
|
||||||
|
@ -1174,16 +1169,17 @@ impl MessageEditor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text(&self, cx: &App) -> String {
|
|
||||||
self.editor.read(cx).text(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
editor.set_text(text, window, cx);
|
editor.set_text(text, window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn text(&self, cx: &App) -> String {
|
||||||
|
self.editor.read(cx).text(cx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
|
fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
|
||||||
|
|
|
@ -462,7 +462,7 @@ impl AcpThreadHistory {
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}))
|
}))
|
||||||
.end_slot::<IconButton>(if hovered {
|
.end_slot::<IconButton>(if hovered || selected {
|
||||||
Some(
|
Some(
|
||||||
IconButton::new("delete", IconName::Trash)
|
IconButton::new("delete", IconName::Trash)
|
||||||
.shape(IconButtonShape::Square)
|
.shape(IconButtonShape::Square)
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,23 +3,19 @@ mod configure_context_server_modal;
|
||||||
mod manage_profiles_modal;
|
mod manage_profiles_modal;
|
||||||
mod tool_picker;
|
mod tool_picker;
|
||||||
|
|
||||||
use std::{ops::Range, sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini};
|
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::Result;
|
|
||||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||||
use cloud_llm_client::Plan;
|
use cloud_llm_client::Plan;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use context_server::ContextServerId;
|
use context_server::ContextServerId;
|
||||||
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
|
|
||||||
use extension::ExtensionManifest;
|
use extension::ExtensionManifest;
|
||||||
use extension_host::ExtensionStore;
|
use extension_host::ExtensionStore;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
|
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
|
||||||
EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
|
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||||
WeakEntity, percentage,
|
|
||||||
};
|
};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
|
@ -27,24 +23,23 @@ use language_model::{
|
||||||
};
|
};
|
||||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||||
use project::{
|
use project::{
|
||||||
Project,
|
|
||||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||||
project_settings::{ContextServerSettings, ProjectSettings},
|
project_settings::{ContextServerSettings, ProjectSettings},
|
||||||
};
|
};
|
||||||
use settings::{Settings, SettingsStore, update_settings_file};
|
use settings::{Settings, update_settings_file};
|
||||||
use ui::{
|
use ui::{
|
||||||
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
||||||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::{Workspace, create_and_open_local_file};
|
use workspace::Workspace;
|
||||||
use zed_actions::ExtensionCategoryFilter;
|
use zed_actions::ExtensionCategoryFilter;
|
||||||
|
|
||||||
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||||
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer, ExternalAgent, NewExternalAgentThread,
|
AddContextServer,
|
||||||
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -52,7 +47,6 @@ pub struct AgentConfiguration {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||||
context_server_store: Entity<ContextServerStore>,
|
context_server_store: Entity<ContextServerStore>,
|
||||||
|
@ -62,8 +56,6 @@ pub struct AgentConfiguration {
|
||||||
_registry_subscription: Subscription,
|
_registry_subscription: Subscription,
|
||||||
scroll_handle: ScrollHandle,
|
scroll_handle: ScrollHandle,
|
||||||
scrollbar_state: ScrollbarState,
|
scrollbar_state: ScrollbarState,
|
||||||
gemini_is_installed: bool,
|
|
||||||
_check_for_gemini: Task<()>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentConfiguration {
|
impl AgentConfiguration {
|
||||||
|
@ -73,7 +65,6 @@ impl AgentConfiguration {
|
||||||
tools: Entity<ToolWorkingSet>,
|
tools: Entity<ToolWorkingSet>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -98,11 +89,6 @@ impl AgentConfiguration {
|
||||||
|
|
||||||
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
|
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
|
||||||
.detach();
|
.detach();
|
||||||
cx.observe_global_in::<SettingsStore>(window, |this, _, cx| {
|
|
||||||
this.check_for_gemini(cx);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
let scroll_handle = ScrollHandle::new();
|
let scroll_handle = ScrollHandle::new();
|
||||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||||
|
@ -111,7 +97,6 @@ impl AgentConfiguration {
|
||||||
fs,
|
fs,
|
||||||
language_registry,
|
language_registry,
|
||||||
workspace,
|
workspace,
|
||||||
project,
|
|
||||||
focus_handle,
|
focus_handle,
|
||||||
configuration_views_by_provider: HashMap::default(),
|
configuration_views_by_provider: HashMap::default(),
|
||||||
context_server_store,
|
context_server_store,
|
||||||
|
@ -121,11 +106,8 @@ impl AgentConfiguration {
|
||||||
_registry_subscription: registry_subscription,
|
_registry_subscription: registry_subscription,
|
||||||
scroll_handle,
|
scroll_handle,
|
||||||
scrollbar_state,
|
scrollbar_state,
|
||||||
gemini_is_installed: false,
|
|
||||||
_check_for_gemini: Task::ready(()),
|
|
||||||
};
|
};
|
||||||
this.build_provider_configuration_views(window, cx);
|
this.build_provider_configuration_views(window, cx);
|
||||||
this.check_for_gemini(cx);
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,34 +137,6 @@ impl AgentConfiguration {
|
||||||
self.configuration_views_by_provider
|
self.configuration_views_by_provider
|
||||||
.insert(provider.id(), configuration_view);
|
.insert(provider.id(), configuration_view);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_for_gemini(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let project = self.project.clone();
|
|
||||||
let settings = AllAgentServersSettings::get_global(cx).clone();
|
|
||||||
self._check_for_gemini = cx.spawn({
|
|
||||||
async move |this, cx| {
|
|
||||||
let Some(project) = project.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let gemini_is_installed = AgentServerCommand::resolve(
|
|
||||||
Gemini::binary_name(),
|
|
||||||
&[],
|
|
||||||
// TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here
|
|
||||||
None,
|
|
||||||
settings.gemini,
|
|
||||||
&project,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.is_some();
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.gemini_is_installed = gemini_is_installed;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for AgentConfiguration {
|
impl Focusable for AgentConfiguration {
|
||||||
|
@ -257,6 +211,7 @@ impl AgentConfiguration {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(provider_id_string.clone())
|
.id(provider_id_string.clone())
|
||||||
|
.cursor_pointer()
|
||||||
.px_2()
|
.px_2()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
@ -276,7 +231,10 @@ impl AgentConfiguration {
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Label::new(provider_name.clone()))
|
.child(
|
||||||
|
Label::new(provider_name.clone())
|
||||||
|
.size(LabelSize::Large),
|
||||||
|
)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if is_zed_provider && is_signed_in {
|
if is_zed_provider && is_signed_in {
|
||||||
this.child(
|
this.child(
|
||||||
|
@ -321,7 +279,7 @@ impl AgentConfiguration {
|
||||||
"Start New Thread",
|
"Start New Thread",
|
||||||
)
|
)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.icon(IconName::Thread)
|
.icon(IconName::Plus)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
|
@ -420,7 +378,7 @@ impl AgentConfiguration {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
|
Label::new("Add at least one provider to use AI-powered features.")
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -561,14 +519,6 @@ impl AgentConfiguration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
|
|
||||||
cx.theme().colors().background.opacity(0.25)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
|
|
||||||
cx.theme().colors().border.opacity(0.6)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_context_servers_section(
|
fn render_context_servers_section(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
@ -586,12 +536,7 @@ impl AgentConfiguration {
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
||||||
.child(
|
.child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)),
|
||||||
Label::new(
|
|
||||||
"All context servers connected through the Model Context Protocol.",
|
|
||||||
)
|
|
||||||
.color(Color::Muted),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.children(
|
.children(
|
||||||
context_server_ids.into_iter().map(|context_server_id| {
|
context_server_ids.into_iter().map(|context_server_id| {
|
||||||
|
@ -601,7 +546,7 @@ impl AgentConfiguration {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.gap_1p5()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
h_flex().w_full().child(
|
h_flex().w_full().child(
|
||||||
Button::new("add-context-server", "Add Custom Server")
|
Button::new("add-context-server", "Add Custom Server")
|
||||||
|
@ -692,6 +637,8 @@ impl AgentConfiguration {
|
||||||
.map_or([].as_slice(), |tools| tools.as_slice());
|
.map_or([].as_slice(), |tools| tools.as_slice());
|
||||||
let tool_count = tools.len();
|
let tool_count = tools.len();
|
||||||
|
|
||||||
|
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||||
|
|
||||||
let (source_icon, source_tooltip) = if is_from_extension {
|
let (source_icon, source_tooltip) = if is_from_extension {
|
||||||
(
|
(
|
||||||
IconName::ZedMcpExtension,
|
IconName::ZedMcpExtension,
|
||||||
|
@ -834,8 +781,8 @@ impl AgentConfiguration {
|
||||||
.id(item_id.clone())
|
.id(item_id.clone())
|
||||||
.border_1()
|
.border_1()
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
.border_color(self.card_item_border_color(cx))
|
.border_color(border_color)
|
||||||
.bg(self.card_item_bg_color(cx))
|
.bg(cx.theme().colors().background.opacity(0.2))
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -843,11 +790,7 @@ impl AgentConfiguration {
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.when(
|
.when(
|
||||||
error.is_some() || are_tools_expanded && tool_count >= 1,
|
error.is_some() || are_tools_expanded && tool_count >= 1,
|
||||||
|element| {
|
|element| element.border_b_1().border_color(border_color),
|
||||||
element
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(self.card_item_border_color(cx))
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -1029,195 +972,6 @@ impl AgentConfiguration {
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let settings = AllAgentServersSettings::get_global(cx).clone();
|
|
||||||
let user_defined_agents = settings
|
|
||||||
.custom
|
|
||||||
.iter()
|
|
||||||
.map(|(name, settings)| {
|
|
||||||
self.render_agent_server(
|
|
||||||
IconName::Ai,
|
|
||||||
name.clone(),
|
|
||||||
ExternalAgent::Custom {
|
|
||||||
name: name.clone(),
|
|
||||||
settings: settings.clone(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().colors().border)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.p(DynamicSpacing::Base16.rems(cx))
|
|
||||||
.pr(DynamicSpacing::Base20.rems(cx))
|
|
||||||
.gap_2()
|
|
||||||
.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(
|
|
||||||
Label::new(
|
|
||||||
"Bring the agent of your choice to Zed via our new Agent Client Protocol.",
|
|
||||||
)
|
|
||||||
.color(Color::Muted),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(self.render_agent_server(
|
|
||||||
IconName::AiGemini,
|
|
||||||
"Gemini CLI",
|
|
||||||
ExternalAgent::Gemini,
|
|
||||||
(!self.gemini_is_installed).then_some(Gemini::install_command().into()),
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
// TODO add CC
|
|
||||||
.children(user_defined_agents),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_agent_server(
|
|
||||||
&self,
|
|
||||||
icon: IconName,
|
|
||||||
name: impl Into<SharedString>,
|
|
||||||
agent: ExternalAgent,
|
|
||||||
install_command: Option<SharedString>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let name = name.into();
|
|
||||||
h_flex()
|
|
||||||
.p_1()
|
|
||||||
.pl_2()
|
|
||||||
.gap_1p5()
|
|
||||||
.justify_between()
|
|
||||||
.border_1()
|
|
||||||
.rounded_md()
|
|
||||||
.border_color(self.card_item_border_color(cx))
|
|
||||||
.bg(self.card_item_bg_color(cx))
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
|
||||||
.child(Label::new(name.clone())),
|
|
||||||
)
|
|
||||||
.map(|this| {
|
|
||||||
if let Some(install_command) = install_command {
|
|
||||||
this.child(
|
|
||||||
Button::new(
|
|
||||||
SharedString::from(format!("install_external_agent-{name}")),
|
|
||||||
"Install Agent",
|
|
||||||
)
|
|
||||||
.label_size(LabelSize::Small)
|
|
||||||
.icon(IconName::Plus)
|
|
||||||
.icon_position(IconPosition::Start)
|
|
||||||
.icon_size(IconSize::XSmall)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.tooltip(Tooltip::text(install_command.clone()))
|
|
||||||
.on_click(cx.listener(
|
|
||||||
move |this, _, window, cx| {
|
|
||||||
let Some(project) = this.project.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(workspace) = this.workspace.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let cwd = project.read(cx).first_project_directory(cx);
|
|
||||||
let shell =
|
|
||||||
project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
|
||||||
let spawn_in_terminal = task::SpawnInTerminal {
|
|
||||||
id: task::TaskId(install_command.to_string()),
|
|
||||||
full_label: install_command.to_string(),
|
|
||||||
label: install_command.to_string(),
|
|
||||||
command: Some(install_command.to_string()),
|
|
||||||
args: Vec::new(),
|
|
||||||
command_label: install_command.to_string(),
|
|
||||||
cwd,
|
|
||||||
env: Default::default(),
|
|
||||||
use_new_terminal: true,
|
|
||||||
allow_concurrent_runs: true,
|
|
||||||
reveal: Default::default(),
|
|
||||||
reveal_target: Default::default(),
|
|
||||||
hide: Default::default(),
|
|
||||||
shell,
|
|
||||||
show_summary: true,
|
|
||||||
show_command: true,
|
|
||||||
show_rerun: false,
|
|
||||||
};
|
|
||||||
let task = workspace.update(cx, |workspace, cx| {
|
|
||||||
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
|
|
||||||
});
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
task.await;
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.check_for_gemini(cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.child(
|
|
||||||
h_flex().gap_1().child(
|
|
||||||
Button::new(
|
|
||||||
SharedString::from(format!("start_acp_thread-{name}")),
|
|
||||||
"Start New Thread",
|
|
||||||
)
|
|
||||||
.label_size(LabelSize::Small)
|
|
||||||
.icon(IconName::Thread)
|
|
||||||
.icon_position(IconPosition::Start)
|
|
||||||
.icon_size(IconSize::XSmall)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.on_click(move |_, window, cx| {
|
|
||||||
window.dispatch_action(
|
|
||||||
NewExternalAgentThread {
|
|
||||||
agent: Some(agent.clone()),
|
|
||||||
}
|
|
||||||
.boxed_clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for AgentConfiguration {
|
impl Render for AgentConfiguration {
|
||||||
|
@ -1237,7 +991,6 @@ impl Render for AgentConfiguration {
|
||||||
.size_full()
|
.size_full()
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
.child(self.render_general_settings_section(cx))
|
.child(self.render_general_settings_section(cx))
|
||||||
.child(self.render_agent_servers_section(cx))
|
|
||||||
.child(self.render_context_servers_section(window, cx))
|
.child(self.render_context_servers_section(window, cx))
|
||||||
.child(self.render_provider_configuration_section(cx)),
|
.child(self.render_provider_configuration_section(cx)),
|
||||||
)
|
)
|
||||||
|
@ -1356,109 +1109,3 @@ fn show_unable_to_uninstall_extension_with_context_server(
|
||||||
|
|
||||||
workspace.toggle_status_toast(status_toast, cx);
|
workspace.toggle_status_toast(status_toast, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn open_new_agent_servers_entry_in_settings_editor(
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
cx: &mut AsyncWindowContext,
|
|
||||||
) -> Result<()> {
|
|
||||||
let settings_editor = workspace
|
|
||||||
.update_in(cx, |_, window, cx| {
|
|
||||||
create_and_open_local_file(paths::settings_file(), window, cx, || {
|
|
||||||
settings::initial_user_settings_content().as_ref().into()
|
|
||||||
})
|
|
||||||
})?
|
|
||||||
.await?
|
|
||||||
.downcast::<Editor>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
settings_editor
|
|
||||||
.downgrade()
|
|
||||||
.update_in(cx, |item, window, cx| {
|
|
||||||
let text = item.buffer().read(cx).snapshot(cx).text();
|
|
||||||
|
|
||||||
let settings = cx.global::<SettingsStore>();
|
|
||||||
|
|
||||||
let mut unique_server_name = None;
|
|
||||||
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
|
|
||||||
let server_name: Option<SharedString> = (0..u8::MAX)
|
|
||||||
.map(|i| {
|
|
||||||
if i == 0 {
|
|
||||||
"your_agent".into()
|
|
||||||
} else {
|
|
||||||
format!("your_agent_{}", i).into()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.find(|name| !file.custom.contains_key(name));
|
|
||||||
if let Some(server_name) = server_name {
|
|
||||||
unique_server_name = Some(server_name.clone());
|
|
||||||
file.custom.insert(
|
|
||||||
server_name,
|
|
||||||
AgentServerSettings {
|
|
||||||
command: AgentServerCommand {
|
|
||||||
path: "path_to_executable".into(),
|
|
||||||
args: vec![],
|
|
||||||
env: Some(HashMap::default()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if edits.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ranges = edits
|
|
||||||
.iter()
|
|
||||||
.map(|(range, _)| range.clone())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
item.edit(edits, cx);
|
|
||||||
if let Some((unique_server_name, buffer)) =
|
|
||||||
unique_server_name.zip(item.buffer().read(cx).as_singleton())
|
|
||||||
{
|
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
|
||||||
if let Some(range) =
|
|
||||||
find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
|
|
||||||
{
|
|
||||||
item.change_selections(
|
|
||||||
SelectionEffects::scroll(Autoscroll::newest()),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
|selections| {
|
|
||||||
selections.select_ranges(vec![range]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_text_in_buffer(
|
|
||||||
text: &str,
|
|
||||||
start: usize,
|
|
||||||
snapshot: &language::BufferSnapshot,
|
|
||||||
) -> Option<Range<usize>> {
|
|
||||||
let chars = text.chars().collect::<Vec<char>>();
|
|
||||||
|
|
||||||
let mut offset = start;
|
|
||||||
let mut char_offset = 0;
|
|
||||||
for c in snapshot.chars_at(start) {
|
|
||||||
if char_offset >= chars.len() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
offset += 1;
|
|
||||||
|
|
||||||
if c == chars[char_offset] {
|
|
||||||
char_offset += 1;
|
|
||||||
} else {
|
|
||||||
char_offset = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if char_offset == chars.len() {
|
|
||||||
Some(offset.saturating_sub(chars.len())..offset)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1529,7 +1529,6 @@ impl AgentDiff {
|
||||||
| AcpThreadEvent::TokenUsageUpdated
|
| AcpThreadEvent::TokenUsageUpdated
|
||||||
| AcpThreadEvent::EntriesRemoved(_)
|
| AcpThreadEvent::EntriesRemoved(_)
|
||||||
| AcpThreadEvent::ToolAuthorizationRequired
|
| AcpThreadEvent::ToolAuthorizationRequired
|
||||||
| AcpThreadEvent::PromptCapabilitiesUpdated
|
|
||||||
| AcpThreadEvent::Retry(_) => {}
|
| AcpThreadEvent::Retry(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,9 @@ use agent_servers::AgentServerSettings;
|
||||||
use agent2::{DbThreadMetadata, HistoryEntry};
|
use agent2::{DbThreadMetadata, HistoryEntry};
|
||||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use zed_actions::OpenBrowser;
|
|
||||||
use zed_actions::agent::ReauthenticateAgent;
|
|
||||||
|
|
||||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||||
use crate::agent_diff::AgentDiffThread;
|
use crate::agent_diff::AgentDiffThread;
|
||||||
use crate::ui::AcpOnboardingModal;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||||
|
@ -78,10 +75,7 @@ use workspace::{
|
||||||
};
|
};
|
||||||
use zed_actions::{
|
use zed_actions::{
|
||||||
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
||||||
agent::{
|
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
|
||||||
OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
|
|
||||||
ToggleModelSelector,
|
|
||||||
},
|
|
||||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -205,9 +199,6 @@ pub fn init(cx: &mut App) {
|
||||||
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
|
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
|
||||||
AgentOnboardingModal::toggle(workspace, window, cx)
|
AgentOnboardingModal::toggle(workspace, window, cx)
|
||||||
})
|
})
|
||||||
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
|
|
||||||
AcpOnboardingModal::toggle(workspace, window, cx)
|
|
||||||
})
|
|
||||||
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
||||||
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
||||||
window.refresh();
|
window.refresh();
|
||||||
|
@ -249,7 +240,6 @@ enum WhichFontSize {
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO unify this with ExternalAgent
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum AgentType {
|
pub enum AgentType {
|
||||||
#[default]
|
#[default]
|
||||||
|
@ -598,6 +588,17 @@ impl AgentPanel {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Wait for the Gemini/Native feature flag to be available.
|
||||||
|
let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
|
||||||
|
if !client.status().borrow().is_signed_out() {
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
|
||||||
|
Duration::from_secs(2),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| {
|
let panel = cx.new(|cx| {
|
||||||
Self::new(
|
Self::new(
|
||||||
|
@ -1023,8 +1024,6 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
telemetry::event!("Agent Thread Started", agent = "zed-text");
|
|
||||||
|
|
||||||
let context = self
|
let context = self
|
||||||
.context_store
|
.context_store
|
||||||
.update(cx, |context_store, cx| context_store.create(cx));
|
.update(cx, |context_store, cx| context_store.create(cx));
|
||||||
|
@ -1117,8 +1116,6 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
|
|
||||||
|
|
||||||
let server = ext_agent.server(fs, history);
|
let server = ext_agent.server(fs, history);
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
@ -1476,7 +1473,6 @@ impl AgentPanel {
|
||||||
tools,
|
tools,
|
||||||
self.language_registry.clone(),
|
self.language_registry.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.project.downgrade(),
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -1848,6 +1844,19 @@ impl AgentPanel {
|
||||||
menu
|
menu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_selected_agent(
|
||||||
|
&mut self,
|
||||||
|
agent: AgentType,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if self.selected_agent != agent {
|
||||||
|
self.selected_agent = agent.clone();
|
||||||
|
self.serialize(cx);
|
||||||
|
}
|
||||||
|
self.new_agent_thread(agent, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selected_agent(&self) -> AgentType {
|
pub fn selected_agent(&self) -> AgentType {
|
||||||
self.selected_agent.clone()
|
self.selected_agent.clone()
|
||||||
}
|
}
|
||||||
|
@ -1858,11 +1867,6 @@ impl AgentPanel {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if self.selected_agent != agent {
|
|
||||||
self.selected_agent = agent.clone();
|
|
||||||
self.serialize(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
match agent {
|
match agent {
|
||||||
AgentType::Zed => {
|
AgentType::Zed => {
|
||||||
window.dispatch_action(
|
window.dispatch_action(
|
||||||
|
@ -2200,8 +2204,6 @@ impl AgentPanel {
|
||||||
"Enable Full Screen"
|
"Enable Full Screen"
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected_agent = self.selected_agent.clone();
|
|
||||||
|
|
||||||
PopoverMenu::new("agent-options-menu")
|
PopoverMenu::new("agent-options-menu")
|
||||||
.trigger_with_tooltip(
|
.trigger_with_tooltip(
|
||||||
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
||||||
|
@ -2281,11 +2283,6 @@ impl AgentPanel {
|
||||||
.action("Settings", Box::new(OpenSettings))
|
.action("Settings", Box::new(OpenSettings))
|
||||||
.separator()
|
.separator()
|
||||||
.action(full_screen_label, Box::new(ToggleZoom));
|
.action(full_screen_label, Box::new(ToggleZoom));
|
||||||
|
|
||||||
if selected_agent == AgentType::Gemini {
|
|
||||||
menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
|
|
||||||
}
|
|
||||||
|
|
||||||
menu
|
menu
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -2320,8 +2317,6 @@ impl AgentPanel {
|
||||||
.menu({
|
.menu({
|
||||||
let menu = self.assistant_navigation_menu.clone();
|
let menu = self.assistant_navigation_menu.clone();
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
telemetry::event!("View Thread History Clicked");
|
|
||||||
|
|
||||||
if let Some(menu) = menu.as_ref() {
|
if let Some(menu) = menu.as_ref() {
|
||||||
menu.update(cx, |_, cx| {
|
menu.update(cx, |_, cx| {
|
||||||
cx.defer_in(window, |menu, window, cx| {
|
cx.defer_in(window, |menu, window, cx| {
|
||||||
|
@ -2500,8 +2495,6 @@ impl AgentPanel {
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
|
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
telemetry::event!("New Thread Clicked");
|
|
||||||
|
|
||||||
let active_thread = active_thread.clone();
|
let active_thread = active_thread.clone();
|
||||||
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||||
menu = menu
|
menu = menu
|
||||||
|
@ -2543,7 +2536,7 @@ impl AgentPanel {
|
||||||
workspace.panel::<AgentPanel>(cx)
|
workspace.panel::<AgentPanel>(cx)
|
||||||
{
|
{
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.new_agent_thread(
|
panel.set_selected_agent(
|
||||||
AgentType::NativeAgent,
|
AgentType::NativeAgent,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2569,7 +2562,7 @@ impl AgentPanel {
|
||||||
workspace.panel::<AgentPanel>(cx)
|
workspace.panel::<AgentPanel>(cx)
|
||||||
{
|
{
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.new_agent_thread(
|
panel.set_selected_agent(
|
||||||
AgentType::TextThread,
|
AgentType::TextThread,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2597,7 +2590,7 @@ impl AgentPanel {
|
||||||
workspace.panel::<AgentPanel>(cx)
|
workspace.panel::<AgentPanel>(cx)
|
||||||
{
|
{
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.new_agent_thread(
|
panel.set_selected_agent(
|
||||||
AgentType::Gemini,
|
AgentType::Gemini,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2624,7 +2617,7 @@ impl AgentPanel {
|
||||||
workspace.panel::<AgentPanel>(cx)
|
workspace.panel::<AgentPanel>(cx)
|
||||||
{
|
{
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.new_agent_thread(
|
panel.set_selected_agent(
|
||||||
AgentType::ClaudeCode,
|
AgentType::ClaudeCode,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2657,7 +2650,7 @@ impl AgentPanel {
|
||||||
workspace.panel::<AgentPanel>(cx)
|
workspace.panel::<AgentPanel>(cx)
|
||||||
{
|
{
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.new_agent_thread(
|
panel.set_selected_agent(
|
||||||
AgentType::Custom {
|
AgentType::Custom {
|
||||||
name: agent_name
|
name: agent_name
|
||||||
.clone(),
|
.clone(),
|
||||||
|
@ -2678,15 +2671,6 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
menu
|
menu
|
||||||
})
|
|
||||||
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
|
|
||||||
menu.separator().link(
|
|
||||||
"Add Other Agents",
|
|
||||||
OpenBrowser {
|
|
||||||
url: zed_urls::external_agents_docs(cx),
|
|
||||||
}
|
|
||||||
.boxed_clone(),
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
menu
|
menu
|
||||||
}))
|
}))
|
||||||
|
@ -3767,11 +3751,6 @@ impl Render for AgentPanel {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.on_action(cx.listener(Self::toggle_burn_mode))
|
.on_action(cx.listener(Self::toggle_burn_mode))
|
||||||
.on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
|
|
||||||
if let Some(thread_view) = this.active_thread_view() {
|
|
||||||
thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.child(self.render_toolbar(window, cx))
|
.child(self.render_toolbar(window, cx))
|
||||||
.children(self.render_onboarding(window, cx))
|
.children(self.render_onboarding(window, cx))
|
||||||
.map(|parent| match &self.active_view {
|
.map(|parent| match &self.active_view {
|
||||||
|
|
|
@ -160,7 +160,6 @@ pub struct NewNativeAgentThreadFromSummary {
|
||||||
from_session_id: agent_client_protocol::SessionId,
|
from_session_id: agent_client_protocol::SessionId,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO unify this with AgentType
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
enum ExternalAgent {
|
enum ExternalAgent {
|
||||||
|
@ -175,15 +174,6 @@ enum ExternalAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExternalAgent {
|
impl ExternalAgent {
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::NativeAgent => "zed",
|
|
||||||
Self::Gemini => "gemini-cli",
|
|
||||||
Self::ClaudeCode => "claude-code",
|
|
||||||
Self::Custom { .. } => "custom",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn server(
|
pub fn server(
|
||||||
&self,
|
&self,
|
||||||
fs: Arc<dyn fs::Fs>,
|
fs: Arc<dyn fs::Fs>,
|
||||||
|
|
|
@ -6,8 +6,7 @@ use feature_flags::ZedProFeatureFlag;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||||
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||||
LanguageModelRegistry,
|
|
||||||
};
|
};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
|
@ -77,7 +76,6 @@ pub struct LanguageModelPickerDelegate {
|
||||||
all_models: Arc<GroupedModels>,
|
all_models: Arc<GroupedModels>,
|
||||||
filtered_entries: Vec<LanguageModelPickerEntry>,
|
filtered_entries: Vec<LanguageModelPickerEntry>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
_authenticate_all_providers_task: Task<()>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +96,6 @@ impl LanguageModelPickerDelegate {
|
||||||
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
|
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
|
||||||
filtered_entries: entries,
|
filtered_entries: entries,
|
||||||
get_active_model: Arc::new(get_active_model),
|
get_active_model: Arc::new(get_active_model),
|
||||||
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
|
|
||||||
_subscriptions: vec![cx.subscribe_in(
|
_subscriptions: vec![cx.subscribe_in(
|
||||||
&LanguageModelRegistry::global(cx),
|
&LanguageModelRegistry::global(cx),
|
||||||
window,
|
window,
|
||||||
|
@ -142,56 +139,6 @@ impl LanguageModelPickerDelegate {
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticates all providers in the [`LanguageModelRegistry`].
|
|
||||||
///
|
|
||||||
/// We do this so that we can populate the language selector with all of the
|
|
||||||
/// models from the configured providers.
|
|
||||||
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
|
|
||||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
|
||||||
.read(cx)
|
|
||||||
.providers()
|
|
||||||
.iter()
|
|
||||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
cx.spawn(async move |_cx| {
|
|
||||||
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
|
|
||||||
if let Err(err) = authenticate_task.await {
|
|
||||||
if matches!(err, AuthenticateError::CredentialsNotFound) {
|
|
||||||
// Since we're authenticating these providers in the
|
|
||||||
// background for the purposes of populating the
|
|
||||||
// language selector, we don't care about providers
|
|
||||||
// where the credentials are not found.
|
|
||||||
} else {
|
|
||||||
// Some providers have noisy failure states that we
|
|
||||||
// don't want to spam the logs with every time the
|
|
||||||
// language model selector is initialized.
|
|
||||||
//
|
|
||||||
// Ideally these should have more clear failure modes
|
|
||||||
// that we know are safe to ignore here, like what we do
|
|
||||||
// with `CredentialsNotFound` above.
|
|
||||||
match provider_id.0.as_ref() {
|
|
||||||
"lmstudio" | "ollama" => {
|
|
||||||
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
|
||||||
//
|
|
||||||
// These fail noisily, so we don't log them.
|
|
||||||
}
|
|
||||||
"copilot_chat" => {
|
|
||||||
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log::error!(
|
|
||||||
"Failed to authenticate provider: {}: {err}",
|
|
||||||
provider_name.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||||
(self.get_active_model)(cx)
|
(self.get_active_model)(cx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -361,7 +361,6 @@ impl TextThreadEditor {
|
||||||
if self.sending_disabled(cx) {
|
if self.sending_disabled(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
telemetry::event!("Agent Message Sent", agent = "zed-text");
|
|
||||||
self.send_to_model(window, cx);
|
self.send_to_model(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
mod acp_onboarding_modal;
|
|
||||||
mod agent_notification;
|
mod agent_notification;
|
||||||
mod burn_mode_tooltip;
|
mod burn_mode_tooltip;
|
||||||
mod context_pill;
|
mod context_pill;
|
||||||
|
@ -7,7 +6,6 @@ mod onboarding_modal;
|
||||||
pub mod preview;
|
pub mod preview;
|
||||||
mod unavailable_editing_tooltip;
|
mod unavailable_editing_tooltip;
|
||||||
|
|
||||||
pub use acp_onboarding_modal::*;
|
|
||||||
pub use agent_notification::*;
|
pub use agent_notification::*;
|
||||||
pub use burn_mode_tooltip::*;
|
pub use burn_mode_tooltip::*;
|
||||||
pub use context_pill::*;
|
pub use context_pill::*;
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}
|
||||||
|
|
||||||
#[derive(IntoElement, RegisterComponent)]
|
#[derive(IntoElement, RegisterComponent)]
|
||||||
pub struct AiUpsellCard {
|
pub struct AiUpsellCard {
|
||||||
sign_in_status: SignInStatus,
|
pub sign_in_status: SignInStatus,
|
||||||
sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||||
account_too_young: bool,
|
pub account_too_young: bool,
|
||||||
user_plan: Option<Plan>,
|
pub user_plan: Option<Plan>,
|
||||||
tab_index: Option<isize>,
|
pub tab_index: Option<isize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AiUpsellCard {
|
impl AiUpsellCard {
|
||||||
|
@ -43,11 +43,6 @@ impl AiUpsellCard {
|
||||||
tab_index: None,
|
tab_index: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
|
|
||||||
self.tab_index = tab_index;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for AiUpsellCard {
|
impl RenderOnce for AiUpsellCard {
|
||||||
|
|
|
@ -118,7 +118,7 @@ impl Tool for FetchTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
true
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
fn may_perform_edits(&self) -> bool {
|
||||||
|
|
|
@ -435,8 +435,8 @@ mod test {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
matches,
|
matches,
|
||||||
&[
|
&[
|
||||||
PathBuf::from(path!("root/apple/banana/carrot")),
|
PathBuf::from("root/apple/banana/carrot"),
|
||||||
PathBuf::from(path!("root/apple/bandana/carbonara"))
|
PathBuf::from("root/apple/bandana/carbonara")
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -447,8 +447,8 @@ mod test {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
matches,
|
matches,
|
||||||
&[
|
&[
|
||||||
PathBuf::from(path!("root/apple/banana/carrot")),
|
PathBuf::from("root/apple/banana/carrot"),
|
||||||
PathBuf::from(path!("root/apple/bandana/carbonara"))
|
PathBuf::from("root/apple/bandana/carbonara")
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ impl Tool for ReadFileTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon(&self) -> IconName {
|
fn icon(&self) -> IconName {
|
||||||
IconName::ToolSearch
|
IconName::ToolRead
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||||
|
|
|
@ -43,11 +43,3 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
|
||||||
server_url = server_url(cx)
|
server_url = server_url(cx)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the URL to Zed AI's external agents documentation.
|
|
||||||
pub fn external_agents_docs(cx: &App) -> String {
|
|
||||||
format!(
|
|
||||||
"{server_url}/docs/ai/external-agents",
|
|
||||||
server_url = server_url(cx)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use db::{
|
use db::{
|
||||||
query,
|
define_connection, query,
|
||||||
sqlez::{
|
sqlez::{bindable::Column, statement::Statement},
|
||||||
bindable::Column, domain::Domain, statement::Statement,
|
|
||||||
thread_safe_connection::ThreadSafeConnection,
|
|
||||||
},
|
|
||||||
sqlez_macros::sql,
|
sqlez_macros::sql,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -53,11 +50,8 @@ impl Column for SerializedCommandInvocation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CommandPaletteDB(ThreadSafeConnection);
|
define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
|
||||||
|
&[sql!(
|
||||||
impl Domain for CommandPaletteDB {
|
|
||||||
const NAME: &str = stringify!(CommandPaletteDB);
|
|
||||||
const MIGRATIONS: &[&str] = &[sql!(
|
|
||||||
CREATE TABLE IF NOT EXISTS command_invocations(
|
CREATE TABLE IF NOT EXISTS command_invocations(
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
command_name TEXT NOT NULL,
|
command_name TEXT NOT NULL,
|
||||||
|
@ -65,9 +59,7 @@ impl Domain for CommandPaletteDB {
|
||||||
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
|
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)];
|
)];
|
||||||
}
|
);
|
||||||
|
|
||||||
db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
|
|
||||||
|
|
||||||
impl CommandPaletteDB {
|
impl CommandPaletteDB {
|
||||||
pub async fn write_command_invocation(
|
pub async fn write_command_invocation(
|
||||||
|
|
|
@ -110,14 +110,11 @@ pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implements a basic DB wrapper for a given domain
|
/// Implements a basic DB wrapper for a given domain
|
||||||
///
|
|
||||||
/// Arguments:
|
|
||||||
/// - static variable name for connection
|
|
||||||
/// - type of connection wrapper
|
|
||||||
/// - dependencies, whose migrations should be run prior to this domain's migrations
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! static_connection {
|
macro_rules! define_connection {
|
||||||
($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => {
|
(pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => {
|
||||||
|
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||||
|
|
||||||
impl ::std::ops::Deref for $t {
|
impl ::std::ops::Deref for $t {
|
||||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||||
|
|
||||||
|
@ -126,6 +123,16 @@ macro_rules! static_connection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl $crate::sqlez::domain::Domain for $t {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
stringify!($t)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrations() -> &'static [&'static str] {
|
||||||
|
$migrations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl $t {
|
impl $t {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub async fn open_test_db(name: &'static str) -> Self {
|
pub async fn open_test_db(name: &'static str) -> Self {
|
||||||
|
@ -135,8 +142,7 @@ macro_rules! static_connection {
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||||
#[allow(unused_parens)]
|
$t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id))))
|
||||||
$t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id))))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
@ -147,10 +153,46 @@ macro_rules! static_connection {
|
||||||
} else {
|
} else {
|
||||||
$crate::RELEASE_CHANNEL.dev_name()
|
$crate::RELEASE_CHANNEL.dev_name()
|
||||||
};
|
};
|
||||||
#[allow(unused_parens)]
|
$t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope)))
|
||||||
$t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope)))
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => {
|
||||||
|
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||||
|
|
||||||
|
impl ::std::ops::Deref for $t {
|
||||||
|
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $crate::sqlez::domain::Domain for $t {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
stringify!($t)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrations() -> &'static [&'static str] {
|
||||||
|
$migrations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||||
|
$t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id))))
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||||
|
let db_dir = $crate::database_dir();
|
||||||
|
let scope = if false $(|| stringify!($global) == "global")? {
|
||||||
|
"global"
|
||||||
|
} else {
|
||||||
|
$crate::RELEASE_CHANNEL.dev_name()
|
||||||
|
};
|
||||||
|
$t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope)))
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
|
pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
|
||||||
|
@ -177,12 +219,17 @@ mod tests {
|
||||||
enum BadDB {}
|
enum BadDB {}
|
||||||
|
|
||||||
impl Domain for BadDB {
|
impl Domain for BadDB {
|
||||||
const NAME: &str = "db_tests";
|
fn name() -> &'static str {
|
||||||
const MIGRATIONS: &[&str] = &[
|
"db_tests"
|
||||||
sql!(CREATE TABLE test(value);),
|
}
|
||||||
// failure because test already exists
|
|
||||||
sql!(CREATE TABLE test(value);),
|
fn migrations() -> &'static [&'static str] {
|
||||||
];
|
&[
|
||||||
|
sql!(CREATE TABLE test(value);),
|
||||||
|
// failure because test already exists
|
||||||
|
sql!(CREATE TABLE test(value);),
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tempdir = tempfile::Builder::new()
|
let tempdir = tempfile::Builder::new()
|
||||||
|
@ -204,15 +251,25 @@ mod tests {
|
||||||
enum CorruptedDB {}
|
enum CorruptedDB {}
|
||||||
|
|
||||||
impl Domain for CorruptedDB {
|
impl Domain for CorruptedDB {
|
||||||
const NAME: &str = "db_tests";
|
fn name() -> &'static str {
|
||||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
"db_tests"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrations() -> &'static [&'static str] {
|
||||||
|
&[sql!(CREATE TABLE test(value);)]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GoodDB {}
|
enum GoodDB {}
|
||||||
|
|
||||||
impl Domain for GoodDB {
|
impl Domain for GoodDB {
|
||||||
const NAME: &str = "db_tests"; //Notice same name
|
fn name() -> &'static str {
|
||||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)];
|
"db_tests" //Notice same name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrations() -> &'static [&'static str] {
|
||||||
|
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tempdir = tempfile::Builder::new()
|
let tempdir = tempfile::Builder::new()
|
||||||
|
@ -248,16 +305,25 @@ mod tests {
|
||||||
enum CorruptedDB {}
|
enum CorruptedDB {}
|
||||||
|
|
||||||
impl Domain for CorruptedDB {
|
impl Domain for CorruptedDB {
|
||||||
const NAME: &str = "db_tests";
|
fn name() -> &'static str {
|
||||||
|
"db_tests"
|
||||||
|
}
|
||||||
|
|
||||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
fn migrations() -> &'static [&'static str] {
|
||||||
|
&[sql!(CREATE TABLE test(value);)]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GoodDB {}
|
enum GoodDB {}
|
||||||
|
|
||||||
impl Domain for GoodDB {
|
impl Domain for GoodDB {
|
||||||
const NAME: &str = "db_tests"; //Notice same name
|
fn name() -> &'static str {
|
||||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration
|
"db_tests" //Notice same name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrations() -> &'static [&'static str] {
|
||||||
|
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tempdir = tempfile::Builder::new()
|
let tempdir = tempfile::Builder::new()
|
||||||
|
|
|
@ -2,26 +2,16 @@ use gpui::App;
|
||||||
use sqlez_macros::sql;
|
use sqlez_macros::sql;
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
use crate::{
|
use crate::{define_connection, query, write_and_log};
|
||||||
query,
|
|
||||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
|
||||||
write_and_log,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
|
||||||
|
&[sql!(
|
||||||
impl Domain for KeyValueStore {
|
|
||||||
const NAME: &str = stringify!(KeyValueStore);
|
|
||||||
|
|
||||||
const MIGRATIONS: &[&str] = &[sql!(
|
|
||||||
CREATE TABLE IF NOT EXISTS kv_store(
|
CREATE TABLE IF NOT EXISTS kv_store(
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT NOT NULL
|
value TEXT NOT NULL
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)];
|
)];
|
||||||
}
|
);
|
||||||
|
|
||||||
crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
|
|
||||||
|
|
||||||
pub trait Dismissable {
|
pub trait Dismissable {
|
||||||
const KEY: &'static str;
|
const KEY: &'static str;
|
||||||
|
@ -101,19 +91,15 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GlobalKeyValueStore(ThreadSafeConnection);
|
define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> =
|
||||||
|
&[sql!(
|
||||||
impl Domain for GlobalKeyValueStore {
|
|
||||||
const NAME: &str = stringify!(GlobalKeyValueStore);
|
|
||||||
const MIGRATIONS: &[&str] = &[sql!(
|
|
||||||
CREATE TABLE IF NOT EXISTS kv_store(
|
CREATE TABLE IF NOT EXISTS kv_store(
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT NOT NULL
|
value TEXT NOT NULL
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)];
|
)];
|
||||||
}
|
global
|
||||||
|
);
|
||||||
crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global);
|
|
||||||
|
|
||||||
impl GlobalKeyValueStore {
|
impl GlobalKeyValueStore {
|
||||||
query! {
|
query! {
|
||||||
|
|
|
@ -19,10 +19,6 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||||
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
|
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
|
||||||
});
|
});
|
||||||
|
|
||||||
static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
|
||||||
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
|
|
||||||
});
|
|
||||||
|
|
||||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||||
|
|
||||||
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
||||||
|
@ -220,7 +216,6 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
|
||||||
let keymap = match os {
|
let keymap = match os {
|
||||||
"macos" => &KEYMAP_MACOS,
|
"macos" => &KEYMAP_MACOS,
|
||||||
"linux" | "freebsd" => &KEYMAP_LINUX,
|
"linux" | "freebsd" => &KEYMAP_LINUX,
|
||||||
"windows" => &KEYMAP_WINDOWS,
|
|
||||||
_ => unreachable!("Not a valid OS: {}", os),
|
_ => unreachable!("Not a valid OS: {}", os),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2588,7 +2588,7 @@ impl Editor {
|
||||||
|| binding
|
|| binding
|
||||||
.keystrokes()
|
.keystrokes()
|
||||||
.first()
|
.first()
|
||||||
.is_some_and(|keystroke| keystroke.display_modifiers.modified())
|
.is_some_and(|keystroke| keystroke.modifiers.modified())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7686,16 +7686,16 @@ impl Editor {
|
||||||
.keystroke()
|
.keystroke()
|
||||||
{
|
{
|
||||||
modifiers_held = modifiers_held
|
modifiers_held = modifiers_held
|
||||||
|| (&accept_keystroke.display_modifiers == modifiers
|
|| (&accept_keystroke.modifiers == modifiers
|
||||||
&& accept_keystroke.display_modifiers.modified());
|
&& accept_keystroke.modifiers.modified());
|
||||||
};
|
};
|
||||||
if let Some(accept_partial_keystroke) = self
|
if let Some(accept_partial_keystroke) = self
|
||||||
.accept_edit_prediction_keybind(true, window, cx)
|
.accept_edit_prediction_keybind(true, window, cx)
|
||||||
.keystroke()
|
.keystroke()
|
||||||
{
|
{
|
||||||
modifiers_held = modifiers_held
|
modifiers_held = modifiers_held
|
||||||
|| (&accept_partial_keystroke.display_modifiers == modifiers
|
|| (&accept_partial_keystroke.modifiers == modifiers
|
||||||
&& accept_partial_keystroke.display_modifiers.modified());
|
&& accept_partial_keystroke.modifiers.modified());
|
||||||
}
|
}
|
||||||
|
|
||||||
if modifiers_held {
|
if modifiers_held {
|
||||||
|
@ -9044,7 +9044,7 @@ impl Editor {
|
||||||
|
|
||||||
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
|
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
|
||||||
|
|
||||||
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
|
let modifiers_color = if accept_keystroke.modifiers == window.modifiers() {
|
||||||
Color::Accent
|
Color::Accent
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
@ -9056,19 +9056,19 @@ impl Editor {
|
||||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||||
.text_size(TextSize::XSmall.rems(cx))
|
.text_size(TextSize::XSmall.rems(cx))
|
||||||
.child(h_flex().children(ui::render_modifiers(
|
.child(h_flex().children(ui::render_modifiers(
|
||||||
&accept_keystroke.display_modifiers,
|
&accept_keystroke.modifiers,
|
||||||
PlatformStyle::platform(),
|
PlatformStyle::platform(),
|
||||||
Some(modifiers_color),
|
Some(modifiers_color),
|
||||||
Some(IconSize::XSmall.rems().into()),
|
Some(IconSize::XSmall.rems().into()),
|
||||||
true,
|
true,
|
||||||
)))
|
)))
|
||||||
.when(is_platform_style_mac, |parent| {
|
.when(is_platform_style_mac, |parent| {
|
||||||
parent.child(accept_keystroke.display_key.clone())
|
parent.child(accept_keystroke.key.clone())
|
||||||
})
|
})
|
||||||
.when(!is_platform_style_mac, |parent| {
|
.when(!is_platform_style_mac, |parent| {
|
||||||
parent.child(
|
parent.child(
|
||||||
Key::new(
|
Key::new(
|
||||||
util::capitalize(&accept_keystroke.display_key),
|
util::capitalize(&accept_keystroke.key),
|
||||||
Some(Color::Default),
|
Some(Color::Default),
|
||||||
)
|
)
|
||||||
.size(Some(IconSize::XSmall.rems().into())),
|
.size(Some(IconSize::XSmall.rems().into())),
|
||||||
|
@ -9171,7 +9171,7 @@ impl Editor {
|
||||||
max_width: Pixels,
|
max_width: Pixels,
|
||||||
cursor_point: Point,
|
cursor_point: Point,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
accept_keystroke: Option<&gpui::KeybindingKeystroke>,
|
accept_keystroke: Option<&gpui::Keystroke>,
|
||||||
_window: &Window,
|
_window: &Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
|
@ -9249,7 +9249,7 @@ impl Editor {
|
||||||
accept_keystroke.as_ref(),
|
accept_keystroke.as_ref(),
|
||||||
|el, accept_keystroke| {
|
|el, accept_keystroke| {
|
||||||
el.child(h_flex().children(ui::render_modifiers(
|
el.child(h_flex().children(ui::render_modifiers(
|
||||||
&accept_keystroke.display_modifiers,
|
&accept_keystroke.modifiers,
|
||||||
PlatformStyle::platform(),
|
PlatformStyle::platform(),
|
||||||
Some(Color::Default),
|
Some(Color::Default),
|
||||||
Some(IconSize::XSmall.rems().into()),
|
Some(IconSize::XSmall.rems().into()),
|
||||||
|
@ -9319,7 +9319,7 @@ impl Editor {
|
||||||
.child(completion),
|
.child(completion),
|
||||||
)
|
)
|
||||||
.when_some(accept_keystroke, |el, accept_keystroke| {
|
.when_some(accept_keystroke, |el, accept_keystroke| {
|
||||||
if !accept_keystroke.display_modifiers.modified() {
|
if !accept_keystroke.modifiers.modified() {
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9338,7 +9338,7 @@ impl Editor {
|
||||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||||
.when(is_platform_style_mac, |parent| parent.gap_1())
|
.when(is_platform_style_mac, |parent| parent.gap_1())
|
||||||
.child(h_flex().children(ui::render_modifiers(
|
.child(h_flex().children(ui::render_modifiers(
|
||||||
&accept_keystroke.display_modifiers,
|
&accept_keystroke.modifiers,
|
||||||
PlatformStyle::platform(),
|
PlatformStyle::platform(),
|
||||||
Some(if !has_completion {
|
Some(if !has_completion {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -43,10 +43,10 @@ use gpui::{
|
||||||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||||
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
||||||
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
||||||
KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
|
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
|
||||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
|
||||||
ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
|
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||||
Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||||
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
||||||
transparent_black,
|
transparent_black,
|
||||||
};
|
};
|
||||||
|
@ -74,7 +74,7 @@ use std::{
|
||||||
fmt::{self, Write},
|
fmt::{self, Write},
|
||||||
iter, mem,
|
iter, mem,
|
||||||
ops::{Deref, Range},
|
ops::{Deref, Range},
|
||||||
path::{self, Path},
|
path::Path,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
|
@ -90,8 +90,8 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
use util::{RangeExt, ResultExt, debug_panic};
|
use util::{RangeExt, ResultExt, debug_panic};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace,
|
CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item,
|
||||||
item::Item, notifications::NotifyTaskExt,
|
notifications::NotifyTaskExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Determines what kinds of highlights should be applied to a lines background.
|
/// Determines what kinds of highlights should be applied to a lines background.
|
||||||
|
@ -3603,187 +3603,176 @@ impl EditorElement {
|
||||||
let focus_handle = editor.focus_handle(cx);
|
let focus_handle = editor.focus_handle(cx);
|
||||||
let colors = cx.theme().colors();
|
let colors = cx.theme().colors();
|
||||||
|
|
||||||
let header = div()
|
let header =
|
||||||
.p_1()
|
div()
|
||||||
.w_full()
|
.p_1()
|
||||||
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
.w_full()
|
||||||
.child(
|
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
||||||
h_flex()
|
.child(
|
||||||
.size_full()
|
h_flex()
|
||||||
.gap_2()
|
.size_full()
|
||||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
.gap_2()
|
||||||
.pl_0p5()
|
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||||
.pr_5()
|
.pl_0p5()
|
||||||
.rounded_sm()
|
.pr_5()
|
||||||
.when(is_sticky, |el| el.shadow_md())
|
.rounded_sm()
|
||||||
.border_1()
|
.when(is_sticky, |el| el.shadow_md())
|
||||||
.map(|div| {
|
.border_1()
|
||||||
let border_color = if is_selected
|
.map(|div| {
|
||||||
&& is_folded
|
let border_color = if is_selected
|
||||||
&& focus_handle.contains_focused(window, cx)
|
&& is_folded
|
||||||
{
|
&& focus_handle.contains_focused(window, cx)
|
||||||
colors.border_focused
|
{
|
||||||
} else {
|
colors.border_focused
|
||||||
colors.border
|
} else {
|
||||||
};
|
colors.border
|
||||||
div.border_color(border_color)
|
};
|
||||||
})
|
div.border_color(border_color)
|
||||||
.bg(colors.editor_subheader_background)
|
})
|
||||||
.hover(|style| style.bg(colors.element_hover))
|
.bg(colors.editor_subheader_background)
|
||||||
.map(|header| {
|
.hover(|style| style.bg(colors.element_hover))
|
||||||
let editor = self.editor.clone();
|
.map(|header| {
|
||||||
let buffer_id = for_excerpt.buffer_id;
|
let editor = self.editor.clone();
|
||||||
let toggle_chevron_icon =
|
let buffer_id = for_excerpt.buffer_id;
|
||||||
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
let toggle_chevron_icon =
|
||||||
header.child(
|
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
||||||
div()
|
header.child(
|
||||||
.hover(|style| style.bg(colors.element_selected))
|
div()
|
||||||
.rounded_xs()
|
.hover(|style| style.bg(colors.element_selected))
|
||||||
.child(
|
.rounded_xs()
|
||||||
ButtonLike::new("toggle-buffer-fold")
|
.child(
|
||||||
.style(ui::ButtonStyle::Transparent)
|
ButtonLike::new("toggle-buffer-fold")
|
||||||
.height(px(28.).into())
|
.style(ui::ButtonStyle::Transparent)
|
||||||
.width(px(28.))
|
.height(px(28.).into())
|
||||||
.children(toggle_chevron_icon)
|
.width(px(28.))
|
||||||
.tooltip({
|
.children(toggle_chevron_icon)
|
||||||
let focus_handle = focus_handle.clone();
|
.tooltip({
|
||||||
move |window, cx| {
|
let focus_handle = focus_handle.clone();
|
||||||
Tooltip::with_meta_in(
|
move |window, cx| {
|
||||||
"Toggle Excerpt Fold",
|
Tooltip::with_meta_in(
|
||||||
Some(&ToggleFold),
|
"Toggle Excerpt Fold",
|
||||||
"Alt+click to toggle all",
|
Some(&ToggleFold),
|
||||||
&focus_handle,
|
"Alt+click to toggle all",
|
||||||
window,
|
&focus_handle,
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_click(move |event, window, cx| {
|
|
||||||
if event.modifiers().alt {
|
|
||||||
// Alt+click toggles all buffers
|
|
||||||
editor.update(cx, |editor, cx| {
|
|
||||||
editor.toggle_fold_all(
|
|
||||||
&ToggleFoldAll,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
)
|
||||||
});
|
}
|
||||||
} else {
|
})
|
||||||
// Regular click toggles single buffer
|
.on_click(move |event, window, cx| {
|
||||||
if is_folded {
|
if event.modifiers().alt {
|
||||||
|
// Alt+click toggles all buffers
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
editor.unfold_buffer(buffer_id, cx);
|
editor.toggle_fold_all(
|
||||||
|
&ToggleFoldAll,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
editor.update(cx, |editor, cx| {
|
// Regular click toggles single buffer
|
||||||
editor.fold_buffer(buffer_id, cx);
|
if is_folded {
|
||||||
});
|
editor.update(cx, |editor, cx| {
|
||||||
}
|
editor.unfold_buffer(buffer_id, cx);
|
||||||
}
|
});
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.children(
|
|
||||||
editor
|
|
||||||
.addons
|
|
||||||
.values()
|
|
||||||
.filter_map(|addon| {
|
|
||||||
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
|
||||||
})
|
|
||||||
.take(1),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.size(Pixels(12.0))
|
|
||||||
.justify_center()
|
|
||||||
.children(indicator),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.cursor_pointer()
|
|
||||||
.id("path header block")
|
|
||||||
.size_full()
|
|
||||||
.justify_between()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.map(|path_header| {
|
|
||||||
let filename = filename
|
|
||||||
.map(SharedString::from)
|
|
||||||
.unwrap_or_else(|| "untitled".into());
|
|
||||||
|
|
||||||
path_header
|
|
||||||
.when(ItemSettings::get_global(cx).file_icons, |el| {
|
|
||||||
let path = path::Path::new(filename.as_str());
|
|
||||||
let icon = FileIcons::get_icon(path, cx)
|
|
||||||
.unwrap_or_default();
|
|
||||||
let icon =
|
|
||||||
Icon::from_path(icon).color(Color::Muted);
|
|
||||||
el.child(icon)
|
|
||||||
})
|
|
||||||
.child(Label::new(filename).single_line().when_some(
|
|
||||||
file_status,
|
|
||||||
|el, status| {
|
|
||||||
el.color(if status.is_conflicted() {
|
|
||||||
Color::Conflict
|
|
||||||
} else if status.is_modified() {
|
|
||||||
Color::Modified
|
|
||||||
} else if status.is_deleted() {
|
|
||||||
Color::Disabled
|
|
||||||
} else {
|
} else {
|
||||||
Color::Created
|
editor.update(cx, |editor, cx| {
|
||||||
})
|
editor.fold_buffer(buffer_id, cx);
|
||||||
.when(status.is_deleted(), |el| {
|
});
|
||||||
el.strikethrough()
|
}
|
||||||
})
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.children(
|
||||||
|
editor
|
||||||
|
.addons
|
||||||
|
.values()
|
||||||
|
.filter_map(|addon| {
|
||||||
|
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
||||||
|
})
|
||||||
|
.take(1),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.size(Pixels(12.0))
|
||||||
|
.justify_center()
|
||||||
|
.children(indicator),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.cursor_pointer()
|
||||||
|
.id("path header block")
|
||||||
|
.size_full()
|
||||||
|
.justify_between()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Label::new(
|
||||||
|
filename
|
||||||
|
.map(SharedString::from)
|
||||||
|
.unwrap_or_else(|| "untitled".into()),
|
||||||
|
)
|
||||||
|
.single_line()
|
||||||
|
.when_some(file_status, |el, status| {
|
||||||
|
el.color(if status.is_conflicted() {
|
||||||
|
Color::Conflict
|
||||||
|
} else if status.is_modified() {
|
||||||
|
Color::Modified
|
||||||
|
} else if status.is_deleted() {
|
||||||
|
Color::Disabled
|
||||||
|
} else {
|
||||||
|
Color::Created
|
||||||
|
})
|
||||||
|
.when(status.is_deleted(), |el| el.strikethrough())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when_some(parent_path, |then, path| {
|
||||||
|
then.child(div().child(path).text_color(
|
||||||
|
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||||
|
colors.text_disabled
|
||||||
|
} else {
|
||||||
|
colors.text_muted
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})
|
}),
|
||||||
.when_some(parent_path, |then, path| {
|
)
|
||||||
then.child(div().child(path).text_color(
|
.when(
|
||||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||||
colors.text_disabled
|
|el| {
|
||||||
} else {
|
el.child(
|
||||||
colors.text_muted
|
h_flex()
|
||||||
},
|
.id("jump-to-file-button")
|
||||||
))
|
.gap_2p5()
|
||||||
}),
|
.child(Label::new("Jump To File"))
|
||||||
)
|
.children(
|
||||||
.when(
|
KeyBinding::for_action_in(
|
||||||
can_open_excerpts && is_selected && relative_path.is_some(),
|
&OpenExcerpts,
|
||||||
|el| {
|
&focus_handle,
|
||||||
el.child(
|
window,
|
||||||
h_flex()
|
cx,
|
||||||
.id("jump-to-file-button")
|
)
|
||||||
.gap_2p5()
|
.map(|binding| binding.into_any_element()),
|
||||||
.child(Label::new("Jump To File"))
|
),
|
||||||
.children(
|
)
|
||||||
KeyBinding::for_action_in(
|
},
|
||||||
&OpenExcerpts,
|
)
|
||||||
&focus_handle,
|
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||||
window,
|
.on_click(window.listener_for(&self.editor, {
|
||||||
cx,
|
move |editor, e: &ClickEvent, window, cx| {
|
||||||
)
|
editor.open_excerpts_common(
|
||||||
.map(|binding| binding.into_any_element()),
|
Some(jump_data.clone()),
|
||||||
),
|
e.modifiers().secondary(),
|
||||||
)
|
window,
|
||||||
},
|
cx,
|
||||||
)
|
);
|
||||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
}
|
||||||
.on_click(window.listener_for(&self.editor, {
|
})),
|
||||||
move |editor, e: &ClickEvent, window, cx| {
|
),
|
||||||
editor.open_excerpts_common(
|
);
|
||||||
Some(jump_data.clone()),
|
|
||||||
e.modifiers().secondary(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let file = for_excerpt.buffer.file().cloned();
|
let file = for_excerpt.buffer.file().cloned();
|
||||||
let editor = self.editor.clone();
|
let editor = self.editor.clone();
|
||||||
|
@ -7150,7 +7139,7 @@ fn header_jump_data(
|
||||||
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
|
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
|
||||||
|
|
||||||
impl AcceptEditPredictionBinding {
|
impl AcceptEditPredictionBinding {
|
||||||
pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
|
pub fn keystroke(&self) -> Option<&Keystroke> {
|
||||||
if let Some(binding) = self.0.as_ref() {
|
if let Some(binding) = self.0.as_ref() {
|
||||||
match &binding.keystrokes() {
|
match &binding.keystrokes() {
|
||||||
[keystroke, ..] => Some(keystroke),
|
[keystroke, ..] => Some(keystroke),
|
||||||
|
|
|
@ -1404,7 +1404,7 @@ impl ProjectItem for Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn for_broken_project_item(
|
fn for_broken_project_item(
|
||||||
abs_path: &Path,
|
abs_path: PathBuf,
|
||||||
is_local: bool,
|
is_local: bool,
|
||||||
e: &anyhow::Error,
|
e: &anyhow::Error,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use db::{
|
use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
|
||||||
query,
|
use db::sqlez::statement::Statement;
|
||||||
sqlez::{
|
|
||||||
bindable::{Bind, Column, StaticColumnCount},
|
|
||||||
domain::Domain,
|
|
||||||
statement::Statement,
|
|
||||||
},
|
|
||||||
sqlez_macros::sql,
|
|
||||||
};
|
|
||||||
use fs::MTime;
|
use fs::MTime;
|
||||||
use itertools::Itertools as _;
|
use itertools::Itertools as _;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use db::sqlez_macros::sql;
|
||||||
|
use db::{define_connection, query};
|
||||||
|
|
||||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Default)]
|
#[derive(Clone, Debug, PartialEq, Default)]
|
||||||
|
@ -87,11 +83,7 @@ impl Column for SerializedEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
|
define_connection!(
|
||||||
|
|
||||||
impl Domain for EditorDb {
|
|
||||||
const NAME: &str = stringify!(EditorDb);
|
|
||||||
|
|
||||||
// Current schema shape using pseudo-rust syntax:
|
// Current schema shape using pseudo-rust syntax:
|
||||||
// editors(
|
// editors(
|
||||||
// item_id: usize,
|
// item_id: usize,
|
||||||
|
@ -121,8 +113,7 @@ impl Domain for EditorDb {
|
||||||
// start: usize,
|
// start: usize,
|
||||||
// end: usize,
|
// end: usize,
|
||||||
// )
|
// )
|
||||||
|
pub static ref DB: EditorDb<WorkspaceDb> = &[
|
||||||
const MIGRATIONS: &[&str] = &[
|
|
||||||
sql! (
|
sql! (
|
||||||
CREATE TABLE editors(
|
CREATE TABLE editors(
|
||||||
item_id INTEGER NOT NULL,
|
item_id INTEGER NOT NULL,
|
||||||
|
@ -198,9 +189,7 @@ impl Domain for EditorDb {
|
||||||
) STRICT;
|
) STRICT;
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
);
|
||||||
|
|
||||||
db::static_connection!(DB, EditorDb, [WorkspaceDb]);
|
|
||||||
|
|
||||||
// https://www.sqlite.org/limits.html
|
// https://www.sqlite.org/limits.html
|
||||||
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
|
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
|
||||||
|
|
|
@ -98,10 +98,6 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag {
|
||||||
// integration too, and we'd like to turn Gemini/Native on in new builds
|
// integration too, and we'd like to turn Gemini/Native on in new builds
|
||||||
// without enabling Claude Code in old builds.
|
// without enabling Claude Code in old builds.
|
||||||
const NAME: &'static str = "gemini-and-native";
|
const NAME: &'static str = "gemini-and-native";
|
||||||
|
|
||||||
fn enabled_for_all() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ClaudeCodeFeatureFlag;
|
pub struct ClaudeCodeFeatureFlag;
|
||||||
|
@ -205,7 +201,7 @@ impl FeatureFlagAppExt for App {
|
||||||
fn has_flag<T: FeatureFlag>(&self) -> bool {
|
fn has_flag<T: FeatureFlag>(&self) -> bool {
|
||||||
self.try_global::<FeatureFlags>()
|
self.try_global::<FeatureFlags>()
|
||||||
.map(|flags| flags.has_flag::<T>())
|
.map(|flags| flags.has_flag::<T>())
|
||||||
.unwrap_or(T::enabled_for_all())
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_staff(&self) -> bool {
|
fn is_staff(&self) -> bool {
|
||||||
|
|
|
@ -1401,16 +1401,13 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let raw_query = raw_query.trim().to_owned().replace("/", "\\");
|
let raw_query = raw_query.trim().to_owned().replace("/", "\\");
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let raw_query = raw_query.trim();
|
let raw_query = raw_query.trim().to_owned();
|
||||||
|
|
||||||
let raw_query = raw_query.trim_end_matches(':').to_owned();
|
let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query {
|
||||||
let path = path_position.path.to_str();
|
|
||||||
let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':');
|
|
||||||
let file_query_end = if path_trimmed == raw_query {
|
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
// Safe to unwrap as we won't get here when the unwrap in if fails
|
// Safe to unwrap as we won't get here when the unwrap in if fails
|
||||||
Some(path.unwrap().len())
|
Some(path_position.path.to_str().unwrap().len())
|
||||||
};
|
};
|
||||||
|
|
||||||
let query = FileSearchQuery {
|
let query = FileSearchQuery {
|
||||||
|
|
|
@ -218,7 +218,6 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
|
||||||
" ndan ",
|
" ndan ",
|
||||||
" band ",
|
" band ",
|
||||||
"a bandana",
|
"a bandana",
|
||||||
"bandana:",
|
|
||||||
] {
|
] {
|
||||||
picker
|
picker
|
||||||
.update_in(cx, |picker, window, cx| {
|
.update_in(cx, |picker, window, cx| {
|
||||||
|
@ -253,53 +252,6 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_matching_paths_with_colon(cx: &mut TestAppContext) {
|
|
||||||
let app_state = init_test(cx);
|
|
||||||
app_state
|
|
||||||
.fs
|
|
||||||
.as_fake()
|
|
||||||
.insert_tree(
|
|
||||||
path!("/root"),
|
|
||||||
json!({
|
|
||||||
"a": {
|
|
||||||
"foo:bar.rs": "",
|
|
||||||
"foo.rs": "",
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
|
||||||
|
|
||||||
let (picker, _, cx) = build_find_picker(project, cx);
|
|
||||||
|
|
||||||
// 'foo:' matches both files
|
|
||||||
cx.simulate_input("foo:");
|
|
||||||
picker.update(cx, |picker, _| {
|
|
||||||
assert_eq!(picker.delegate.matches.len(), 3);
|
|
||||||
assert_match_at_position(picker, 0, "foo.rs");
|
|
||||||
assert_match_at_position(picker, 1, "foo:bar.rs");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 'foo:b' matches one of the files
|
|
||||||
cx.simulate_input("b");
|
|
||||||
picker.update(cx, |picker, _| {
|
|
||||||
assert_eq!(picker.delegate.matches.len(), 2);
|
|
||||||
assert_match_at_position(picker, 0, "foo:bar.rs");
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.dispatch_action(editor::actions::Backspace);
|
|
||||||
|
|
||||||
// 'foo:1' matches both files, specifying which row to jump to
|
|
||||||
cx.simulate_input("1");
|
|
||||||
picker.update(cx, |picker, _| {
|
|
||||||
assert_eq!(picker.delegate.matches.len(), 3);
|
|
||||||
assert_match_at_position(picker, 0, "foo.rs");
|
|
||||||
assert_match_at_position(picker, 1, "foo:bar.rs");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_unicode_paths(cx: &mut TestAppContext) {
|
async fn test_unicode_paths(cx: &mut TestAppContext) {
|
||||||
let app_state = init_test(cx);
|
let app_state = init_test(cx);
|
||||||
|
|
|
@ -4466,7 +4466,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn Language
|
||||||
is_enabled
|
is_enabled
|
||||||
.then(|| {
|
.then(|| {
|
||||||
let ConfiguredModel { provider, model } =
|
let ConfiguredModel { provider, model } =
|
||||||
LanguageModelRegistry::read_global(cx).commit_message_model()?;
|
LanguageModelRegistry::read_global(cx).commit_message_model(cx)?;
|
||||||
|
|
||||||
provider.is_authenticated(cx).then(|| model)
|
provider.is_authenticated(cx).then(|| model)
|
||||||
})
|
})
|
||||||
|
|
|
@ -37,10 +37,10 @@ use crate::{
|
||||||
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
||||||
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
|
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
|
||||||
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
|
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
||||||
Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
|
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
|
||||||
Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
WindowHandle, WindowId, WindowInvalidator,
|
||||||
colors::{Colors, GlobalColors},
|
colors::{Colors, GlobalColors},
|
||||||
current_platform, hash, init_app_menus,
|
current_platform, hash, init_app_menus,
|
||||||
};
|
};
|
||||||
|
@ -263,7 +263,6 @@ pub struct App {
|
||||||
pub(crate) focus_handles: Arc<FocusMap>,
|
pub(crate) focus_handles: Arc<FocusMap>,
|
||||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||||
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
||||||
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
|
|
||||||
pub(crate) global_action_listeners:
|
pub(crate) global_action_listeners:
|
||||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||||
pending_effects: VecDeque<Effect>,
|
pending_effects: VecDeque<Effect>,
|
||||||
|
@ -313,7 +312,6 @@ impl App {
|
||||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||||
let entities = EntityMap::new();
|
let entities = EntityMap::new();
|
||||||
let keyboard_layout = platform.keyboard_layout();
|
let keyboard_layout = platform.keyboard_layout();
|
||||||
let keyboard_mapper = platform.keyboard_mapper();
|
|
||||||
|
|
||||||
let app = Rc::new_cyclic(|this| AppCell {
|
let app = Rc::new_cyclic(|this| AppCell {
|
||||||
app: RefCell::new(App {
|
app: RefCell::new(App {
|
||||||
|
@ -339,7 +337,6 @@ impl App {
|
||||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||||
keyboard_layout,
|
keyboard_layout,
|
||||||
keyboard_mapper,
|
|
||||||
global_action_listeners: FxHashMap::default(),
|
global_action_listeners: FxHashMap::default(),
|
||||||
pending_effects: VecDeque::new(),
|
pending_effects: VecDeque::new(),
|
||||||
pending_notifications: FxHashSet::default(),
|
pending_notifications: FxHashSet::default(),
|
||||||
|
@ -379,7 +376,6 @@ impl App {
|
||||||
if let Some(app) = app.upgrade() {
|
if let Some(app) = app.upgrade() {
|
||||||
let cx = &mut app.borrow_mut();
|
let cx = &mut app.borrow_mut();
|
||||||
cx.keyboard_layout = cx.platform.keyboard_layout();
|
cx.keyboard_layout = cx.platform.keyboard_layout();
|
||||||
cx.keyboard_mapper = cx.platform.keyboard_mapper();
|
|
||||||
cx.keyboard_layout_observers
|
cx.keyboard_layout_observers
|
||||||
.clone()
|
.clone()
|
||||||
.retain(&(), move |callback| (callback)(cx));
|
.retain(&(), move |callback| (callback)(cx));
|
||||||
|
@ -428,11 +424,6 @@ impl App {
|
||||||
self.keyboard_layout.as_ref()
|
self.keyboard_layout.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current keyboard mapper.
|
|
||||||
pub fn keyboard_mapper(&self) -> &Rc<dyn PlatformKeyboardMapper> {
|
|
||||||
&self.keyboard_mapper
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Invokes a handler when the current keyboard layout changes
|
/// Invokes a handler when the current keyboard layout changes
|
||||||
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
|
|
|
@ -4,7 +4,7 @@ mod context;
|
||||||
pub use binding::*;
|
pub use binding::*;
|
||||||
pub use context::*;
|
pub use context::*;
|
||||||
|
|
||||||
use crate::{Action, AsKeystroke, Keystroke, is_no_action};
|
use crate::{Action, Keystroke, is_no_action};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
|
@ -141,7 +141,7 @@ impl Keymap {
|
||||||
/// only.
|
/// only.
|
||||||
pub fn bindings_for_input(
|
pub fn bindings_for_input(
|
||||||
&self,
|
&self,
|
||||||
input: &[impl AsKeystroke],
|
input: &[Keystroke],
|
||||||
context_stack: &[KeyContext],
|
context_stack: &[KeyContext],
|
||||||
) -> (SmallVec<[KeyBinding; 1]>, bool) {
|
) -> (SmallVec<[KeyBinding; 1]>, bool) {
|
||||||
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
|
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
|
||||||
|
@ -192,6 +192,7 @@ impl Keymap {
|
||||||
|
|
||||||
(bindings, !pending.is_empty())
|
(bindings, !pending.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the given binding is enabled, given a certain key context.
|
/// Check if the given binding is enabled, given a certain key context.
|
||||||
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
||||||
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
||||||
|
@ -638,7 +639,7 @@ mod tests {
|
||||||
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
||||||
let actual = keymap
|
let actual = keymap
|
||||||
.bindings_for_action(action)
|
.bindings_for_action(action)
|
||||||
.map(|binding| binding.keystrokes[0].inner.unparse())
|
.map(|binding| binding.keystrokes[0].unparse())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(actual, expected, "{:?}", action);
|
assert_eq!(actual, expected, "{:?}", action);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{
|
use collections::HashMap;
|
||||||
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
|
|
||||||
KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
|
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
|
||||||
};
|
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
/// A keybinding and its associated metadata, from the keymap.
|
/// A keybinding and its associated metadata, from the keymap.
|
||||||
pub struct KeyBinding {
|
pub struct KeyBinding {
|
||||||
pub(crate) action: Box<dyn Action>,
|
pub(crate) action: Box<dyn Action>,
|
||||||
pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
|
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||||
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||||
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
||||||
/// The json input string used when building the keybinding, if any
|
/// The json input string used when building the keybinding, if any
|
||||||
|
@ -33,15 +32,7 @@ impl KeyBinding {
|
||||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||||
let context_predicate =
|
let context_predicate =
|
||||||
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
|
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
|
||||||
Self::load(
|
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
|
||||||
keystrokes,
|
|
||||||
Box::new(action),
|
|
||||||
context_predicate,
|
|
||||||
false,
|
|
||||||
None,
|
|
||||||
&DummyKeyboardMapper,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a keybinding from the given raw data.
|
/// Load a keybinding from the given raw data.
|
||||||
|
@ -49,22 +40,24 @@ impl KeyBinding {
|
||||||
keystrokes: &str,
|
keystrokes: &str,
|
||||||
action: Box<dyn Action>,
|
action: Box<dyn Action>,
|
||||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||||
use_key_equivalents: bool,
|
key_equivalents: Option<&HashMap<char, char>>,
|
||||||
action_input: Option<SharedString>,
|
action_input: Option<SharedString>,
|
||||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
|
||||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||||
let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
|
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.map(|source| {
|
.map(Keystroke::parse)
|
||||||
let keystroke = Keystroke::parse(source)?;
|
|
||||||
Ok(KeybindingKeystroke::new(
|
|
||||||
keystroke,
|
|
||||||
use_key_equivalents,
|
|
||||||
keyboard_mapper,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect::<std::result::Result<_, _>>()?;
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
|
|
||||||
|
if let Some(equivalents) = key_equivalents {
|
||||||
|
for keystroke in keystrokes.iter_mut() {
|
||||||
|
if keystroke.key.chars().count() == 1
|
||||||
|
&& let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
|
||||||
|
{
|
||||||
|
keystroke.key = key.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
keystrokes,
|
keystrokes,
|
||||||
action,
|
action,
|
||||||
|
@ -86,13 +79,13 @@ impl KeyBinding {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the given keystrokes match this binding.
|
/// Check if the given keystrokes match this binding.
|
||||||
pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option<bool> {
|
pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
|
||||||
if self.keystrokes.len() < typed.len() {
|
if self.keystrokes.len() < typed.len() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
|
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
|
||||||
if !typed.as_keystroke().should_match(target) {
|
if !typed.should_match(target) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,7 +94,7 @@ impl KeyBinding {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the keystrokes associated with this binding
|
/// Get the keystrokes associated with this binding
|
||||||
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
pub fn keystrokes(&self) -> &[Keystroke] {
|
||||||
self.keystrokes.as_slice()
|
self.keystrokes.as_slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -231,6 +231,7 @@ pub(crate) trait Platform: 'static {
|
||||||
|
|
||||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||||
|
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||||
|
|
||||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||||
|
@ -250,6 +251,7 @@ pub(crate) trait Platform: 'static {
|
||||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||||
|
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||||
|
|
||||||
fn compositor_name(&self) -> &'static str {
|
fn compositor_name(&self) -> &'static str {
|
||||||
""
|
""
|
||||||
|
@ -270,10 +272,6 @@ pub(crate) trait Platform: 'static {
|
||||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||||
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
||||||
|
|
||||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
|
||||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
|
|
||||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
use collections::HashMap;
|
|
||||||
|
|
||||||
use crate::{KeybindingKeystroke, Keystroke};
|
|
||||||
|
|
||||||
/// A trait for platform-specific keyboard layouts
|
/// A trait for platform-specific keyboard layouts
|
||||||
pub trait PlatformKeyboardLayout {
|
pub trait PlatformKeyboardLayout {
|
||||||
/// Get the keyboard layout ID, which should be unique to the layout
|
/// Get the keyboard layout ID, which should be unique to the layout
|
||||||
|
@ -9,33 +5,3 @@ pub trait PlatformKeyboardLayout {
|
||||||
/// Get the keyboard layout display name
|
/// Get the keyboard layout display name
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait for platform-specific keyboard mappings
|
|
||||||
pub trait PlatformKeyboardMapper {
|
|
||||||
/// Map a key equivalent to its platform-specific representation
|
|
||||||
fn map_key_equivalent(
|
|
||||||
&self,
|
|
||||||
keystroke: Keystroke,
|
|
||||||
use_key_equivalents: bool,
|
|
||||||
) -> KeybindingKeystroke;
|
|
||||||
/// Get the key equivalents for the current keyboard layout,
|
|
||||||
/// only used on macOS
|
|
||||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A dummy implementation of the platform keyboard mapper
|
|
||||||
pub struct DummyKeyboardMapper;
|
|
||||||
|
|
||||||
impl PlatformKeyboardMapper for DummyKeyboardMapper {
|
|
||||||
fn map_key_equivalent(
|
|
||||||
&self,
|
|
||||||
keystroke: Keystroke,
|
|
||||||
_use_key_equivalents: bool,
|
|
||||||
) -> KeybindingKeystroke {
|
|
||||||
KeybindingKeystroke::from_keystroke(keystroke)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,14 +5,6 @@ use std::{
|
||||||
fmt::{Display, Write},
|
fmt::{Display, Write},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::PlatformKeyboardMapper;
|
|
||||||
|
|
||||||
/// This is a helper trait so that we can simplify the implementation of some functions
|
|
||||||
pub trait AsKeystroke {
|
|
||||||
/// Returns the GPUI representation of the keystroke.
|
|
||||||
fn as_keystroke(&self) -> &Keystroke;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A keystroke and associated metadata generated by the platform
|
/// A keystroke and associated metadata generated by the platform
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||||
pub struct Keystroke {
|
pub struct Keystroke {
|
||||||
|
@ -32,17 +24,6 @@ pub struct Keystroke {
|
||||||
pub key_char: Option<String>,
|
pub key_char: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a keystroke that can be used in keybindings and displayed to the user.
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
||||||
pub struct KeybindingKeystroke {
|
|
||||||
/// The GPUI representation of the keystroke.
|
|
||||||
pub inner: Keystroke,
|
|
||||||
/// The modifiers to display.
|
|
||||||
pub display_modifiers: Modifiers,
|
|
||||||
/// The key to display.
|
|
||||||
pub display_key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
||||||
/// markdown to display it.
|
/// markdown to display it.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -77,7 +58,7 @@ impl Keystroke {
|
||||||
///
|
///
|
||||||
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
||||||
/// both possibilities for self against the target.
|
/// both possibilities for self against the target.
|
||||||
pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
|
pub fn should_match(&self, target: &Keystroke) -> bool {
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
if let Some(key_char) = self
|
if let Some(key_char) = self
|
||||||
.key_char
|
.key_char
|
||||||
|
@ -90,7 +71,7 @@ impl Keystroke {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
|
if &target.key == key_char && target.modifiers == ime_modifiers {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,12 +83,12 @@ impl Keystroke {
|
||||||
.filter(|key_char| key_char != &&self.key)
|
.filter(|key_char| key_char != &&self.key)
|
||||||
{
|
{
|
||||||
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
||||||
if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
|
if &target.key == key_char && target.modifiers == Modifiers::none() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target.inner.modifiers == self.modifiers && target.inner.key == self.key
|
target.modifiers == self.modifiers && target.key == self.key
|
||||||
}
|
}
|
||||||
|
|
||||||
/// key syntax is:
|
/// key syntax is:
|
||||||
|
@ -219,7 +200,31 @@ impl Keystroke {
|
||||||
|
|
||||||
/// Produces a representation of this key that Parse can understand.
|
/// Produces a representation of this key that Parse can understand.
|
||||||
pub fn unparse(&self) -> String {
|
pub fn unparse(&self) -> String {
|
||||||
unparse(&self.modifiers, &self.key)
|
let mut str = String::new();
|
||||||
|
if self.modifiers.function {
|
||||||
|
str.push_str("fn-");
|
||||||
|
}
|
||||||
|
if self.modifiers.control {
|
||||||
|
str.push_str("ctrl-");
|
||||||
|
}
|
||||||
|
if self.modifiers.alt {
|
||||||
|
str.push_str("alt-");
|
||||||
|
}
|
||||||
|
if self.modifiers.platform {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
str.push_str("cmd-");
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
|
str.push_str("super-");
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
str.push_str("win-");
|
||||||
|
}
|
||||||
|
if self.modifiers.shift {
|
||||||
|
str.push_str("shift-");
|
||||||
|
}
|
||||||
|
str.push_str(&self.key);
|
||||||
|
str
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this keystroke left
|
/// Returns true if this keystroke left
|
||||||
|
@ -261,32 +266,6 @@ impl Keystroke {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeybindingKeystroke {
|
|
||||||
/// Create a new keybinding keystroke from the given keystroke
|
|
||||||
pub fn new(
|
|
||||||
inner: Keystroke,
|
|
||||||
use_key_equivalents: bool,
|
|
||||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
|
||||||
) -> Self {
|
|
||||||
keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self {
|
|
||||||
let key = keystroke.key.clone();
|
|
||||||
let modifiers = keystroke.modifiers;
|
|
||||||
KeybindingKeystroke {
|
|
||||||
inner: keystroke,
|
|
||||||
display_modifiers: modifiers,
|
|
||||||
display_key: key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Produces a representation of this key that Parse can understand.
|
|
||||||
pub fn unparse(&self) -> String {
|
|
||||||
unparse(&self.display_modifiers, &self.display_key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_printable_key(key: &str) -> bool {
|
fn is_printable_key(key: &str) -> bool {
|
||||||
!matches!(
|
!matches!(
|
||||||
key,
|
key,
|
||||||
|
@ -343,15 +322,65 @@ fn is_printable_key(key: &str) -> bool {
|
||||||
|
|
||||||
impl std::fmt::Display for Keystroke {
|
impl std::fmt::Display for Keystroke {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
display_modifiers(&self.modifiers, f)?;
|
if self.modifiers.control {
|
||||||
display_key(&self.key, f)
|
#[cfg(target_os = "macos")]
|
||||||
}
|
f.write_char('^')?;
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for KeybindingKeystroke {
|
#[cfg(not(target_os = "macos"))]
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
write!(f, "ctrl-")?;
|
||||||
display_modifiers(&self.display_modifiers, f)?;
|
}
|
||||||
display_key(&self.display_key, f)
|
if self.modifiers.alt {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('⌥')?;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
write!(f, "alt-")?;
|
||||||
|
}
|
||||||
|
if self.modifiers.platform {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('⌘')?;
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
|
f.write_char('❖')?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
f.write_char('⊞')?;
|
||||||
|
}
|
||||||
|
if self.modifiers.shift {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('⇧')?;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
write!(f, "shift-")?;
|
||||||
|
}
|
||||||
|
let key = match self.key.as_str() {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"backspace" => '⌫',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"up" => '↑',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"down" => '↓',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"left" => '←',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"right" => '→',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"tab" => '⇥',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"escape" => '⎋',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"shift" => '⇧',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"control" => '⌃',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"alt" => '⌥',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"platform" => '⌘',
|
||||||
|
|
||||||
|
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||||
|
key => return f.write_str(key),
|
||||||
|
};
|
||||||
|
f.write_char(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -571,110 +600,3 @@ pub struct Capslock {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub on: bool,
|
pub on: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsKeystroke for Keystroke {
|
|
||||||
fn as_keystroke(&self) -> &Keystroke {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsKeystroke for KeybindingKeystroke {
|
|
||||||
fn as_keystroke(&self) -> &Keystroke {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
if modifiers.control {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
f.write_char('^')?;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
write!(f, "ctrl-")?;
|
|
||||||
}
|
|
||||||
if modifiers.alt {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
f.write_char('⌥')?;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
write!(f, "alt-")?;
|
|
||||||
}
|
|
||||||
if modifiers.platform {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
f.write_char('⌘')?;
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
||||||
f.write_char('❖')?;
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
f.write_char('⊞')?;
|
|
||||||
}
|
|
||||||
if modifiers.shift {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
f.write_char('⇧')?;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
write!(f, "shift-")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let key = match key {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"backspace" => '⌫',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"up" => '↑',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"down" => '↓',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"left" => '←',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"right" => '→',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"tab" => '⇥',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"escape" => '⎋',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"shift" => '⇧',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"control" => '⌃',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"alt" => '⌥',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"platform" => '⌘',
|
|
||||||
|
|
||||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
|
||||||
key => return f.write_str(key),
|
|
||||||
};
|
|
||||||
f.write_char(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn unparse(modifiers: &Modifiers, key: &str) -> String {
|
|
||||||
let mut result = String::new();
|
|
||||||
if modifiers.function {
|
|
||||||
result.push_str("fn-");
|
|
||||||
}
|
|
||||||
if modifiers.control {
|
|
||||||
result.push_str("ctrl-");
|
|
||||||
}
|
|
||||||
if modifiers.alt {
|
|
||||||
result.push_str("alt-");
|
|
||||||
}
|
|
||||||
if modifiers.platform {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
result.push_str("cmd-");
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
||||||
result.push_str("super-");
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
result.push_str("win-");
|
|
||||||
}
|
|
||||||
if modifiers.shift {
|
|
||||||
result.push_str("shift-");
|
|
||||||
}
|
|
||||||
result.push_str(&key);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
|
@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||||
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
||||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
|
||||||
PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
|
Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||||
|
@ -144,10 +144,6 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||||
self.keyboard_layout()
|
self.keyboard_layout()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
|
||||||
Rc::new(crate::DummyKeyboardMapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||||
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
|
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
use super::{
|
use super::{
|
||||||
BoolExt, MacKeyboardLayout, MacKeyboardMapper,
|
BoolExt, MacKeyboardLayout,
|
||||||
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
||||||
events::key_to_native,
|
events::key_to_native,
|
||||||
renderer,
|
renderer,
|
||||||
|
@ -8,9 +8,8 @@ use crate::{
|
||||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||||
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
||||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
|
||||||
PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams,
|
SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
|
||||||
hash,
|
|
||||||
};
|
};
|
||||||
use anyhow::{Context as _, anyhow};
|
use anyhow::{Context as _, anyhow};
|
||||||
use block::ConcreteBlock;
|
use block::ConcreteBlock;
|
||||||
|
@ -172,7 +171,6 @@ pub(crate) struct MacPlatformState {
|
||||||
finish_launching: Option<Box<dyn FnOnce()>>,
|
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||||
dock_menu: Option<id>,
|
dock_menu: Option<id>,
|
||||||
menus: Option<Vec<OwnedMenu>>,
|
menus: Option<Vec<OwnedMenu>>,
|
||||||
keyboard_mapper: Rc<MacKeyboardMapper>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MacPlatform {
|
impl Default for MacPlatform {
|
||||||
|
@ -191,9 +189,6 @@ impl MacPlatform {
|
||||||
#[cfg(not(feature = "font-kit"))]
|
#[cfg(not(feature = "font-kit"))]
|
||||||
let text_system = Arc::new(crate::NoopTextSystem::new());
|
let text_system = Arc::new(crate::NoopTextSystem::new());
|
||||||
|
|
||||||
let keyboard_layout = MacKeyboardLayout::new();
|
|
||||||
let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
|
||||||
|
|
||||||
Self(Mutex::new(MacPlatformState {
|
Self(Mutex::new(MacPlatformState {
|
||||||
headless,
|
headless,
|
||||||
text_system,
|
text_system,
|
||||||
|
@ -214,7 +209,6 @@ impl MacPlatform {
|
||||||
dock_menu: None,
|
dock_menu: None,
|
||||||
on_keyboard_layout_change: None,
|
on_keyboard_layout_change: None,
|
||||||
menus: None,
|
menus: None,
|
||||||
keyboard_mapper,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,19 +348,19 @@ impl MacPlatform {
|
||||||
let mut mask = NSEventModifierFlags::empty();
|
let mut mask = NSEventModifierFlags::empty();
|
||||||
for (modifier, flag) in &[
|
for (modifier, flag) in &[
|
||||||
(
|
(
|
||||||
keystroke.display_modifiers.platform,
|
keystroke.modifiers.platform,
|
||||||
NSEventModifierFlags::NSCommandKeyMask,
|
NSEventModifierFlags::NSCommandKeyMask,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
keystroke.display_modifiers.control,
|
keystroke.modifiers.control,
|
||||||
NSEventModifierFlags::NSControlKeyMask,
|
NSEventModifierFlags::NSControlKeyMask,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
keystroke.display_modifiers.alt,
|
keystroke.modifiers.alt,
|
||||||
NSEventModifierFlags::NSAlternateKeyMask,
|
NSEventModifierFlags::NSAlternateKeyMask,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
keystroke.display_modifiers.shift,
|
keystroke.modifiers.shift,
|
||||||
NSEventModifierFlags::NSShiftKeyMask,
|
NSEventModifierFlags::NSShiftKeyMask,
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
|
@ -379,7 +373,7 @@ impl MacPlatform {
|
||||||
.initWithTitle_action_keyEquivalent_(
|
.initWithTitle_action_keyEquivalent_(
|
||||||
ns_string(name),
|
ns_string(name),
|
||||||
selector,
|
selector,
|
||||||
ns_string(key_to_native(&keystroke.display_key).as_ref()),
|
ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||||
)
|
)
|
||||||
.autorelease();
|
.autorelease();
|
||||||
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
|
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
|
||||||
|
@ -888,10 +882,6 @@ impl Platform for MacPlatform {
|
||||||
Box::new(MacKeyboardLayout::new())
|
Box::new(MacKeyboardLayout::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
|
||||||
self.0.lock().keyboard_mapper.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app_path(&self) -> Result<PathBuf> {
|
fn app_path(&self) -> Result<PathBuf> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let bundle: id = NSBundle::mainBundle();
|
let bundle: id = NSBundle::mainBundle();
|
||||||
|
@ -1403,8 +1393,6 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
|
||||||
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
|
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
|
||||||
let platform = unsafe { get_mac_platform(this) };
|
let platform = unsafe { get_mac_platform(this) };
|
||||||
let mut lock = platform.0.lock();
|
let mut lock = platform.0.lock();
|
||||||
let keyboard_layout = MacKeyboardLayout::new();
|
|
||||||
lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
|
||||||
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
|
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
|
||||||
drop(lock);
|
drop(lock);
|
||||||
callback();
|
callback();
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||||
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||||
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||||
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
|
@ -238,10 +237,6 @@ impl Platform for TestPlatform {
|
||||||
Box::new(TestKeyboardLayout)
|
Box::new(TestKeyboardLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
|
||||||
Rc::new(DummyKeyboardMapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
||||||
|
|
||||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||||
|
|
|
@ -9,8 +9,10 @@ use parking::Parker;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use windows::{
|
use windows::{
|
||||||
|
Foundation::TimeSpan,
|
||||||
System::Threading::{
|
System::Threading::{
|
||||||
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority,
|
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions,
|
||||||
|
WorkItemPriority,
|
||||||
},
|
},
|
||||||
Win32::{
|
Win32::{
|
||||||
Foundation::{LPARAM, WPARAM},
|
Foundation::{LPARAM, WPARAM},
|
||||||
|
@ -54,7 +56,12 @@ impl WindowsDispatcher {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err();
|
ThreadPool::RunWithPriorityAndOptionsAsync(
|
||||||
|
&handler,
|
||||||
|
WorkItemPriority::High,
|
||||||
|
WorkItemOptions::TimeSliced,
|
||||||
|
)
|
||||||
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
|
fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
|
||||||
|
@ -65,7 +72,12 @@ impl WindowsDispatcher {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err();
|
let delay = TimeSpan {
|
||||||
|
// A time period expressed in 100-nanosecond units.
|
||||||
|
// 10,000,000 ticks per second
|
||||||
|
Duration: (duration.as_nanos() / 100) as i64,
|
||||||
|
};
|
||||||
|
ThreadPoolTimer::CreateTimer(&handler, delay).log_err();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,31 +1,22 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::HashMap;
|
|
||||||
use windows::Win32::UI::{
|
use windows::Win32::UI::{
|
||||||
Input::KeyboardAndMouse::{
|
Input::KeyboardAndMouse::{
|
||||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
|
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
|
||||||
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
|
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
|
||||||
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
|
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
|
||||||
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||||
},
|
},
|
||||||
WindowsAndMessaging::KL_NAMELENGTH,
|
WindowsAndMessaging::KL_NAMELENGTH,
|
||||||
};
|
};
|
||||||
use windows_core::HSTRING;
|
use windows_core::HSTRING;
|
||||||
|
|
||||||
use crate::{
|
use crate::{Modifiers, PlatformKeyboardLayout};
|
||||||
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) struct WindowsKeyboardLayout {
|
pub(crate) struct WindowsKeyboardLayout {
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct WindowsKeyboardMapper {
|
|
||||||
key_to_vkey: HashMap<String, (u16, bool)>,
|
|
||||||
vkey_to_key: HashMap<u16, String>,
|
|
||||||
vkey_to_shifted: HashMap<u16, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
||||||
fn id(&self) -> &str {
|
fn id(&self) -> &str {
|
||||||
&self.id
|
&self.id
|
||||||
|
@ -36,65 +27,6 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
|
|
||||||
fn map_key_equivalent(
|
|
||||||
&self,
|
|
||||||
mut keystroke: Keystroke,
|
|
||||||
use_key_equivalents: bool,
|
|
||||||
) -> KeybindingKeystroke {
|
|
||||||
let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
|
|
||||||
else {
|
|
||||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
|
||||||
};
|
|
||||||
if shifted_key && keystroke.modifiers.shift {
|
|
||||||
log::warn!(
|
|
||||||
"Keystroke '{}' has both shift and a shifted key, this is likely a bug",
|
|
||||||
keystroke.key
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let shift = shifted_key || keystroke.modifiers.shift;
|
|
||||||
keystroke.modifiers.shift = false;
|
|
||||||
|
|
||||||
let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
|
|
||||||
log::error!(
|
|
||||||
"Failed to map key equivalent '{:?}' to a valid key",
|
|
||||||
keystroke
|
|
||||||
);
|
|
||||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
|
||||||
};
|
|
||||||
|
|
||||||
keystroke.key = if shift {
|
|
||||||
let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
|
|
||||||
log::error!(
|
|
||||||
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
|
|
||||||
keystroke,
|
|
||||||
vkey
|
|
||||||
);
|
|
||||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
|
||||||
};
|
|
||||||
shifted_key
|
|
||||||
} else {
|
|
||||||
key.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let modifiers = Modifiers {
|
|
||||||
shift,
|
|
||||||
..keystroke.modifiers
|
|
||||||
};
|
|
||||||
|
|
||||||
KeybindingKeystroke {
|
|
||||||
inner: keystroke,
|
|
||||||
display_modifiers: modifiers,
|
|
||||||
display_key: key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowsKeyboardLayout {
|
impl WindowsKeyboardLayout {
|
||||||
pub(crate) fn new() -> Result<Self> {
|
pub(crate) fn new() -> Result<Self> {
|
||||||
let mut buffer = [0u16; KL_NAMELENGTH as usize];
|
let mut buffer = [0u16; KL_NAMELENGTH as usize];
|
||||||
|
@ -116,41 +48,6 @@ impl WindowsKeyboardLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowsKeyboardMapper {
|
|
||||||
pub(crate) fn new() -> Self {
|
|
||||||
let mut key_to_vkey = HashMap::default();
|
|
||||||
let mut vkey_to_key = HashMap::default();
|
|
||||||
let mut vkey_to_shifted = HashMap::default();
|
|
||||||
for vkey in CANDIDATE_VKEYS {
|
|
||||||
if let Some(key) = get_key_from_vkey(*vkey) {
|
|
||||||
key_to_vkey.insert(key.clone(), (vkey.0, false));
|
|
||||||
vkey_to_key.insert(vkey.0, key);
|
|
||||||
}
|
|
||||||
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
|
|
||||||
if scan_code == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
|
|
||||||
key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
|
|
||||||
vkey_to_shifted.insert(vkey.0, shifted_key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self {
|
|
||||||
key_to_vkey,
|
|
||||||
vkey_to_key,
|
|
||||||
vkey_to_shifted,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
|
|
||||||
if use_key_equivalents {
|
|
||||||
get_vkey_from_key_with_us_layout(key)
|
|
||||||
} else {
|
|
||||||
self.key_to_vkey.get(key).cloned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_keystroke_key(
|
pub(crate) fn get_keystroke_key(
|
||||||
vkey: VIRTUAL_KEY,
|
vkey: VIRTUAL_KEY,
|
||||||
scan_code: u32,
|
scan_code: u32,
|
||||||
|
@ -243,134 +140,3 @@ pub(crate) fn generate_key_char(
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
|
|
||||||
match key {
|
|
||||||
// ` => VK_OEM_3
|
|
||||||
"`" => Some((VK_OEM_3.0, false)),
|
|
||||||
"~" => Some((VK_OEM_3.0, true)),
|
|
||||||
"1" => Some((VK_1.0, false)),
|
|
||||||
"!" => Some((VK_1.0, true)),
|
|
||||||
"2" => Some((VK_2.0, false)),
|
|
||||||
"@" => Some((VK_2.0, true)),
|
|
||||||
"3" => Some((VK_3.0, false)),
|
|
||||||
"#" => Some((VK_3.0, true)),
|
|
||||||
"4" => Some((VK_4.0, false)),
|
|
||||||
"$" => Some((VK_4.0, true)),
|
|
||||||
"5" => Some((VK_5.0, false)),
|
|
||||||
"%" => Some((VK_5.0, true)),
|
|
||||||
"6" => Some((VK_6.0, false)),
|
|
||||||
"^" => Some((VK_6.0, true)),
|
|
||||||
"7" => Some((VK_7.0, false)),
|
|
||||||
"&" => Some((VK_7.0, true)),
|
|
||||||
"8" => Some((VK_8.0, false)),
|
|
||||||
"*" => Some((VK_8.0, true)),
|
|
||||||
"9" => Some((VK_9.0, false)),
|
|
||||||
"(" => Some((VK_9.0, true)),
|
|
||||||
"0" => Some((VK_0.0, false)),
|
|
||||||
")" => Some((VK_0.0, true)),
|
|
||||||
"-" => Some((VK_OEM_MINUS.0, false)),
|
|
||||||
"_" => Some((VK_OEM_MINUS.0, true)),
|
|
||||||
"=" => Some((VK_OEM_PLUS.0, false)),
|
|
||||||
"+" => Some((VK_OEM_PLUS.0, true)),
|
|
||||||
"[" => Some((VK_OEM_4.0, false)),
|
|
||||||
"{" => Some((VK_OEM_4.0, true)),
|
|
||||||
"]" => Some((VK_OEM_6.0, false)),
|
|
||||||
"}" => Some((VK_OEM_6.0, true)),
|
|
||||||
"\\" => Some((VK_OEM_5.0, false)),
|
|
||||||
"|" => Some((VK_OEM_5.0, true)),
|
|
||||||
";" => Some((VK_OEM_1.0, false)),
|
|
||||||
":" => Some((VK_OEM_1.0, true)),
|
|
||||||
"'" => Some((VK_OEM_7.0, false)),
|
|
||||||
"\"" => Some((VK_OEM_7.0, true)),
|
|
||||||
"," => Some((VK_OEM_COMMA.0, false)),
|
|
||||||
"<" => Some((VK_OEM_COMMA.0, true)),
|
|
||||||
"." => Some((VK_OEM_PERIOD.0, false)),
|
|
||||||
">" => Some((VK_OEM_PERIOD.0, true)),
|
|
||||||
"/" => Some((VK_OEM_2.0, false)),
|
|
||||||
"?" => Some((VK_OEM_2.0, true)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
|
|
||||||
VK_OEM_3,
|
|
||||||
VK_OEM_MINUS,
|
|
||||||
VK_OEM_PLUS,
|
|
||||||
VK_OEM_4,
|
|
||||||
VK_OEM_5,
|
|
||||||
VK_OEM_6,
|
|
||||||
VK_OEM_1,
|
|
||||||
VK_OEM_7,
|
|
||||||
VK_OEM_COMMA,
|
|
||||||
VK_OEM_PERIOD,
|
|
||||||
VK_OEM_2,
|
|
||||||
VK_OEM_102,
|
|
||||||
VK_OEM_8,
|
|
||||||
VK_ABNT_C1,
|
|
||||||
VK_0,
|
|
||||||
VK_1,
|
|
||||||
VK_2,
|
|
||||||
VK_3,
|
|
||||||
VK_4,
|
|
||||||
VK_5,
|
|
||||||
VK_6,
|
|
||||||
VK_7,
|
|
||||||
VK_8,
|
|
||||||
VK_9,
|
|
||||||
];
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_keyboard_mapper() {
|
|
||||||
let mapper = WindowsKeyboardMapper::new();
|
|
||||||
|
|
||||||
// Normal case
|
|
||||||
let keystroke = Keystroke {
|
|
||||||
modifiers: Modifiers::control(),
|
|
||||||
key: "a".to_string(),
|
|
||||||
key_char: None,
|
|
||||||
};
|
|
||||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
|
||||||
assert_eq!(mapped.inner, keystroke);
|
|
||||||
assert_eq!(mapped.display_key, "a");
|
|
||||||
assert_eq!(mapped.display_modifiers, Modifiers::control());
|
|
||||||
|
|
||||||
// Shifted case, ctrl-$
|
|
||||||
let keystroke = Keystroke {
|
|
||||||
modifiers: Modifiers::control(),
|
|
||||||
key: "$".to_string(),
|
|
||||||
key_char: None,
|
|
||||||
};
|
|
||||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
|
||||||
assert_eq!(mapped.inner, keystroke);
|
|
||||||
assert_eq!(mapped.display_key, "4");
|
|
||||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
|
||||||
|
|
||||||
// Shifted case, but shift is true
|
|
||||||
let keystroke = Keystroke {
|
|
||||||
modifiers: Modifiers::control_shift(),
|
|
||||||
key: "$".to_string(),
|
|
||||||
key_char: None,
|
|
||||||
};
|
|
||||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
|
||||||
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
|
||||||
assert_eq!(mapped.display_key, "4");
|
|
||||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
|
||||||
|
|
||||||
// Windows style
|
|
||||||
let keystroke = Keystroke {
|
|
||||||
modifiers: Modifiers::control_shift(),
|
|
||||||
key: "4".to_string(),
|
|
||||||
key_char: None,
|
|
||||||
};
|
|
||||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
|
||||||
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
|
||||||
assert_eq!(mapped.inner.key, "$");
|
|
||||||
assert_eq!(mapped.display_key, "4");
|
|
||||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -351,10 +351,6 @@ impl Platform for WindowsPlatform {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
|
||||||
Rc::new(WindowsKeyboardMapper::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||||
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
|
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,7 +215,6 @@ pub enum IconName {
|
||||||
Tab,
|
Tab,
|
||||||
Terminal,
|
Terminal,
|
||||||
TerminalAlt,
|
TerminalAlt,
|
||||||
TerminalGhost,
|
|
||||||
TextSnippet,
|
TextSnippet,
|
||||||
TextThread,
|
TextThread,
|
||||||
Thread,
|
Thread,
|
||||||
|
|
|
@ -401,19 +401,12 @@ pub fn init(cx: &mut App) {
|
||||||
mod persistence {
|
mod persistence {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use db::{
|
use db::{define_connection, query, sqlez_macros::sql};
|
||||||
query,
|
|
||||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
|
||||||
sqlez_macros::sql,
|
|
||||||
};
|
|
||||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||||
|
|
||||||
pub struct ImageViewerDb(ThreadSafeConnection);
|
define_connection! {
|
||||||
|
pub static ref IMAGE_VIEWER: ImageViewerDb<WorkspaceDb> =
|
||||||
impl Domain for ImageViewerDb {
|
&[sql!(
|
||||||
const NAME: &str = stringify!(ImageViewerDb);
|
|
||||||
|
|
||||||
const MIGRATIONS: &[&str] = &[sql!(
|
|
||||||
CREATE TABLE image_viewers (
|
CREATE TABLE image_viewers (
|
||||||
workspace_id INTEGER,
|
workspace_id INTEGER,
|
||||||
item_id INTEGER UNIQUE,
|
item_id INTEGER UNIQUE,
|
||||||
|
@ -424,11 +417,9 @@ mod persistence {
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)];
|
)];
|
||||||
}
|
}
|
||||||
|
|
||||||
db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]);
|
|
||||||
|
|
||||||
impl ImageViewerDb {
|
impl ImageViewerDb {
|
||||||
query! {
|
query! {
|
||||||
pub async fn save_image_path(
|
pub async fn save_image_path(
|
||||||
|
|
|
@ -24,7 +24,6 @@ serde_json_lenient.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
util_macros.workspace = true
|
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
|
|
|
@ -25,7 +25,7 @@ use util::split_str_with_ranges;
|
||||||
|
|
||||||
/// Path used for unsaved buffer that contains style json. To support the json language server, this
|
/// Path used for unsaved buffer that contains style json. To support the json language server, this
|
||||||
/// matches the name used in the generated schemas.
|
/// matches the name used in the generated schemas.
|
||||||
const ZED_INSPECTOR_STYLE_JSON: &str = util_macros::path!("/zed-inspector-style.json");
|
const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json";
|
||||||
|
|
||||||
pub(crate) struct DivInspector {
|
pub(crate) struct DivInspector {
|
||||||
state: State,
|
state: State,
|
||||||
|
|
|
@ -1569,21 +1569,11 @@ impl Buffer {
|
||||||
self.send_operation(op, true, cx);
|
self.send_operation(op, true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn buffer_diagnostics(
|
pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> {
|
||||||
&self,
|
let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else {
|
||||||
for_server: Option<LanguageServerId>,
|
return None;
|
||||||
) -> Vec<&DiagnosticEntry<Anchor>> {
|
};
|
||||||
match for_server {
|
Some(&self.diagnostics[idx].1)
|
||||||
Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) {
|
|
||||||
Ok(idx) => self.diagnostics[idx].1.iter().collect(),
|
|
||||||
Err(_) => Vec::new(),
|
|
||||||
},
|
|
||||||
None => self
|
|
||||||
.diagnostics
|
|
||||||
.iter()
|
|
||||||
.flat_map(|(_, diagnostic_set)| diagnostic_set.iter())
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_autoindent(&mut self, cx: &mut Context<Self>) {
|
fn request_autoindent(&mut self, cx: &mut Context<Self>) {
|
||||||
|
|
|
@ -4,16 +4,12 @@ use crate::{
|
||||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||||
LanguageModelRequest, LanguageModelToolChoice,
|
LanguageModelRequest, LanguageModelToolChoice,
|
||||||
};
|
};
|
||||||
use anyhow::anyhow;
|
|
||||||
use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
|
use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
|
||||||
use gpui::{AnyView, App, AsyncApp, Entity, Task, Window};
|
use gpui::{AnyView, App, AsyncApp, Entity, Task, Window};
|
||||||
use http_client::Result;
|
use http_client::Result;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use std::sync::{
|
use std::sync::Arc;
|
||||||
Arc,
|
|
||||||
atomic::{AtomicBool, Ordering::SeqCst},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FakeLanguageModelProvider {
|
pub struct FakeLanguageModelProvider {
|
||||||
|
@ -110,7 +106,6 @@ pub struct FakeLanguageModel {
|
||||||
>,
|
>,
|
||||||
)>,
|
)>,
|
||||||
>,
|
>,
|
||||||
forbid_requests: AtomicBool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FakeLanguageModel {
|
impl Default for FakeLanguageModel {
|
||||||
|
@ -119,20 +114,11 @@ impl Default for FakeLanguageModel {
|
||||||
provider_id: LanguageModelProviderId::from("fake".to_string()),
|
provider_id: LanguageModelProviderId::from("fake".to_string()),
|
||||||
provider_name: LanguageModelProviderName::from("Fake".to_string()),
|
provider_name: LanguageModelProviderName::from("Fake".to_string()),
|
||||||
current_completion_txs: Mutex::new(Vec::new()),
|
current_completion_txs: Mutex::new(Vec::new()),
|
||||||
forbid_requests: AtomicBool::new(false),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FakeLanguageModel {
|
impl FakeLanguageModel {
|
||||||
pub fn allow_requests(&self) {
|
|
||||||
self.forbid_requests.store(false, SeqCst);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn forbid_requests(&self) {
|
|
||||||
self.forbid_requests.store(true, SeqCst);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
|
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
|
||||||
self.current_completion_txs
|
self.current_completion_txs
|
||||||
.lock()
|
.lock()
|
||||||
|
@ -265,18 +251,9 @@ impl LanguageModel for FakeLanguageModel {
|
||||||
LanguageModelCompletionError,
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
if self.forbid_requests.load(SeqCst) {
|
let (tx, rx) = mpsc::unbounded();
|
||||||
async move {
|
self.current_completion_txs.lock().push((request, tx));
|
||||||
Err(LanguageModelCompletionError::Other(anyhow!(
|
async move { Ok(rx.boxed()) }.boxed()
|
||||||
"requests are forbidden"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
.boxed()
|
|
||||||
} else {
|
|
||||||
let (tx, rx) = mpsc::unbounded();
|
|
||||||
self.current_completion_txs.lock().push((request, tx));
|
|
||||||
async move { Ok(rx.boxed()) }.boxed()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_fake(&self) -> &Self {
|
fn as_fake(&self) -> &Self {
|
||||||
|
|
|
@ -6,7 +6,6 @@ use collections::BTreeMap;
|
||||||
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
|
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
|
||||||
use std::{str::FromStr, sync::Arc};
|
use std::{str::FromStr, sync::Arc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use util::maybe;
|
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
let registry = cx.new(|_cx| LanguageModelRegistry::default());
|
let registry = cx.new(|_cx| LanguageModelRegistry::default());
|
||||||
|
@ -42,7 +41,9 @@ impl std::fmt::Debug for ConfigurationError {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct LanguageModelRegistry {
|
pub struct LanguageModelRegistry {
|
||||||
default_model: Option<ConfiguredModel>,
|
default_model: Option<ConfiguredModel>,
|
||||||
default_fast_model: Option<ConfiguredModel>,
|
/// This model is automatically configured by a user's environment after
|
||||||
|
/// authenticating all providers. It's only used when default_model is not available.
|
||||||
|
environment_fallback_model: Option<ConfiguredModel>,
|
||||||
inline_assistant_model: Option<ConfiguredModel>,
|
inline_assistant_model: Option<ConfiguredModel>,
|
||||||
commit_message_model: Option<ConfiguredModel>,
|
commit_message_model: Option<ConfiguredModel>,
|
||||||
thread_summary_model: Option<ConfiguredModel>,
|
thread_summary_model: Option<ConfiguredModel>,
|
||||||
|
@ -98,9 +99,6 @@ impl ConfiguredModel {
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
DefaultModelChanged,
|
DefaultModelChanged,
|
||||||
InlineAssistantModelChanged,
|
|
||||||
CommitMessageModelChanged,
|
|
||||||
ThreadSummaryModelChanged,
|
|
||||||
ProviderStateChanged(LanguageModelProviderId),
|
ProviderStateChanged(LanguageModelProviderId),
|
||||||
AddedProvider(LanguageModelProviderId),
|
AddedProvider(LanguageModelProviderId),
|
||||||
RemovedProvider(LanguageModelProviderId),
|
RemovedProvider(LanguageModelProviderId),
|
||||||
|
@ -226,7 +224,7 @@ impl LanguageModelRegistry {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||||
self.set_inline_assistant_model(configured_model, cx);
|
self.set_inline_assistant_model(configured_model);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_commit_message_model(
|
pub fn select_commit_message_model(
|
||||||
|
@ -235,7 +233,7 @@ impl LanguageModelRegistry {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||||
self.set_commit_message_model(configured_model, cx);
|
self.set_commit_message_model(configured_model);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_thread_summary_model(
|
pub fn select_thread_summary_model(
|
||||||
|
@ -244,7 +242,7 @@ impl LanguageModelRegistry {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||||
self.set_thread_summary_model(configured_model, cx);
|
self.set_thread_summary_model(configured_model);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selects and sets the inline alternatives for language models based on
|
/// Selects and sets the inline alternatives for language models based on
|
||||||
|
@ -278,68 +276,60 @@ impl LanguageModelRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_default_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
|
pub fn set_default_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
|
||||||
match (self.default_model.as_ref(), model.as_ref()) {
|
match (self.default_model(), model.as_ref()) {
|
||||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||||
(None, None) => {}
|
(None, None) => {}
|
||||||
_ => cx.emit(Event::DefaultModelChanged),
|
_ => cx.emit(Event::DefaultModelChanged),
|
||||||
}
|
}
|
||||||
self.default_fast_model = maybe!({
|
|
||||||
let provider = &model.as_ref()?.provider;
|
|
||||||
let fast_model = provider.default_fast_model(cx)?;
|
|
||||||
Some(ConfiguredModel {
|
|
||||||
provider: provider.clone(),
|
|
||||||
model: fast_model,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
self.default_model = model;
|
self.default_model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_inline_assistant_model(
|
pub fn set_environment_fallback_model(
|
||||||
&mut self,
|
&mut self,
|
||||||
model: Option<ConfiguredModel>,
|
model: Option<ConfiguredModel>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
match (self.inline_assistant_model.as_ref(), model.as_ref()) {
|
if self.default_model.is_none() {
|
||||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
match (self.environment_fallback_model.as_ref(), model.as_ref()) {
|
||||||
(None, None) => {}
|
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||||
_ => cx.emit(Event::InlineAssistantModelChanged),
|
(None, None) => {}
|
||||||
|
_ => cx.emit(Event::DefaultModelChanged),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
self.environment_fallback_model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_inline_assistant_model(&mut self, model: Option<ConfiguredModel>) {
|
||||||
self.inline_assistant_model = model;
|
self.inline_assistant_model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_commit_message_model(
|
pub fn set_commit_message_model(&mut self, model: Option<ConfiguredModel>) {
|
||||||
&mut self,
|
|
||||||
model: Option<ConfiguredModel>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
match (self.commit_message_model.as_ref(), model.as_ref()) {
|
|
||||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
|
||||||
(None, None) => {}
|
|
||||||
_ => cx.emit(Event::CommitMessageModelChanged),
|
|
||||||
}
|
|
||||||
self.commit_message_model = model;
|
self.commit_message_model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_thread_summary_model(
|
pub fn set_thread_summary_model(&mut self, model: Option<ConfiguredModel>) {
|
||||||
&mut self,
|
|
||||||
model: Option<ConfiguredModel>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
match (self.thread_summary_model.as_ref(), model.as_ref()) {
|
|
||||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
|
||||||
(None, None) => {}
|
|
||||||
_ => cx.emit(Event::ThreadSummaryModelChanged),
|
|
||||||
}
|
|
||||||
self.thread_summary_model = model;
|
self.thread_summary_model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn default_model(&self) -> Option<ConfiguredModel> {
|
pub fn default_model(&self) -> Option<ConfiguredModel> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.default_model.clone()
|
self.default_model
|
||||||
|
.clone()
|
||||||
|
.or_else(|| self.environment_fallback_model.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_fast_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||||
|
let provider = self.default_model()?.provider;
|
||||||
|
let fast_model = provider.default_fast_model(cx)?;
|
||||||
|
Some(ConfiguredModel {
|
||||||
|
provider,
|
||||||
|
model: fast_model,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inline_assistant_model(&self) -> Option<ConfiguredModel> {
|
pub fn inline_assistant_model(&self) -> Option<ConfiguredModel> {
|
||||||
|
@ -353,7 +343,7 @@ impl LanguageModelRegistry {
|
||||||
.or_else(|| self.default_model.clone())
|
.or_else(|| self.default_model.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit_message_model(&self) -> Option<ConfiguredModel> {
|
pub fn commit_message_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||||
return None;
|
return None;
|
||||||
|
@ -361,11 +351,11 @@ impl LanguageModelRegistry {
|
||||||
|
|
||||||
self.commit_message_model
|
self.commit_message_model
|
||||||
.clone()
|
.clone()
|
||||||
.or_else(|| self.default_fast_model.clone())
|
.or_else(|| self.default_fast_model(cx))
|
||||||
.or_else(|| self.default_model.clone())
|
.or_else(|| self.default_model.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn thread_summary_model(&self) -> Option<ConfiguredModel> {
|
pub fn thread_summary_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||||
return None;
|
return None;
|
||||||
|
@ -373,7 +363,7 @@ impl LanguageModelRegistry {
|
||||||
|
|
||||||
self.thread_summary_model
|
self.thread_summary_model
|
||||||
.clone()
|
.clone()
|
||||||
.or_else(|| self.default_fast_model.clone())
|
.or_else(|| self.default_fast_model(cx))
|
||||||
.or_else(|| self.default_model.clone())
|
.or_else(|| self.default_model.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,4 +400,34 @@ mod tests {
|
||||||
let providers = registry.read(cx).providers();
|
let providers = registry.read(cx).providers();
|
||||||
assert!(providers.is_empty());
|
assert!(providers.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) {
|
||||||
|
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||||
|
|
||||||
|
let provider = FakeLanguageModelProvider::default();
|
||||||
|
registry.update(cx, |registry, cx| {
|
||||||
|
registry.register_provider(provider.clone(), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|cx| provider.authenticate(cx)).await.unwrap();
|
||||||
|
|
||||||
|
registry.update(cx, |registry, cx| {
|
||||||
|
let provider = registry.provider(&provider.id()).unwrap();
|
||||||
|
|
||||||
|
registry.set_environment_fallback_model(
|
||||||
|
Some(ConfiguredModel {
|
||||||
|
provider: provider.clone(),
|
||||||
|
model: provider.default_model(cx).unwrap(),
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let default_model = registry.default_model().unwrap();
|
||||||
|
let fallback_model = registry.environment_fallback_model.clone().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(default_model.model.id(), fallback_model.model.id());
|
||||||
|
assert_eq!(default_model.provider.id(), fallback_model.provider.id());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ ollama = { workspace = true, features = ["schemars"] }
|
||||||
open_ai = { workspace = true, features = ["schemars"] }
|
open_ai = { workspace = true, features = ["schemars"] }
|
||||||
open_router = { workspace = true, features = ["schemars"] }
|
open_router = { workspace = true, features = ["schemars"] }
|
||||||
partial-json-fixer.workspace = true
|
partial-json-fixer.workspace = true
|
||||||
|
project.workspace = true
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
@ -3,8 +3,12 @@ use std::sync::Arc;
|
||||||
use ::settings::{Settings, SettingsStore};
|
use ::settings::{Settings, SettingsStore};
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use gpui::{App, Context, Entity};
|
use futures::future;
|
||||||
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
|
use gpui::{App, AppContext as _, Context, Entity};
|
||||||
|
use language_model::{
|
||||||
|
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||||
|
};
|
||||||
|
use project::DisableAiSettings;
|
||||||
use provider::deepseek::DeepSeekLanguageModelProvider;
|
use provider::deepseek::DeepSeekLanguageModelProvider;
|
||||||
|
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
|
@ -13,7 +17,7 @@ pub mod ui;
|
||||||
|
|
||||||
use crate::provider::anthropic::AnthropicLanguageModelProvider;
|
use crate::provider::anthropic::AnthropicLanguageModelProvider;
|
||||||
use crate::provider::bedrock::BedrockLanguageModelProvider;
|
use crate::provider::bedrock::BedrockLanguageModelProvider;
|
||||||
use crate::provider::cloud::CloudLanguageModelProvider;
|
use crate::provider::cloud::{self, CloudLanguageModelProvider};
|
||||||
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
|
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
|
||||||
use crate::provider::google::GoogleLanguageModelProvider;
|
use crate::provider::google::GoogleLanguageModelProvider;
|
||||||
use crate::provider::lmstudio::LmStudioLanguageModelProvider;
|
use crate::provider::lmstudio::LmStudioLanguageModelProvider;
|
||||||
|
@ -48,6 +52,13 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut already_authenticated = false;
|
||||||
|
if !DisableAiSettings::get_global(cx).disable_ai {
|
||||||
|
authenticate_all_providers(registry.clone(), cx);
|
||||||
|
already_authenticated = true;
|
||||||
|
}
|
||||||
|
|
||||||
cx.observe_global::<SettingsStore>(move |cx| {
|
cx.observe_global::<SettingsStore>(move |cx| {
|
||||||
let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx)
|
let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx)
|
||||||
.openai_compatible
|
.openai_compatible
|
||||||
|
@ -65,6 +76,12 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
openai_compatible_providers = openai_compatible_providers_new;
|
openai_compatible_providers = openai_compatible_providers_new;
|
||||||
|
already_authenticated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated {
|
||||||
|
authenticate_all_providers(registry.clone(), cx);
|
||||||
|
already_authenticated = true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -151,3 +168,83 @@ fn register_language_model_providers(
|
||||||
registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx);
|
registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx);
|
||||||
registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
|
registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Authenticates all providers in the [`LanguageModelRegistry`].
|
||||||
|
///
|
||||||
|
/// We do this so that we can populate the language selector with all of the
|
||||||
|
/// models from the configured providers.
|
||||||
|
///
|
||||||
|
/// This function won't do anything if AI is disabled.
|
||||||
|
fn authenticate_all_providers(registry: Entity<LanguageModelRegistry>, cx: &mut App) {
|
||||||
|
let providers_to_authenticate = registry
|
||||||
|
.read(cx)
|
||||||
|
.providers()
|
||||||
|
.iter()
|
||||||
|
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut tasks = Vec::with_capacity(providers_to_authenticate.len());
|
||||||
|
|
||||||
|
for (provider_id, provider_name, authenticate_task) in providers_to_authenticate {
|
||||||
|
tasks.push(cx.background_spawn(async move {
|
||||||
|
if let Err(err) = authenticate_task.await {
|
||||||
|
if matches!(err, AuthenticateError::CredentialsNotFound) {
|
||||||
|
// Since we're authenticating these providers in the
|
||||||
|
// background for the purposes of populating the
|
||||||
|
// language selector, we don't care about providers
|
||||||
|
// where the credentials are not found.
|
||||||
|
} else {
|
||||||
|
// Some providers have noisy failure states that we
|
||||||
|
// don't want to spam the logs with every time the
|
||||||
|
// language model selector is initialized.
|
||||||
|
//
|
||||||
|
// Ideally these should have more clear failure modes
|
||||||
|
// that we know are safe to ignore here, like what we do
|
||||||
|
// with `CredentialsNotFound` above.
|
||||||
|
match provider_id.0.as_ref() {
|
||||||
|
"lmstudio" | "ollama" => {
|
||||||
|
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
||||||
|
//
|
||||||
|
// These fail noisily, so we don't log them.
|
||||||
|
}
|
||||||
|
"copilot_chat" => {
|
||||||
|
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to authenticate provider: {}: {err}",
|
||||||
|
provider_name.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let all_authenticated_future = future::join_all(tasks);
|
||||||
|
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
all_authenticated_future.await;
|
||||||
|
|
||||||
|
registry
|
||||||
|
.update(cx, |registry, cx| {
|
||||||
|
let cloud_provider = registry.provider(&cloud::PROVIDER_ID);
|
||||||
|
let fallback_model = cloud_provider
|
||||||
|
.iter()
|
||||||
|
.chain(registry.providers().iter())
|
||||||
|
.find(|provider| provider.is_authenticated(cx))
|
||||||
|
.and_then(|provider| {
|
||||||
|
Some(ConfiguredModel {
|
||||||
|
provider: provider.clone(),
|
||||||
|
model: provider
|
||||||
|
.default_model(cx)
|
||||||
|
.or_else(|| provider.recommended_models(cx).first().cloned())?,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
registry.set_environment_fallback_model(fallback_model, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
|
@ -44,8 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i
|
||||||
use crate::provider::google::{GoogleEventMapper, into_google};
|
use crate::provider::google::{GoogleEventMapper, into_google};
|
||||||
use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
|
use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
|
||||||
|
|
||||||
const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
|
pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
|
||||||
const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
|
pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, PartialEq)]
|
#[derive(Default, Clone, Debug, PartialEq)]
|
||||||
pub struct ZedDotDevSettings {
|
pub struct ZedDotDevSettings {
|
||||||
|
@ -146,7 +146,7 @@ impl State {
|
||||||
default_fast_model: None,
|
default_fast_model: None,
|
||||||
recommended_models: Vec::new(),
|
recommended_models: Vec::new(),
|
||||||
_fetch_models_task: cx.spawn(async move |this, cx| {
|
_fetch_models_task: cx.spawn(async move |this, cx| {
|
||||||
maybe!(async move {
|
maybe!(async {
|
||||||
let (client, llm_api_token) = this
|
let (client, llm_api_token) = this
|
||||||
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;
|
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use settings::get_key_equivalents;
|
||||||
use ui::{Button, ButtonStyle};
|
use ui::{Button, ButtonStyle};
|
||||||
use ui::{
|
use ui::{
|
||||||
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
|
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
|
||||||
|
@ -168,8 +169,7 @@ impl Item for KeyContextView {
|
||||||
impl Render for KeyContextView {
|
impl Render for KeyContextView {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
let key_equivalents = get_key_equivalents(cx.keyboard_layout().id());
|
||||||
let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("key-context-view")
|
.id("key-context-view")
|
||||||
.overflow_scroll()
|
.overflow_scroll()
|
||||||
|
|
|
@ -1743,5 +1743,6 @@ pub enum Event {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<Event> for LogStore {}
|
impl EventEmitter<Event> for LogStore {}
|
||||||
|
impl EventEmitter<Event> for LspLogView {}
|
||||||
impl EventEmitter<EditorEvent> for LspLogView {}
|
impl EventEmitter<EditorEvent> for LspLogView {}
|
||||||
impl EventEmitter<SearchEvent> for LspLogView {}
|
impl EventEmitter<SearchEvent> for LspLogView {}
|
||||||
|
|
|
@ -231,7 +231,6 @@
|
||||||
"implements"
|
"implements"
|
||||||
"interface"
|
"interface"
|
||||||
"keyof"
|
"keyof"
|
||||||
"module"
|
|
||||||
"namespace"
|
"namespace"
|
||||||
"private"
|
"private"
|
||||||
"protected"
|
"protected"
|
||||||
|
@ -251,4 +250,4 @@
|
||||||
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
|
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
|
||||||
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
|
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
|
||||||
(jsx_attribute "=" @punctuation.delimiter.jsx)
|
(jsx_attribute "=" @punctuation.delimiter.jsx)
|
||||||
(jsx_text) @text.jsx
|
(jsx_text) @text.jsx
|
|
@ -11,21 +11,6 @@
|
||||||
(#set! injection.language "css"))
|
(#set! injection.language "css"))
|
||||||
)
|
)
|
||||||
|
|
||||||
(call_expression
|
|
||||||
function: (member_expression
|
|
||||||
object: (identifier) @_obj (#eq? @_obj "styled")
|
|
||||||
property: (property_identifier))
|
|
||||||
arguments: (template_string (string_fragment) @injection.content
|
|
||||||
(#set! injection.language "css"))
|
|
||||||
)
|
|
||||||
|
|
||||||
(call_expression
|
|
||||||
function: (call_expression
|
|
||||||
function: (identifier) @_name (#eq? @_name "styled"))
|
|
||||||
arguments: (template_string (string_fragment) @injection.content
|
|
||||||
(#set! injection.language "css"))
|
|
||||||
)
|
|
||||||
|
|
||||||
(call_expression
|
(call_expression
|
||||||
function: (identifier) @_name (#eq? @_name "html")
|
function: (identifier) @_name (#eq? @_name "html")
|
||||||
arguments: (template_string) @injection.content
|
arguments: (template_string) @injection.content
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
(self) @variable.special
|
(self) @variable.special
|
||||||
(field_identifier) @property
|
(field_identifier) @property
|
||||||
|
|
||||||
|
(shorthand_field_initializer
|
||||||
|
(identifier) @property)
|
||||||
|
|
||||||
(trait_item name: (type_identifier) @type.interface)
|
(trait_item name: (type_identifier) @type.interface)
|
||||||
(impl_item trait: (type_identifier) @type.interface)
|
(impl_item trait: (type_identifier) @type.interface)
|
||||||
(abstract_type trait: (type_identifier) @type.interface)
|
(abstract_type trait: (type_identifier) @type.interface)
|
||||||
|
@ -38,11 +41,20 @@
|
||||||
(identifier) @function.special
|
(identifier) @function.special
|
||||||
(scoped_identifier
|
(scoped_identifier
|
||||||
name: (identifier) @function.special)
|
name: (identifier) @function.special)
|
||||||
])
|
]
|
||||||
|
"!" @function.special)
|
||||||
|
|
||||||
(macro_definition
|
(macro_definition
|
||||||
name: (identifier) @function.special.definition)
|
name: (identifier) @function.special.definition)
|
||||||
|
|
||||||
|
(mod_item
|
||||||
|
name: (identifier) @module)
|
||||||
|
|
||||||
|
(visibility_modifier [
|
||||||
|
(crate) @keyword
|
||||||
|
(super) @keyword
|
||||||
|
])
|
||||||
|
|
||||||
; Identifier conventions
|
; Identifier conventions
|
||||||
|
|
||||||
; Assume uppercase names are types/enum-constructors
|
; Assume uppercase names are types/enum-constructors
|
||||||
|
@ -115,9 +127,7 @@
|
||||||
"where"
|
"where"
|
||||||
"while"
|
"while"
|
||||||
"yield"
|
"yield"
|
||||||
(crate)
|
|
||||||
(mutable_specifier)
|
(mutable_specifier)
|
||||||
(super)
|
|
||||||
] @keyword
|
] @keyword
|
||||||
|
|
||||||
[
|
[
|
||||||
|
@ -189,6 +199,7 @@
|
||||||
operator: "/" @operator
|
operator: "/" @operator
|
||||||
|
|
||||||
(lifetime) @lifetime
|
(lifetime) @lifetime
|
||||||
|
(lifetime (identifier) @lifetime)
|
||||||
|
|
||||||
(parameter (identifier) @variable.parameter)
|
(parameter (identifier) @variable.parameter)
|
||||||
|
|
||||||
|
|
|
@ -237,7 +237,6 @@
|
||||||
"implements"
|
"implements"
|
||||||
"interface"
|
"interface"
|
||||||
"keyof"
|
"keyof"
|
||||||
"module"
|
|
||||||
"namespace"
|
"namespace"
|
||||||
"private"
|
"private"
|
||||||
"protected"
|
"protected"
|
||||||
|
@ -257,4 +256,4 @@
|
||||||
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
|
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
|
||||||
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
|
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
|
||||||
(jsx_attribute "=" @punctuation.delimiter.jsx)
|
(jsx_attribute "=" @punctuation.delimiter.jsx)
|
||||||
(jsx_text) @text.jsx
|
(jsx_text) @text.jsx
|
|
@ -11,21 +11,6 @@
|
||||||
(#set! injection.language "css"))
|
(#set! injection.language "css"))
|
||||||
)
|
)
|
||||||
|
|
||||||
(call_expression
|
|
||||||
function: (member_expression
|
|
||||||
object: (identifier) @_obj (#eq? @_obj "styled")
|
|
||||||
property: (property_identifier))
|
|
||||||
arguments: (template_string (string_fragment) @injection.content
|
|
||||||
(#set! injection.language "css"))
|
|
||||||
)
|
|
||||||
|
|
||||||
(call_expression
|
|
||||||
function: (call_expression
|
|
||||||
function: (identifier) @_name (#eq? @_name "styled"))
|
|
||||||
arguments: (template_string (string_fragment) @injection.content
|
|
||||||
(#set! injection.language "css"))
|
|
||||||
)
|
|
||||||
|
|
||||||
(call_expression
|
(call_expression
|
||||||
function: (identifier) @_name (#eq? @_name "html")
|
function: (identifier) @_name (#eq? @_name "html")
|
||||||
arguments: (template_string (string_fragment) @injection.content
|
arguments: (template_string (string_fragment) @injection.content
|
||||||
|
|
|
@ -248,7 +248,6 @@
|
||||||
"is"
|
"is"
|
||||||
"keyof"
|
"keyof"
|
||||||
"let"
|
"let"
|
||||||
"module"
|
|
||||||
"namespace"
|
"namespace"
|
||||||
"new"
|
"new"
|
||||||
"of"
|
"of"
|
||||||
|
@ -273,4 +272,4 @@
|
||||||
"while"
|
"while"
|
||||||
"with"
|
"with"
|
||||||
"yield"
|
"yield"
|
||||||
] @keyword
|
] @keyword
|
|
@ -15,21 +15,6 @@
|
||||||
(#set! injection.language "css"))
|
(#set! injection.language "css"))
|
||||||
)
|
)
|
||||||
|
|
||||||
(call_expression
|
|
||||||
function: (member_expression
|
|
||||||
object: (identifier) @_obj (#eq? @_obj "styled")
|
|
||||||
property: (property_identifier))
|
|
||||||
arguments: (template_string (string_fragment) @injection.content
|
|
||||||
(#set! injection.language "css"))
|
|
||||||
)
|
|
||||||
|
|
||||||
(call_expression
|
|
||||||
function: (call_expression
|
|
||||||
function: (identifier) @_name (#eq? @_name "styled"))
|
|
||||||
arguments: (template_string (string_fragment) @injection.content
|
|
||||||
(#set! injection.language "css"))
|
|
||||||
)
|
|
||||||
|
|
||||||
(call_expression
|
(call_expression
|
||||||
function: (identifier) @_name (#eq? @_name "html")
|
function: (identifier) @_name (#eq? @_name "html")
|
||||||
arguments: (template_string) @injection.content
|
arguments: (template_string) @injection.content
|
||||||
|
|
98
crates/lsp/src/capabilities.rs
Normal file
98
crates/lsp/src/capabilities.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use lsp_types::{
|
||||||
|
ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
|
||||||
|
TextDocumentSyncSaveOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::DynamicCapabilities;
|
||||||
|
|
||||||
|
pub mod cap {
|
||||||
|
pub struct DidChangeTextDocument;
|
||||||
|
pub struct DidSaveTextDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait EffectiveCapability {
|
||||||
|
type Value;
|
||||||
|
fn compute(static_caps: &ServerCapabilities, dynamic_caps: &DynamicCapabilities)
|
||||||
|
-> Self::Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EffectiveCapability for cap::DidChangeTextDocument {
|
||||||
|
type Value = Option<TextDocumentSyncKind>;
|
||||||
|
|
||||||
|
fn compute(
|
||||||
|
static_caps: &ServerCapabilities,
|
||||||
|
dynamic_caps: &DynamicCapabilities,
|
||||||
|
) -> Self::Value {
|
||||||
|
dynamic_caps
|
||||||
|
.text_document_sync_did_change
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id_to_sync_kind_map| {
|
||||||
|
if id_to_sync_kind_map.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut has_incremental = false;
|
||||||
|
for data in id_to_sync_kind_map.values() {
|
||||||
|
let sync_kind = match data.sync_kind {
|
||||||
|
0 => Some(TextDocumentSyncKind::NONE),
|
||||||
|
1 => Some(TextDocumentSyncKind::FULL),
|
||||||
|
2 => Some(TextDocumentSyncKind::INCREMENTAL),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if sync_kind == Some(TextDocumentSyncKind::FULL) {
|
||||||
|
return Some(TextDocumentSyncKind::FULL);
|
||||||
|
}
|
||||||
|
if sync_kind == Some(TextDocumentSyncKind::INCREMENTAL) {
|
||||||
|
has_incremental = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has_incremental {
|
||||||
|
Some(TextDocumentSyncKind::INCREMENTAL)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
static_caps
|
||||||
|
.text_document_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|sync| match sync {
|
||||||
|
TextDocumentSyncCapability::Kind(kind) => Some(*kind),
|
||||||
|
TextDocumentSyncCapability::Options(opts) => opts.change,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EffectiveCapability for cap::DidSaveTextDocument {
|
||||||
|
type Value = Option<bool>;
|
||||||
|
|
||||||
|
fn compute(
|
||||||
|
static_caps: &ServerCapabilities,
|
||||||
|
dynamic_caps: &DynamicCapabilities,
|
||||||
|
) -> Self::Value {
|
||||||
|
dynamic_caps
|
||||||
|
.text_document_sync_did_save
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id_to_save_options_map| {
|
||||||
|
if id_to_save_options_map.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
id_to_save_options_map
|
||||||
|
.values()
|
||||||
|
.any(|data| data.include_text.unwrap_or(false)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_else(|| match static_caps.text_document_sync.as_ref()? {
|
||||||
|
TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? {
|
||||||
|
TextDocumentSyncSaveOptions::Supported(true) => Some(false),
|
||||||
|
TextDocumentSyncSaveOptions::Supported(false) => None,
|
||||||
|
TextDocumentSyncSaveOptions::SaveOptions(save_opts) => {
|
||||||
|
Some(save_opts.include_text.unwrap_or(false))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TextDocumentSyncCapability::Kind(_) => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
|
mod capabilities;
|
||||||
mod input_handler;
|
mod input_handler;
|
||||||
|
|
||||||
|
pub use capabilities::{EffectiveCapability, cap};
|
||||||
|
|
||||||
pub use lsp_types::request::*;
|
pub use lsp_types::request::*;
|
||||||
pub use lsp_types::*;
|
pub use lsp_types::*;
|
||||||
|
|
||||||
|
@ -86,7 +89,8 @@ pub struct LanguageServer {
|
||||||
name: LanguageServerName,
|
name: LanguageServerName,
|
||||||
process_name: Arc<str>,
|
process_name: Arc<str>,
|
||||||
binary: LanguageServerBinary,
|
binary: LanguageServerBinary,
|
||||||
capabilities: RwLock<ServerCapabilities>,
|
static_capabilities: RwLock<ServerCapabilities>,
|
||||||
|
dynamic_capabilities: RwLock<DynamicCapabilities>,
|
||||||
/// Configuration sent to the server, stored for display in the language server logs
|
/// Configuration sent to the server, stored for display in the language server logs
|
||||||
/// buffer. This is represented as the message sent to the LSP in order to avoid cloning it (can
|
/// buffer. This is represented as the message sent to the LSP in order to avoid cloning it (can
|
||||||
/// be large in cases like sending schemas to the json server).
|
/// be large in cases like sending schemas to the json server).
|
||||||
|
@ -301,6 +305,13 @@ pub struct AdapterServerCapabilities {
|
||||||
pub code_action_kinds: Option<Vec<CodeActionKind>>,
|
pub code_action_kinds: Option<Vec<CodeActionKind>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct DynamicCapabilities {
|
||||||
|
pub text_document_sync_did_change:
|
||||||
|
Option<HashMap<String, TextDocumentChangeRegistrationOptions>>,
|
||||||
|
pub text_document_sync_did_save: Option<HashMap<String, TextDocumentSaveRegistrationOptions>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl LanguageServer {
|
impl LanguageServer {
|
||||||
/// Starts a language server process.
|
/// Starts a language server process.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
@ -484,7 +495,8 @@ impl LanguageServer {
|
||||||
.map(|name| Arc::from(name.to_string_lossy()))
|
.map(|name| Arc::from(name.to_string_lossy()))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
binary,
|
binary,
|
||||||
capabilities: Default::default(),
|
static_capabilities: Default::default(),
|
||||||
|
dynamic_capabilities: Default::default(),
|
||||||
configuration,
|
configuration,
|
||||||
code_action_kinds,
|
code_action_kinds,
|
||||||
next_id: Default::default(),
|
next_id: Default::default(),
|
||||||
|
@ -898,7 +910,7 @@ impl LanguageServer {
|
||||||
if let Some(info) = response.server_info {
|
if let Some(info) = response.server_info {
|
||||||
self.process_name = info.name.into();
|
self.process_name = info.name.into();
|
||||||
}
|
}
|
||||||
self.capabilities = RwLock::new(response.capabilities);
|
self.static_capabilities = RwLock::new(response.capabilities);
|
||||||
self.configuration = configuration;
|
self.configuration = configuration;
|
||||||
|
|
||||||
self.notify::<notification::Initialized>(&InitializedParams {})?;
|
self.notify::<notification::Initialized>(&InitializedParams {})?;
|
||||||
|
@ -1130,7 +1142,18 @@ impl LanguageServer {
|
||||||
|
|
||||||
/// Get the reported capabilities of the running language server.
|
/// Get the reported capabilities of the running language server.
|
||||||
pub fn capabilities(&self) -> ServerCapabilities {
|
pub fn capabilities(&self) -> ServerCapabilities {
|
||||||
self.capabilities.read().clone()
|
self.static_capabilities.read().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_dynamic_capabilities(&self, update: impl FnOnce(&mut DynamicCapabilities)) {
|
||||||
|
update(self.dynamic_capabilities.write().deref_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get effective capabilities by combining static and dynamic capabilities.
|
||||||
|
pub fn effective_capability<Cap: EffectiveCapability>(&self) -> Cap::Value {
|
||||||
|
let static_capabilities = self.capabilities();
|
||||||
|
let dynamic_capabilities = self.dynamic_capabilities.read().clone();
|
||||||
|
Cap::compute(&static_capabilities, &dynamic_capabilities)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the reported capabilities of the running language server and
|
/// Get the reported capabilities of the running language server and
|
||||||
|
@ -1143,7 +1166,7 @@ impl LanguageServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_capabilities(&self, update: impl FnOnce(&mut ServerCapabilities)) {
|
pub fn update_capabilities(&self, update: impl FnOnce(&mut ServerCapabilities)) {
|
||||||
update(self.capabilities.write().deref_mut());
|
update(self.static_capabilities.write().deref_mut());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn configuration(&self) -> &Value {
|
pub fn configuration(&self) -> &Value {
|
||||||
|
|
|
@ -1085,10 +1085,10 @@ impl Element for MarkdownElement {
|
||||||
);
|
);
|
||||||
el.child(
|
el.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_4()
|
.w_5()
|
||||||
.absolute()
|
.absolute()
|
||||||
.top_1p5()
|
.top_1()
|
||||||
.right_1p5()
|
.right_1()
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.child(codeblock),
|
.child(codeblock),
|
||||||
)
|
)
|
||||||
|
@ -1115,12 +1115,11 @@ impl Element for MarkdownElement {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
el.child(
|
el.child(
|
||||||
h_flex()
|
div()
|
||||||
.w_4()
|
|
||||||
.absolute()
|
.absolute()
|
||||||
.top_0()
|
.top_0()
|
||||||
.right_0()
|
.right_0()
|
||||||
.justify_end()
|
.w_5()
|
||||||
.visible_on_hover("code_block")
|
.visible_on_hover("code_block")
|
||||||
.child(codeblock),
|
.child(codeblock),
|
||||||
)
|
)
|
||||||
|
@ -1323,7 +1322,7 @@ fn render_copy_code_block_button(
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.style(ButtonStyle::Filled)
|
.style(ButtonStyle::Filled)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.shape(ui::IconButtonShape::Square)
|
||||||
.tooltip(Tooltip::text("Copy"))
|
.tooltip(Tooltip::text("Copy Code"))
|
||||||
.on_click({
|
.on_click({
|
||||||
let markdown = markdown;
|
let markdown = markdown;
|
||||||
move |_event, _window, cx| {
|
move |_event, _window, cx| {
|
||||||
|
|
|
@ -835,7 +835,7 @@ impl MultiBuffer {
|
||||||
this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns);
|
this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns);
|
||||||
drop(snapshot);
|
drop(snapshot);
|
||||||
|
|
||||||
let mut buffer_ids = Vec::with_capacity(buffer_edits.len());
|
let mut buffer_ids = Vec::new();
|
||||||
for (buffer_id, mut edits) in buffer_edits {
|
for (buffer_id, mut edits) in buffer_edits {
|
||||||
buffer_ids.push(buffer_id);
|
buffer_ids.push(buffer_id);
|
||||||
edits.sort_by_key(|edit| edit.range.start);
|
edits.sort_by_key(|edit| edit.range.start);
|
||||||
|
|
|
@ -283,13 +283,17 @@ pub(crate) fn render_ai_setup_page(
|
||||||
v_flex()
|
v_flex()
|
||||||
.mt_2()
|
.mt_2()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.child(
|
.child({
|
||||||
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx)
|
let mut ai_upsell_card =
|
||||||
.tab_index(Some({
|
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx);
|
||||||
tab_index += 1;
|
|
||||||
tab_index - 1
|
ai_upsell_card.tab_index = Some({
|
||||||
})),
|
tab_index += 1;
|
||||||
)
|
tab_index - 1
|
||||||
|
});
|
||||||
|
|
||||||
|
ai_upsell_card
|
||||||
|
})
|
||||||
.child(render_llm_provider_section(
|
.child(render_llm_provider_section(
|
||||||
&mut tab_index,
|
&mut tab_index,
|
||||||
workspace,
|
workspace,
|
||||||
|
|
|
@ -850,19 +850,13 @@ impl workspace::SerializableItem for Onboarding {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod persistence {
|
mod persistence {
|
||||||
use db::{
|
use db::{define_connection, query, sqlez_macros::sql};
|
||||||
query,
|
|
||||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
|
||||||
sqlez_macros::sql,
|
|
||||||
};
|
|
||||||
use workspace::WorkspaceDb;
|
use workspace::WorkspaceDb;
|
||||||
|
|
||||||
pub struct OnboardingPagesDb(ThreadSafeConnection);
|
define_connection! {
|
||||||
|
pub static ref ONBOARDING_PAGES: OnboardingPagesDb<WorkspaceDb> =
|
||||||
impl Domain for OnboardingPagesDb {
|
&[
|
||||||
const NAME: &str = stringify!(OnboardingPagesDb);
|
sql!(
|
||||||
|
|
||||||
const MIGRATIONS: &[&str] = &[sql!(
|
|
||||||
CREATE TABLE onboarding_pages (
|
CREATE TABLE onboarding_pages (
|
||||||
workspace_id INTEGER,
|
workspace_id INTEGER,
|
||||||
item_id INTEGER UNIQUE,
|
item_id INTEGER UNIQUE,
|
||||||
|
@ -872,11 +866,10 @@ mod persistence {
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)];
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
|
|
||||||
|
|
||||||
impl OnboardingPagesDb {
|
impl OnboardingPagesDb {
|
||||||
query! {
|
query! {
|
||||||
pub async fn save_onboarding_page(
|
pub async fn save_onboarding_page(
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue