Compare commits
40 Commits
acp-rewind
...
use-defaul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26149bfc33 | ||
|
|
97bcb15e9f | ||
|
|
c72e594afe | ||
|
|
b4d4294bee | ||
|
|
e5c0614e88 | ||
|
|
ea347b0aa1 | ||
|
|
a03897012e | ||
|
|
f4071bdd8e | ||
|
|
abd6009b41 | ||
|
|
a3e1611fa8 | ||
|
|
e6e64017ea | ||
|
|
d0aef3cec1 | ||
|
|
1eae76e856 | ||
|
|
d713390366 | ||
|
|
9614b72b06 | ||
|
|
d7c735959e | ||
|
|
d8847192c8 | ||
|
|
bd4e943597 | ||
|
|
c5d3c7d790 | ||
|
|
fff0ecead1 | ||
|
|
b1b60bb7fe | ||
|
|
0e575b2809 | ||
|
|
65c6c709fd | ||
|
|
858ab9cc23 | ||
|
|
2c64b05ea4 | ||
|
|
b7dad2cf71 | ||
|
|
76dbcde628 | ||
|
|
aa0f7a2d09 | ||
|
|
372b3c7af6 | ||
|
|
10a1140d49 | ||
|
|
e96b68bc15 | ||
|
|
b249593abe | ||
|
|
c14d84cfdb | ||
|
|
428fc6d483 | ||
|
|
64b14ef848 | ||
|
|
bf5ed6d1c9 | ||
|
|
bb5cfe118f | ||
|
|
633ce23ae9 | ||
|
|
d43df9e841 | ||
|
|
f8667a8379 |
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
### Description
|
||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
|
||||
- Any code must be sufficient to reproduce (include context!)
|
||||
- Code must as text, not just as a screenshot.
|
||||
- Include code as text, not just as a screenshot.
|
||||
- Issues with insufficient detail may be summarily closed.
|
||||
-->
|
||||
|
||||
|
||||
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -191,7 +191,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.0.32"
|
||||
version = "0.0.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -17183,8 +17185,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-cpp"
|
||||
version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=5cb9b693cfd7bfacab1d9ff4acac1a4150700609#5cb9b693cfd7bfacab1d9ff4acac1a4150700609"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
@@ -20394,7 +20395,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.202.0"
|
||||
version = "0.203.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
|
||||
@@ -426,7 +426,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { path = "../agent-client-protocol"}
|
||||
agent-client-protocol = "0.0.31"
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
@@ -624,7 +624,7 @@ tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.6", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.25.0"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
|
||||
tree-sitter-css = "0.23"
|
||||
tree-sitter-diff = "0.1.0"
|
||||
tree-sitter-elixir = "0.3"
|
||||
|
||||
4
assets/icons/terminal_ghost.svg
Normal file
4
assets/icons/terminal_ghost.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 12.375H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 11.125L6.75003 7.375L3 3.62497" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 336 B |
1257
assets/images/acp_grid.svg
Normal file
1257
assets/images/acp_grid.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 176 KiB |
1
assets/images/acp_logo.svg
Normal file
1
assets/images/acp_logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="61" fill="none"><g clip-path="url(#a)"><path fill="#000" d="M130.75.385c5.428 0 10.297 2.81 13.011 7.511l14.214 24.618-.013-.005c2.599 4.504 2.707 9.932.28 14.513-2.618 4.944-7.862 8.015-13.679 8.015h-31.811c-.452 0-.873-.242-1.103-.637a1.268 1.268 0 0 1 0-1.274l3.919-6.78c.223-.394.65-.636 1.102-.636h28.288a5.622 5.622 0 0 0 4.925-2.849 5.615 5.615 0 0 0 0-5.69l-14.214-24.617a5.621 5.621 0 0 0-4.925-2.848 5.621 5.621 0 0 0-4.925 2.848l-14.214 24.618a6.267 6.267 0 0 0-.319.643.998.998 0 0 1-.069.14L101.724 54.4l-.823 1.313-2.529 4.39a1.27 1.27 0 0 1-1.103.636h-7.83c-.452 0-.873-.242-1.102-.637-.23-.394-.23-.879 0-1.274l2.188-3.791H66.803c-3.32 0-6.454-1.122-8.818-3.167a17.141 17.141 0 0 1-3.394-3.96 1.261 1.261 0 0 1-.091-.137L34.2 12.573a5.622 5.622 0 0 0-4.925-2.849 5.621 5.621 0 0 0-4.924 2.85L10.137 37.19a5.615 5.615 0 0 0 0 5.69 5.63 5.63 0 0 0 4.925 2.841h29.862a1.276 1.276 0 0 1 1.102 1.912l-3.912 6.778a1.27 1.27 0 0 1-1.102.638H14.495c-3.32 0-6.454-1.128-8.817-3.173-5.906-5.104-7.36-12.883-3.62-19.363L16.267 7.89C18.872 3.385 23.517.583 28.697.39c.184-.006.356-.006.534-.006 5.378 0 10.45 3.007 13.246 7.85l12.986 22.372L68.58 7.891C71.186 3.385 75.83.582 81.01.39c.185-.006.358-.006.536-.006 4.453 0 8.71 2.039 11.672 5.588.337.407.388.98.127 1.446l-3.765 6.6a1.268 1.268 0 0 1-2.205.006l-.847-1.465a5.623 5.623 0 0 0-4.926-2.848 5.622 5.622 0 0 0-4.924 2.848L62.464 37.18a5.614 5.614 0 0 0 0 5.689 5.628 5.628 0 0 0 4.925 2.842H95.91L117.76 7.87c2.714-4.683 7.575-7.486 12.99-7.486Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .385h160v60.36H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
2
assets/images/acp_logo_serif.svg
Normal file
2
assets/images/acp_logo_serif.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
@@ -40,7 +40,7 @@
|
||||
"shift-f11": "debugger::StepOut",
|
||||
"f11": "zed::ToggleFullScreen",
|
||||
"ctrl-alt-z": "edit_prediction::RateCompletions",
|
||||
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-l": "lsp_tool::ToggleMenu"
|
||||
}
|
||||
},
|
||||
@@ -120,7 +120,7 @@
|
||||
"alt-g m": "git::OpenModifiedFiles",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"ctrl-alt-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint"
|
||||
}
|
||||
@@ -130,8 +130,8 @@
|
||||
"bindings": {
|
||||
"shift-enter": "editor::Newline",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "editor::NewlineAbove",
|
||||
"ctrl-shift-enter": "editor::NewlineBelow",
|
||||
"ctrl-enter": "editor::NewlineBelow",
|
||||
"ctrl-shift-enter": "editor::NewlineAbove",
|
||||
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-k z": "editor::ToggleSoftWrap",
|
||||
"find": "buffer_search::Deploy",
|
||||
|
||||
1260
assets/keymaps/default-windows.json
Normal file
1260
assets/keymaps/default-windows.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@
|
||||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
|
||||
@@ -428,11 +428,13 @@
|
||||
"g h": "vim::StartOfLine",
|
||||
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
||||
"g e": "vim::EndOfDocument",
|
||||
"g .": "vim::HelixGotoLastModification", // go to last modification
|
||||
"g r": "editor::FindAllReferences", // zed specific
|
||||
"g t": "vim::WindowTop",
|
||||
"g c": "vim::WindowMiddle",
|
||||
"g b": "vim::WindowBottom",
|
||||
|
||||
"shift-r": "editor::Paste",
|
||||
"x": "editor::SelectLine",
|
||||
"shift-x": "editor::SelectLine",
|
||||
"%": "editor::SelectAll",
|
||||
|
||||
@@ -653,6 +653,8 @@
|
||||
// "never"
|
||||
"show": "always"
|
||||
},
|
||||
// Whether to enable drag-and-drop operations in the project panel.
|
||||
"drag_and_drop": true,
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
"hide_root": false
|
||||
},
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
// "args": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
"shell": "system"
|
||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||
"tags": []
|
||||
// "tags": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -32,15 +32,10 @@ use std::time::{Duration, Instant};
|
||||
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
|
||||
use ui::App;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn new_prompt_id() -> acp::PromptId {
|
||||
acp::PromptId(Uuid::new_v4().to_string().into())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserMessage {
|
||||
pub prompt_id: Option<acp::PromptId>,
|
||||
pub id: Option<UserMessageId>,
|
||||
pub content: ContentBlock,
|
||||
pub chunks: Vec<acp::ContentBlock>,
|
||||
pub checkpoint: Option<Checkpoint>,
|
||||
@@ -794,15 +789,10 @@ pub enum ThreadStatus {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LoadError {
|
||||
NotInstalled {
|
||||
error_message: SharedString,
|
||||
install_message: SharedString,
|
||||
install_command: String,
|
||||
},
|
||||
NotInstalled,
|
||||
Unsupported {
|
||||
error_message: SharedString,
|
||||
upgrade_message: SharedString,
|
||||
upgrade_command: String,
|
||||
command: SharedString,
|
||||
current_version: SharedString,
|
||||
},
|
||||
Exited {
|
||||
status: ExitStatus,
|
||||
@@ -813,9 +803,12 @@ pub enum LoadError {
|
||||
impl Display for LoadError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LoadError::NotInstalled { error_message, .. }
|
||||
| LoadError::Unsupported { error_message, .. } => {
|
||||
write!(f, "{error_message}")
|
||||
LoadError::NotInstalled => write!(f, "not installed"),
|
||||
LoadError::Unsupported {
|
||||
command: path,
|
||||
current_version,
|
||||
} => {
|
||||
write!(f, "version {current_version} from {path} is not supported")
|
||||
}
|
||||
LoadError::Exited { status } => write!(f, "Server exited with status {status}"),
|
||||
LoadError::Other(msg) => write!(f, "{}", msg),
|
||||
@@ -967,7 +960,7 @@ impl AcpThread {
|
||||
|
||||
pub fn push_user_content_block(
|
||||
&mut self,
|
||||
prompt_id: Option<acp::PromptId>,
|
||||
message_id: Option<UserMessageId>,
|
||||
chunk: acp::ContentBlock,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -976,13 +969,13 @@ impl AcpThread {
|
||||
|
||||
if let Some(last_entry) = self.entries.last_mut()
|
||||
&& let AgentThreadEntry::UserMessage(UserMessage {
|
||||
prompt_id: id,
|
||||
id,
|
||||
content,
|
||||
chunks,
|
||||
..
|
||||
}) = last_entry
|
||||
{
|
||||
*id = prompt_id.or(id.take());
|
||||
*id = message_id.or(id.take());
|
||||
content.append(chunk.clone(), &language_registry, cx);
|
||||
chunks.push(chunk);
|
||||
let idx = entries_len - 1;
|
||||
@@ -991,7 +984,7 @@ impl AcpThread {
|
||||
let content = ContentBlock::new(chunk.clone(), &language_registry, cx);
|
||||
self.push_entry(
|
||||
AgentThreadEntry::UserMessage(UserMessage {
|
||||
prompt_id,
|
||||
id: message_id,
|
||||
content,
|
||||
chunks: vec![chunk],
|
||||
checkpoint: None,
|
||||
@@ -1341,7 +1334,6 @@ impl AcpThread {
|
||||
cx: &mut Context<Self>,
|
||||
) -> BoxFuture<'static, Result<()>> {
|
||||
self.send(
|
||||
new_prompt_id(),
|
||||
vec![acp::ContentBlock::Text(acp::TextContent {
|
||||
text: message.to_string(),
|
||||
annotations: None,
|
||||
@@ -1352,7 +1344,6 @@ impl AcpThread {
|
||||
|
||||
pub fn send(
|
||||
&mut self,
|
||||
prompt_id: acp::PromptId,
|
||||
message: Vec<acp::ContentBlock>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> BoxFuture<'static, Result<()>> {
|
||||
@@ -1362,17 +1353,22 @@ impl AcpThread {
|
||||
cx,
|
||||
);
|
||||
let request = acp::PromptRequest {
|
||||
prompt_id: Some(prompt_id.clone()),
|
||||
prompt: message.clone(),
|
||||
session_id: self.session_id.clone(),
|
||||
};
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
|
||||
let message_id = if self.connection.truncate(&self.session_id, cx).is_some() {
|
||||
Some(UserMessageId::new())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.run_turn(cx, async move |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.push_entry(
|
||||
AgentThreadEntry::UserMessage(UserMessage {
|
||||
prompt_id: Some(prompt_id),
|
||||
id: message_id.clone(),
|
||||
content: block,
|
||||
chunks: message,
|
||||
checkpoint: None,
|
||||
@@ -1394,7 +1390,7 @@ impl AcpThread {
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
this.connection.prompt(request, cx)
|
||||
this.connection.prompt(message_id, request, cx)
|
||||
})?
|
||||
.await
|
||||
})
|
||||
@@ -1511,8 +1507,8 @@ impl AcpThread {
|
||||
|
||||
/// Rewinds this thread to before the entry at `index`, removing it and all
|
||||
/// subsequent entries while reverting any changes made from that point.
|
||||
pub fn rewind(&mut self, id: acp::PromptId, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let Some(rewind) = self.connection.rewind(&self.session_id, cx) else {
|
||||
pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let Some(truncate) = self.connection.truncate(&self.session_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("not supported")));
|
||||
};
|
||||
let Some(message) = self.user_message(&id) else {
|
||||
@@ -1532,7 +1528,7 @@ impl AcpThread {
|
||||
.await?;
|
||||
}
|
||||
|
||||
cx.update(|cx| rewind.rewind(id.clone(), cx))?.await?;
|
||||
cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some((ix, _)) = this.user_message_mut(&id) {
|
||||
let range = ix..this.entries.len();
|
||||
@@ -1596,10 +1592,10 @@ impl AcpThread {
|
||||
})
|
||||
}
|
||||
|
||||
fn user_message(&self, id: &acp::PromptId) -> Option<&UserMessage> {
|
||||
fn user_message(&self, id: &UserMessageId) -> Option<&UserMessage> {
|
||||
self.entries.iter().find_map(|entry| {
|
||||
if let AgentThreadEntry::UserMessage(message) = entry {
|
||||
if message.prompt_id.as_ref() == Some(id) {
|
||||
if message.id.as_ref() == Some(id) {
|
||||
Some(message)
|
||||
} else {
|
||||
None
|
||||
@@ -1610,10 +1606,10 @@ impl AcpThread {
|
||||
})
|
||||
}
|
||||
|
||||
fn user_message_mut(&mut self, id: &acp::PromptId) -> Option<(usize, &mut UserMessage)> {
|
||||
fn user_message_mut(&mut self, id: &UserMessageId) -> Option<(usize, &mut UserMessage)> {
|
||||
self.entries.iter_mut().enumerate().find_map(|(ix, entry)| {
|
||||
if let AgentThreadEntry::UserMessage(message) = entry {
|
||||
if message.prompt_id.as_ref() == Some(id) {
|
||||
if message.id.as_ref() == Some(id) {
|
||||
Some((ix, message))
|
||||
} else {
|
||||
None
|
||||
@@ -1907,7 +1903,7 @@ mod tests {
|
||||
thread.update(cx, |thread, cx| {
|
||||
assert_eq!(thread.entries.len(), 1);
|
||||
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] {
|
||||
assert_eq!(user_msg.prompt_id, None);
|
||||
assert_eq!(user_msg.id, None);
|
||||
assert_eq!(user_msg.content.to_markdown(cx), "Hello, ");
|
||||
} else {
|
||||
panic!("Expected UserMessage");
|
||||
@@ -1915,7 +1911,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Test appending to existing user message
|
||||
let message_1_id = new_prompt_id();
|
||||
let message_1_id = UserMessageId::new();
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_user_content_block(
|
||||
Some(message_1_id.clone()),
|
||||
@@ -1930,7 +1926,7 @@ mod tests {
|
||||
thread.update(cx, |thread, cx| {
|
||||
assert_eq!(thread.entries.len(), 1);
|
||||
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] {
|
||||
assert_eq!(user_msg.prompt_id, Some(message_1_id));
|
||||
assert_eq!(user_msg.id, Some(message_1_id));
|
||||
assert_eq!(user_msg.content.to_markdown(cx), "Hello, world!");
|
||||
} else {
|
||||
panic!("Expected UserMessage");
|
||||
@@ -1949,7 +1945,7 @@ mod tests {
|
||||
);
|
||||
});
|
||||
|
||||
let message_2_id = new_prompt_id();
|
||||
let message_2_id = UserMessageId::new();
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_user_content_block(
|
||||
Some(message_2_id.clone()),
|
||||
@@ -1964,7 +1960,7 @@ mod tests {
|
||||
thread.update(cx, |thread, cx| {
|
||||
assert_eq!(thread.entries.len(), 3);
|
||||
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[2] {
|
||||
assert_eq!(user_msg.prompt_id, Some(message_2_id));
|
||||
assert_eq!(user_msg.id, Some(message_2_id));
|
||||
assert_eq!(user_msg.content.to_markdown(cx), "New user message");
|
||||
} else {
|
||||
panic!("Expected UserMessage at index 2");
|
||||
@@ -2261,13 +2257,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), vec!["Hi".into()], cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Hi".into()], cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(cx.read(|cx| !thread.read(cx).has_pending_edit_tool_calls()));
|
||||
}
|
||||
@@ -2326,13 +2318,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), vec!["Lorem".into()], cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Lorem".into()], cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(cx),
|
||||
@@ -2350,13 +2338,9 @@ mod tests {
|
||||
});
|
||||
assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]);
|
||||
|
||||
cx.update(|cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), vec!["ipsum".into()], cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["ipsum".into()], cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(cx),
|
||||
@@ -2390,13 +2374,9 @@ mod tests {
|
||||
|
||||
// Checkpoint isn't stored when there are no changes.
|
||||
simulate_changes.store(false, SeqCst);
|
||||
cx.update(|cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), vec!["dolor".into()], cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["dolor".into()], cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(cx),
|
||||
@@ -2442,7 +2422,7 @@ mod tests {
|
||||
let AgentThreadEntry::UserMessage(message) = &thread.entries[2] else {
|
||||
panic!("unexpected entries {:?}", thread.entries)
|
||||
};
|
||||
thread.rewind(message.prompt_id.clone().unwrap(), cx)
|
||||
thread.rewind(message.id.clone().unwrap(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2508,13 +2488,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), vec!["hello".into()], cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(cx),
|
||||
@@ -2534,13 +2510,9 @@ mod tests {
|
||||
// Simulate refusing the second message, ensuring the conversation gets
|
||||
// truncated to before sending it.
|
||||
refuse_next.store(true, SeqCst);
|
||||
cx.update(|cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), vec!["world".into()], cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(
|
||||
thread.to_markdown(cx),
|
||||
@@ -2679,6 +2651,7 @@ mod tests {
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<acp::PromptResponse>> {
|
||||
@@ -2708,11 +2681,11 @@ mod tests {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn rewind(
|
||||
fn truncate(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionRewind>> {
|
||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||
Some(Rc::new(FakeAgentSessionEditor {
|
||||
_session_id: session_id.clone(),
|
||||
}))
|
||||
@@ -2727,8 +2700,8 @@ mod tests {
|
||||
_session_id: acp::SessionId,
|
||||
}
|
||||
|
||||
impl AgentSessionRewind for FakeAgentSessionEditor {
|
||||
fn rewind(&self, _message_id: acp::PromptId, _cx: &mut App) -> Task<Result<()>> {
|
||||
impl AgentSessionTruncate for FakeAgentSessionEditor {
|
||||
fn run(&self, _message_id: UserMessageId, _cx: &mut App) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,19 @@ use collections::IndexMap;
|
||||
use gpui::{Entity, SharedString, Task};
|
||||
use language_model::LanguageModelProviderId;
|
||||
use project::Project;
|
||||
use std::{any::Any, error::Error, fmt, path::Path, rc::Rc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc};
|
||||
use ui::{App, IconName};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub struct UserMessageId(Arc<str>);
|
||||
|
||||
impl UserMessageId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4().to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AgentConnection {
|
||||
fn new_thread(
|
||||
@@ -20,8 +31,12 @@ pub trait AgentConnection {
|
||||
|
||||
fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
|
||||
|
||||
fn prompt(&self, params: acp::PromptRequest, cx: &mut App)
|
||||
-> Task<Result<acp::PromptResponse>>;
|
||||
fn prompt(
|
||||
&self,
|
||||
user_message_id: Option<UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>>;
|
||||
|
||||
fn resume(
|
||||
&self,
|
||||
@@ -33,11 +48,11 @@ pub trait AgentConnection {
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
|
||||
|
||||
fn rewind(
|
||||
fn truncate(
|
||||
&self,
|
||||
_session_id: &acp::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionRewind>> {
|
||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -70,8 +85,8 @@ impl dyn AgentConnection {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AgentSessionRewind {
|
||||
fn rewind(&self, message_id: acp::PromptId, cx: &mut App) -> Task<Result<()>>;
|
||||
pub trait AgentSessionTruncate {
|
||||
fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
pub trait AgentSessionResume {
|
||||
@@ -347,6 +362,7 @@ mod test_support {
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<acp::PromptResponse>> {
|
||||
@@ -416,11 +432,11 @@ mod test_support {
|
||||
}
|
||||
}
|
||||
|
||||
fn rewind(
|
||||
fn truncate(
|
||||
&self,
|
||||
_session_id: &agent_client_protocol::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionRewind>> {
|
||||
) -> Option<Rc<dyn AgentSessionTruncate>> {
|
||||
Some(Rc::new(StubAgentSessionEditor))
|
||||
}
|
||||
|
||||
@@ -431,8 +447,8 @@ mod test_support {
|
||||
|
||||
struct StubAgentSessionEditor;
|
||||
|
||||
impl AgentSessionRewind for StubAgentSessionEditor {
|
||||
fn rewind(&self, _: acp::PromptId, _: &mut App) -> Task<Result<()>> {
|
||||
impl AgentSessionTruncate for StubAgentSessionEditor {
|
||||
fn run(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,7 +664,7 @@ impl Thread {
|
||||
}
|
||||
|
||||
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
|
||||
if self.configured_model.is_none() || self.messages.is_empty() {
|
||||
if self.configured_model.is_none() {
|
||||
self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
}
|
||||
self.configured_model.clone()
|
||||
@@ -2097,7 +2097,7 @@ impl Thread {
|
||||
}
|
||||
|
||||
pub fn summarize(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
|
||||
println!("No thread summary model");
|
||||
return;
|
||||
};
|
||||
@@ -2416,7 +2416,7 @@ impl Thread {
|
||||
}
|
||||
|
||||
let Some(ConfiguredModel { model, provider }) =
|
||||
LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
|
||||
LanguageModelRegistry::read_global(cx).thread_summary_model()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -5410,10 +5410,13 @@ fn main() {{
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
registry.set_thread_summary_model(Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}));
|
||||
registry.set_thread_summary_model(
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -61,16 +61,19 @@ pub struct LanguageModels {
|
||||
model_list: acp_thread::AgentModelList,
|
||||
refresh_models_rx: watch::Receiver<()>,
|
||||
refresh_models_tx: watch::Sender<()>,
|
||||
_authenticate_all_providers_task: Task<()>,
|
||||
}
|
||||
|
||||
impl LanguageModels {
|
||||
fn new(cx: &App) -> Self {
|
||||
fn new(cx: &mut App) -> Self {
|
||||
let (refresh_models_tx, refresh_models_rx) = watch::channel(());
|
||||
|
||||
let mut this = Self {
|
||||
models: HashMap::default(),
|
||||
model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()),
|
||||
refresh_models_rx,
|
||||
refresh_models_tx,
|
||||
_authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx),
|
||||
};
|
||||
this.refresh_list(cx);
|
||||
this
|
||||
@@ -150,6 +153,52 @@ impl LanguageModels {
|
||||
fn model_id(model: &Arc<dyn LanguageModel>) -> acp_thread::AgentModelId {
|
||||
acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
|
||||
}
|
||||
|
||||
fn authenticate_all_language_model_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.background_spawn(async move {
|
||||
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
|
||||
if let Err(err) = authenticate_task.await {
|
||||
if matches!(err, language_model::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 struct NativeAgent {
|
||||
@@ -228,7 +277,7 @@ impl NativeAgent {
|
||||
) -> Entity<AcpThread> {
|
||||
let connection = Rc::new(NativeAgentConnection(cx.entity()));
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
let summarization_model = registry.thread_summary_model(cx).map(|c| c.model);
|
||||
let summarization_model = registry.thread_summary_model().map(|c| c.model);
|
||||
|
||||
thread_handle.update(cx, |thread, cx| {
|
||||
thread.set_summarization_model(summarization_model, cx);
|
||||
@@ -524,7 +573,7 @@ impl NativeAgent {
|
||||
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
let default_model = registry.default_model().map(|m| m.model);
|
||||
let summarization_model = registry.thread_summary_model(cx).map(|m| m.model);
|
||||
let summarization_model = registry.thread_summary_model().map(|m| m.model);
|
||||
|
||||
for session in self.sessions.values_mut() {
|
||||
session.thread.update(cx, |thread, cx| {
|
||||
@@ -905,10 +954,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let id = params.prompt_id.expect("UserMessageId is required");
|
||||
let id = id.expect("UserMessageId is required");
|
||||
let session_id = params.session_id.clone();
|
||||
log::info!("Received prompt request for session: {}", session_id);
|
||||
log::debug!("Prompt blocks count: {}", params.prompt.len());
|
||||
@@ -947,11 +997,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
});
|
||||
}
|
||||
|
||||
fn rewind(
|
||||
fn truncate(
|
||||
&self,
|
||||
session_id: &agent_client_protocol::SessionId,
|
||||
cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionRewind>> {
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
|
||||
self.0.read_with(cx, |agent, _cx| {
|
||||
agent.sessions.get(session_id).map(|session| {
|
||||
Rc::new(NativeAgentSessionEditor {
|
||||
@@ -1008,10 +1058,10 @@ struct NativeAgentSessionEditor {
|
||||
acp_thread: WeakEntity<AcpThread>,
|
||||
}
|
||||
|
||||
impl acp_thread::AgentSessionRewind for NativeAgentSessionEditor {
|
||||
fn rewind(&self, message_id: acp::PromptId, cx: &mut App) -> Task<Result<()>> {
|
||||
impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor {
|
||||
fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
|
||||
match self.thread.update(cx, |thread, cx| {
|
||||
thread.rewind(message_id.clone(), cx)?;
|
||||
thread.truncate(message_id.clone(), cx)?;
|
||||
Ok(thread.latest_token_usage())
|
||||
}) {
|
||||
Ok(usage) => {
|
||||
@@ -1064,7 +1114,6 @@ mod tests {
|
||||
use super::*;
|
||||
use acp_thread::{
|
||||
AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo, MentionUri,
|
||||
new_prompt_id,
|
||||
};
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
@@ -1311,7 +1360,6 @@ mod tests {
|
||||
|
||||
let send = acp_thread.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
new_prompt_id(),
|
||||
vec![
|
||||
"What does ".into(),
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent};
|
||||
use acp_thread::new_prompt_id;
|
||||
use acp_thread::UserMessageId;
|
||||
use agent::{thread::DetailedSummaryState, thread_store};
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::{AgentProfileId, CompletionMode};
|
||||
@@ -43,7 +43,7 @@ pub struct DbThread {
|
||||
#[serde(default)]
|
||||
pub cumulative_token_usage: language_model::TokenUsage,
|
||||
#[serde(default)]
|
||||
pub request_token_usage: HashMap<acp::PromptId, language_model::TokenUsage>,
|
||||
pub request_token_usage: HashMap<acp_thread::UserMessageId, language_model::TokenUsage>,
|
||||
#[serde(default)]
|
||||
pub model: Option<DbLanguageModel>,
|
||||
#[serde(default)]
|
||||
@@ -97,7 +97,7 @@ impl DbThread {
|
||||
content.push(UserMessageContent::Text(msg.context));
|
||||
}
|
||||
|
||||
let id = new_prompt_id();
|
||||
let id = UserMessageId::new();
|
||||
last_user_message_id = Some(id.clone());
|
||||
|
||||
crate::Message::User(UserMessage {
|
||||
|
||||
@@ -42,6 +42,10 @@ impl AgentServer for NativeAgentServer {
|
||||
ui::IconName::ZedAgent
|
||||
}
|
||||
|
||||
fn install_command(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
_root_dir: &Path,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, new_prompt_id};
|
||||
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId};
|
||||
use agent_client_protocol::{self as acp};
|
||||
use agent_settings::AgentProfileId;
|
||||
use anyhow::Result;
|
||||
@@ -48,7 +48,7 @@ async fn test_echo(cx: &mut TestAppContext) {
|
||||
|
||||
let events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), ["Testing: Reply with 'Hello'"], cx)
|
||||
thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -72,6 +72,7 @@ async fn test_echo(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_thinking(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
@@ -79,7 +80,7 @@ async fn test_thinking(cx: &mut TestAppContext) {
|
||||
let events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
new_prompt_id(),
|
||||
UserMessageId::new(),
|
||||
[indoc! {"
|
||||
Testing:
|
||||
|
||||
@@ -130,7 +131,9 @@ async fn test_system_prompt(cx: &mut TestAppContext) {
|
||||
});
|
||||
thread.update(cx, |thread, _| thread.add_tool(EchoTool));
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.send(new_prompt_id(), ["abc"], cx))
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
let mut pending_completions = fake_model.pending_completions();
|
||||
@@ -166,7 +169,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
||||
// Send initial user message and verify it's cached
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), ["Message 1"], cx)
|
||||
thread.send(UserMessageId::new(), ["Message 1"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -189,7 +192,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
||||
// Send another user message and verify only the latest is cached
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), ["Message 2"], cx)
|
||||
thread.send(UserMessageId::new(), ["Message 2"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -225,7 +228,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
||||
thread.update(cx, |thread, _| thread.add_tool(EchoTool));
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), ["Use the echo tool"], cx)
|
||||
thread.send(UserMessageId::new(), ["Use the echo tool"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -302,7 +305,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(
|
||||
new_prompt_id(),
|
||||
UserMessageId::new(),
|
||||
["Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'."],
|
||||
cx,
|
||||
)
|
||||
@@ -318,7 +321,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
|
||||
thread.remove_tool(&EchoTool::name());
|
||||
thread.add_tool(DelayTool);
|
||||
thread.send(
|
||||
new_prompt_id(),
|
||||
UserMessageId::new(),
|
||||
[
|
||||
"Now call the delay tool with 200ms.",
|
||||
"When the timer goes off, then you echo the output of the tool.",
|
||||
@@ -361,7 +364,7 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) {
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(WordListTool);
|
||||
thread.send(new_prompt_id(), ["Test the word_list tool."], cx)
|
||||
thread.send(UserMessageId::new(), ["Test the word_list tool."], cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -412,7 +415,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(ToolRequiringPermission);
|
||||
thread.send(new_prompt_id(), ["abc"], cx)
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -469,7 +472,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
tool_name: ToolRequiringPermission::name().into(),
|
||||
is_error: true,
|
||||
content: "Permission to run tool denied by user".into(),
|
||||
output: None
|
||||
output: Some("Permission to run tool denied by user".into())
|
||||
})
|
||||
]
|
||||
);
|
||||
@@ -542,7 +545,9 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| thread.send(new_prompt_id(), ["abc"], cx))
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
@@ -571,7 +576,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
|
||||
let events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(new_prompt_id(), ["abc"], cx)
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -680,7 +685,7 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
|
||||
let events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(new_prompt_id(), ["abc"], cx)
|
||||
thread.send(UserMessageId::new(), ["abc"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -714,7 +719,7 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), vec!["ghi"], cx)
|
||||
thread.send(UserMessageId::new(), vec!["ghi"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -814,7 +819,7 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(DelayTool);
|
||||
thread.send(
|
||||
new_prompt_id(),
|
||||
UserMessageId::new(),
|
||||
[
|
||||
"Call the delay tool twice in the same message.",
|
||||
"Once with 100ms. Once with 300ms.",
|
||||
@@ -894,7 +899,7 @@ async fn test_profiles(cx: &mut TestAppContext) {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_profile(AgentProfileId("test-1".into()));
|
||||
thread.send(new_prompt_id(), ["test"], cx)
|
||||
thread.send(UserMessageId::new(), ["test"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -914,7 +919,7 @@ async fn test_profiles(cx: &mut TestAppContext) {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_profile(AgentProfileId("test-2".into()));
|
||||
thread.send(new_prompt_id(), ["test2"], cx)
|
||||
thread.send(UserMessageId::new(), ["test2"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -982,7 +987,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
let events = thread.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), ["Hey"], cx).unwrap()
|
||||
thread.send(UserMessageId::new(), ["Hey"], cx).unwrap()
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -1024,7 +1029,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
|
||||
// Send again after adding the echo tool, ensuring the name collision is resolved.
|
||||
let events = thread.update(cx, |thread, cx| {
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(new_prompt_id(), ["Go"], cx).unwrap()
|
||||
thread.send(UserMessageId::new(), ["Go"], cx).unwrap()
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let completion = fake_model.pending_completions().pop().unwrap();
|
||||
@@ -1231,7 +1236,9 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.send(new_prompt_id(), ["Go"], cx))
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Go"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
let completion = fake_model.pending_completions().pop().unwrap();
|
||||
@@ -1265,7 +1272,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
thread.add_tool(InfiniteTool);
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(
|
||||
new_prompt_id(),
|
||||
UserMessageId::new(),
|
||||
["Call the echo tool, then call the infinite tool, then explain their output"],
|
||||
cx,
|
||||
)
|
||||
@@ -1321,7 +1328,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
let events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
new_prompt_id(),
|
||||
UserMessageId::new(),
|
||||
["Testing: reply with 'Hello' then stop."],
|
||||
cx,
|
||||
)
|
||||
@@ -1341,13 +1348,14 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let events_1 = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), ["Hello 1"], cx)
|
||||
thread.send(UserMessageId::new(), ["Hello 1"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -1356,7 +1364,7 @@ async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
||||
|
||||
let events_2 = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), ["Hello 2"], cx)
|
||||
thread.send(UserMessageId::new(), ["Hello 2"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -1378,7 +1386,7 @@ async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) {
|
||||
|
||||
let events_1 = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), ["Hello 1"], cx)
|
||||
thread.send(UserMessageId::new(), ["Hello 1"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -1390,7 +1398,7 @@ async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) {
|
||||
|
||||
let events_2 = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), ["Hello 2"], cx)
|
||||
thread.send(UserMessageId::new(), ["Hello 2"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -1410,7 +1418,9 @@ async fn test_refusal(cx: &mut TestAppContext) {
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let events = thread
|
||||
.update(cx, |thread, cx| thread.send(new_prompt_id(), ["Hello"], cx))
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
thread.read_with(cx, |thread, _| {
|
||||
@@ -1456,7 +1466,7 @@ async fn test_truncate_first_message(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let message_id = new_prompt_id();
|
||||
let message_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_id.clone(), ["Hello"], cx)
|
||||
@@ -1508,7 +1518,7 @@ async fn test_truncate_first_message(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.rewind(message_id, cx))
|
||||
.update(cx, |thread, cx| thread.truncate(message_id, cx))
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
thread.read_with(cx, |thread, _| {
|
||||
@@ -1518,7 +1528,9 @@ async fn test_truncate_first_message(cx: &mut TestAppContext) {
|
||||
|
||||
// Ensure we can still send a new message after truncation.
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.send(new_prompt_id(), ["Hi"], cx))
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hi"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
thread.update(cx, |thread, _cx| {
|
||||
assert_eq!(
|
||||
@@ -1572,7 +1584,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), ["Message 1"], cx)
|
||||
thread.send(UserMessageId::new(), ["Message 1"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -1615,7 +1627,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
|
||||
|
||||
assert_first_message_state(cx);
|
||||
|
||||
let second_message_id = new_prompt_id();
|
||||
let second_message_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(second_message_id.clone(), ["Message 2"], cx)
|
||||
@@ -1667,7 +1679,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.rewind(second_message_id, cx))
|
||||
.update(cx, |thread, cx| thread.truncate(second_message_id, cx))
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -1686,7 +1698,9 @@ async fn test_title_generation(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
let send = thread
|
||||
.update(cx, |thread, cx| thread.send(new_prompt_id(), ["Hello"], cx))
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -1707,7 +1721,7 @@ async fn test_title_generation(cx: &mut TestAppContext) {
|
||||
// Send another message, ensuring no title is generated this time.
|
||||
let send = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), ["Hello again"], cx)
|
||||
thread.send(UserMessageId::new(), ["Hello again"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -1728,7 +1742,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
||||
.update(cx, |thread, cx| {
|
||||
thread.add_tool(ToolRequiringPermission);
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(new_prompt_id(), ["Hey!"], cx)
|
||||
thread.send(UserMessageId::new(), ["Hey!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -1808,11 +1822,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
let clock = Arc::new(clock::FakeSystemClock::new());
|
||||
let client = Client::new(clock, http_client, cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
Project::init_settings(cx);
|
||||
agent_settings::init(cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store, client.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
agent_settings::init(cx);
|
||||
});
|
||||
cx.executor().forbid_parking();
|
||||
|
||||
@@ -1880,9 +1894,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
let model = model.as_fake();
|
||||
assert_eq!(model.id().0, "fake", "should return default model");
|
||||
|
||||
let request = acp_thread.update(cx, |thread, cx| {
|
||||
thread.send(new_prompt_id(), vec!["abc".into()], cx)
|
||||
});
|
||||
let request = acp_thread.update(cx, |thread, cx| thread.send(vec!["abc".into()], cx));
|
||||
cx.run_until_parked();
|
||||
model.send_last_completion_stream_text_chunk("def");
|
||||
cx.run_until_parked();
|
||||
@@ -1912,8 +1924,8 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
connection.prompt(
|
||||
Some(acp_thread::UserMessageId::new()),
|
||||
acp::PromptRequest {
|
||||
prompt_id: Some(new_prompt_id()),
|
||||
session_id: session_id.clone(),
|
||||
prompt: vec!["ghi".into()],
|
||||
},
|
||||
@@ -1936,7 +1948,9 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| thread.send(new_prompt_id(), ["Think"], cx))
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Think"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -2037,7 +2051,7 @@ async fn test_send_no_retry_on_success(cx: &mut TestAppContext) {
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
|
||||
thread.send(new_prompt_id(), ["Hello!"], cx)
|
||||
thread.send(UserMessageId::new(), ["Hello!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -2081,7 +2095,7 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
|
||||
thread.send(new_prompt_id(), ["Hello!"], cx)
|
||||
thread.send(UserMessageId::new(), ["Hello!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -2147,7 +2161,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
|
||||
thread.add_tool(EchoTool);
|
||||
thread.send(new_prompt_id(), ["Call the echo tool!"], cx)
|
||||
thread.send(UserMessageId::new(), ["Call the echo tool!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -2222,7 +2236,7 @@ async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) {
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
|
||||
thread.send(new_prompt_id(), ["Hello!"], cx)
|
||||
thread.send(UserMessageId::new(), ["Hello!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
ListDirectoryTool, MovePathTool, NowTool, OpenTool, ReadFileTool, SystemPromptTemplate,
|
||||
Template, Templates, TerminalTool, ThinkingTool, WebSearchTool,
|
||||
};
|
||||
use acp_thread::MentionUri;
|
||||
use acp_thread::{MentionUri, UserMessageId};
|
||||
use action_log::ActionLog;
|
||||
use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot};
|
||||
use agent_client_protocol as acp;
|
||||
@@ -137,7 +137,7 @@ impl Message {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UserMessage {
|
||||
pub id: acp::PromptId,
|
||||
pub id: UserMessageId,
|
||||
pub content: Vec<UserMessageContent>,
|
||||
}
|
||||
|
||||
@@ -564,7 +564,7 @@ pub struct Thread {
|
||||
pending_message: Option<AgentMessage>,
|
||||
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||
tool_use_limit_reached: bool,
|
||||
request_token_usage: HashMap<acp::PromptId, language_model::TokenUsage>,
|
||||
request_token_usage: HashMap<UserMessageId, language_model::TokenUsage>,
|
||||
#[allow(unused)]
|
||||
cumulative_token_usage: TokenUsage,
|
||||
#[allow(unused)]
|
||||
@@ -732,7 +732,17 @@ impl Thread {
|
||||
stream.update_tool_call_fields(
|
||||
&tool_use.id,
|
||||
acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
status: Some(
|
||||
tool_result
|
||||
.as_ref()
|
||||
.map_or(acp::ToolCallStatus::Failed, |result| {
|
||||
if result.is_error {
|
||||
acp::ToolCallStatus::Failed
|
||||
} else {
|
||||
acp::ToolCallStatus::Completed
|
||||
}
|
||||
}),
|
||||
),
|
||||
raw_output: output,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1070,7 +1080,7 @@ impl Thread {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn rewind(&mut self, message_id: acp::PromptId, cx: &mut Context<Self>) -> Result<()> {
|
||||
pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context<Self>) -> Result<()> {
|
||||
self.cancel(cx);
|
||||
let Some(position) = self.messages.iter().position(
|
||||
|msg| matches!(msg, Message::User(UserMessage { id, .. }) if id == &message_id),
|
||||
@@ -1118,7 +1128,7 @@ impl Thread {
|
||||
/// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn.
|
||||
pub fn send<T>(
|
||||
&mut self,
|
||||
id: acp::PromptId,
|
||||
id: UserMessageId,
|
||||
content: impl IntoIterator<Item = T>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>
|
||||
@@ -1557,7 +1567,7 @@ impl Thread {
|
||||
tool_name: tool_use.name,
|
||||
is_error: true,
|
||||
content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())),
|
||||
output: None,
|
||||
output: Some(error.to_string().into()),
|
||||
},
|
||||
}
|
||||
}))
|
||||
@@ -2459,6 +2469,30 @@ impl ToolCallEventStreamReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields {
|
||||
let event = self.0.next().await;
|
||||
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
|
||||
update,
|
||||
)))) = event
|
||||
{
|
||||
update.fields
|
||||
} else {
|
||||
panic!("Expected update fields but got: {:?}", event);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn expect_diff(&mut self) -> Entity<acp_thread::Diff> {
|
||||
let event = self.0.next().await;
|
||||
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff(
|
||||
update,
|
||||
)))) = event
|
||||
{
|
||||
update.diff
|
||||
} else {
|
||||
panic!("Expected diff but got: {:?}", event);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> {
|
||||
let event = self.0.next().await;
|
||||
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(
|
||||
|
||||
@@ -273,6 +273,13 @@ impl AgentTool for EditFileTool {
|
||||
|
||||
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
||||
event_stream.update_diff(diff.clone());
|
||||
let _finalize_diff = util::defer({
|
||||
let diff = diff.downgrade();
|
||||
let mut cx = cx.clone();
|
||||
move || {
|
||||
diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
|
||||
}
|
||||
});
|
||||
|
||||
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let old_text = cx
|
||||
@@ -389,8 +396,6 @@ impl AgentTool for EditFileTool {
|
||||
})
|
||||
.await;
|
||||
|
||||
diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
|
||||
|
||||
let input_path = input.path.display();
|
||||
if unified_diff.is_empty() {
|
||||
anyhow::ensure!(
|
||||
@@ -1545,6 +1550,100 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diff_finalization(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/", json!({"main.rs": ""})).await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
|
||||
let languages = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry.clone(),
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Ensure the diff is finalized after the edit completes.
|
||||
{
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit file".into(),
|
||||
path: path!("/main.rs").into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
stream_tx,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
stream_rx.expect_update_fields().await;
|
||||
let diff = stream_rx.expect_diff().await;
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
||||
cx.run_until_parked();
|
||||
model.end_last_completion_stream();
|
||||
edit.await.unwrap();
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
||||
}
|
||||
|
||||
// Ensure the diff is finalized if an error occurs while editing.
|
||||
{
|
||||
model.forbid_requests();
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit file".into(),
|
||||
path: path!("/main.rs").into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
stream_tx,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
stream_rx.expect_update_fields().await;
|
||||
let diff = stream_rx.expect_diff().await;
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
||||
edit.await.unwrap_err();
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
||||
model.allow_requests();
|
||||
}
|
||||
|
||||
// Ensure the diff is finalized if the tool call gets dropped.
|
||||
{
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit file".into(),
|
||||
path: path!("/main.rs").into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
stream_tx,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
stream_rx.expect_update_fields().await;
|
||||
let diff = stream_rx.expect_diff().await;
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
|
||||
drop(edit);
|
||||
cx.run_until_parked();
|
||||
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
||||
@@ -29,7 +29,6 @@ pub struct AcpConnection {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
supports_rewind: bool,
|
||||
_io_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
@@ -57,7 +56,7 @@ impl AcpConnection {
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
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)
|
||||
@@ -148,10 +147,13 @@ impl AcpConnection {
|
||||
server_name,
|
||||
sessions,
|
||||
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
|
||||
supports_rewind: response.agent_capabilities.rewind_session,
|
||||
_io_task: io_task,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
|
||||
&self.prompt_capabilities
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
@@ -164,12 +166,34 @@ impl AgentConnection for AcpConnection {
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||
let mcp_servers = context_server_store
|
||||
.configured_server_ids()
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let configuration = context_server_store.configuration_for_server(id)?;
|
||||
let command = configuration.command();
|
||||
Some(acp::McpServer {
|
||||
name: id.0.to_string(),
|
||||
command: command.path.clone(),
|
||||
args: command.args.clone(),
|
||||
env: if let Some(env) = command.env.as_ref() {
|
||||
env.iter()
|
||||
.map(|(name, value)| acp::EnvVariable {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let response = conn
|
||||
.new_session(acp::NewSessionRequest {
|
||||
mcp_servers: vec![],
|
||||
cwd,
|
||||
})
|
||||
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
@@ -227,22 +251,9 @@ impl AgentConnection for AcpConnection {
|
||||
})
|
||||
}
|
||||
|
||||
fn rewind(
|
||||
&self,
|
||||
session_id: &agent_client_protocol::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionRewind>> {
|
||||
if !self.supports_rewind {
|
||||
return None;
|
||||
}
|
||||
Some(Rc::new(AcpRewinder {
|
||||
connection: self.connection.clone(),
|
||||
session_id: session_id.clone(),
|
||||
}) as _)
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
@@ -317,25 +328,6 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
struct AcpRewinder {
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
session_id: acp::SessionId,
|
||||
}
|
||||
|
||||
impl acp_thread::AgentSessionRewind for AcpRewinder {
|
||||
fn rewind(&self, prompt_id: acp::PromptId, cx: &mut App) -> Task<Result<()>> {
|
||||
let conn = self.connection.clone();
|
||||
let params = acp::RewindRequest {
|
||||
session_id: self.session_id.clone(),
|
||||
prompt_id,
|
||||
};
|
||||
cx.foreground_executor().spawn(async move {
|
||||
conn.rewind(params).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegate {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
cx: AsyncApp,
|
||||
|
||||
@@ -1,524 +0,0 @@
|
||||
// Translates old acp agents into the new schema
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol as acp;
|
||||
use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
use project::Project;
|
||||
use std::{any::Any, cell::RefCell, path::Path, rc::Rc};
|
||||
use ui::App;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::AgentServerCommand;
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct OldAcpClientDelegate {
|
||||
thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
cx: AsyncApp,
|
||||
next_tool_call_id: Rc<RefCell<u64>>,
|
||||
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
|
||||
}
|
||||
|
||||
impl OldAcpClientDelegate {
|
||||
fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
cx,
|
||||
next_tool_call_id: Rc::new(RefCell::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_old::Client for OldAcpClientDelegate {
|
||||
async fn stream_assistant_message_chunk(
|
||||
&self,
|
||||
params: acp_old::StreamAssistantMessageChunkParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread
|
||||
.borrow()
|
||||
.update(cx, |thread, cx| match params.chunk {
|
||||
acp_old::AssistantMessageChunk::Text { text } => {
|
||||
thread.push_assistant_content_block(text.into(), false, cx)
|
||||
}
|
||||
acp_old::AssistantMessageChunk::Thought { thought } => {
|
||||
thread.push_assistant_content_block(thought.into(), true, cx)
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn request_tool_call_confirmation(
|
||||
&self,
|
||||
request: acp_old::RequestToolCallConfirmationParams,
|
||||
) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
|
||||
self.next_tool_call_id.replace(old_acp_id);
|
||||
|
||||
let tool_call = into_new_tool_call(
|
||||
acp::ToolCallId(old_acp_id.to_string().into()),
|
||||
request.tool_call,
|
||||
);
|
||||
|
||||
let mut options = match request.confirmation {
|
||||
acp_old::ToolCallConfirmation::Edit { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow Edits".to_string(),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", root_command),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Mcp {
|
||||
server_name,
|
||||
tool_name,
|
||||
..
|
||||
} => vec![
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", server_name),
|
||||
),
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
format!("Always Allow {}", tool_name),
|
||||
),
|
||||
],
|
||||
acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow".to_string(),
|
||||
)],
|
||||
acp_old::ToolCallConfirmation::Other { .. } => vec![(
|
||||
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
|
||||
acp::PermissionOptionKind::AllowAlways,
|
||||
"Always Allow".to_string(),
|
||||
)],
|
||||
};
|
||||
|
||||
options.extend([
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::Allow,
|
||||
acp::PermissionOptionKind::AllowOnce,
|
||||
"Allow".to_string(),
|
||||
),
|
||||
(
|
||||
acp_old::ToolCallConfirmationOutcome::Reject,
|
||||
acp::PermissionOptionKind::RejectOnce,
|
||||
"Reject".to_string(),
|
||||
),
|
||||
]);
|
||||
|
||||
let mut outcomes = Vec::with_capacity(options.len());
|
||||
let mut acp_options = Vec::with_capacity(options.len());
|
||||
|
||||
for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
|
||||
outcomes.push(outcome);
|
||||
acp_options.push(acp::PermissionOption {
|
||||
id: acp::PermissionOptionId(index.to_string().into()),
|
||||
name: label,
|
||||
kind,
|
||||
})
|
||||
}
|
||||
|
||||
let response = cx
|
||||
.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(tool_call.into(), acp_options, cx)
|
||||
})
|
||||
})??
|
||||
.context("Failed to update thread")?
|
||||
.await;
|
||||
|
||||
let outcome = match response {
|
||||
Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
|
||||
Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
|
||||
};
|
||||
|
||||
Ok(acp_old::RequestToolCallConfirmationResponse {
|
||||
id: acp_old::ToolCallId(old_acp_id),
|
||||
outcome,
|
||||
})
|
||||
}
|
||||
|
||||
async fn push_tool_call(
|
||||
&self,
|
||||
request: acp_old::PushToolCallParams,
|
||||
) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
|
||||
self.next_tool_call_id.replace(old_acp_id);
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.upsert_tool_call(
|
||||
into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})??
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp_old::PushToolCallResponse {
|
||||
id: acp_old::ToolCallId(old_acp_id),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_tool_call(
|
||||
&self,
|
||||
request: acp_old::UpdateToolCallParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.update_tool_call(
|
||||
acp::ToolCallUpdate {
|
||||
id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(into_new_tool_call_status(request.status)),
|
||||
content: Some(
|
||||
request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_content)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.update_plan(
|
||||
acp::Plan {
|
||||
entries: request
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(into_new_plan_entry)
|
||||
.collect(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
|
||||
) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
|
||||
let content = self
|
||||
.cx
|
||||
.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
thread.read_text_file(path, line, limit, false, cx)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
Ok(acp_old::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
&self,
|
||||
acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
|
||||
) -> Result<(), acp_old::Error> {
|
||||
self.cx
|
||||
.update(|cx| {
|
||||
self.thread
|
||||
.borrow()
|
||||
.update(cx, |thread, cx| thread.write_text_file(path, content, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
|
||||
acp::ToolCall {
|
||||
id,
|
||||
title: request.label,
|
||||
kind: acp_kind_from_old_icon(request.icon),
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
content: request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_content)
|
||||
.collect(),
|
||||
locations: request
|
||||
.locations
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_location)
|
||||
.collect(),
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
|
||||
match icon {
|
||||
acp_old::Icon::FileSearch => acp::ToolKind::Search,
|
||||
acp_old::Icon::Folder => acp::ToolKind::Search,
|
||||
acp_old::Icon::Globe => acp::ToolKind::Search,
|
||||
acp_old::Icon::Hammer => acp::ToolKind::Other,
|
||||
acp_old::Icon::LightBulb => acp::ToolKind::Think,
|
||||
acp_old::Icon::Pencil => acp::ToolKind::Edit,
|
||||
acp_old::Icon::Regex => acp::ToolKind::Search,
|
||||
acp_old::Icon::Terminal => acp::ToolKind::Execute,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
|
||||
match status {
|
||||
acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
|
||||
acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
|
||||
acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
|
||||
match content {
|
||||
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
|
||||
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
|
||||
diff: into_new_diff(diff),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
|
||||
acp::Diff {
|
||||
path: diff.path,
|
||||
old_text: diff.old_text,
|
||||
new_text: diff.new_text,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
|
||||
acp::ToolCallLocation {
|
||||
path: location.path,
|
||||
line: location.line,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
|
||||
acp::PlanEntry {
|
||||
content: entry.content,
|
||||
priority: into_new_plan_priority(entry.priority),
|
||||
status: into_new_plan_status(entry.status),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
|
||||
match priority {
|
||||
acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
|
||||
acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
|
||||
acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
|
||||
match status {
|
||||
acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
|
||||
acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
|
||||
acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpConnection {
|
||||
pub name: &'static str,
|
||||
pub connection: acp_old::AgentConnection,
|
||||
pub _child_status: Task<Result<()>>,
|
||||
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
}
|
||||
|
||||
impl AcpConnection {
|
||||
pub fn stdio(
|
||||
name: &'static str,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Self>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().unwrap();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
log::trace!("Spawned (pid: {})", child.id());
|
||||
|
||||
let foreground_executor = cx.foreground_executor().clone();
|
||||
|
||||
let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
|
||||
|
||||
let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
|
||||
OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
|
||||
stdin,
|
||||
stdout,
|
||||
move |fut| foreground_executor.spawn(fut).detach(),
|
||||
);
|
||||
|
||||
let io_task = cx.background_spawn(async move {
|
||||
io_fut.await.log_err();
|
||||
});
|
||||
|
||||
let child_status = cx.background_spawn(async move {
|
||||
let result = match child.status().await {
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
Ok(result) if result.success() => Ok(()),
|
||||
Ok(result) => Err(anyhow!(result)),
|
||||
};
|
||||
drop(io_task);
|
||||
result
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
connection,
|
||||
_child_status: child_status,
|
||||
current_thread: thread_rc,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
_cwd: &Path,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let task = self.connection.request_any(
|
||||
acp_old::InitializeParams {
|
||||
protocol_version: acp_old::ProtocolVersion::latest(),
|
||||
}
|
||||
.into_any(),
|
||||
);
|
||||
let current_thread = self.current_thread.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let result = task.await?;
|
||||
let result = acp_old::InitializeParams::response_from_any(result)?;
|
||||
|
||||
if !result.is_authenticated {
|
||||
anyhow::bail!(AuthRequired::new())
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
let thread = cx.new(|cx| {
|
||||
let session_id = acp::SessionId("acp-old-no-id".into());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
AcpThread::new(self.name, self.clone(), project, action_log, session_id)
|
||||
});
|
||||
current_thread.replace(thread.downgrade());
|
||||
thread
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::AuthenticateParams.into_any());
|
||||
cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let chunks = params
|
||||
.prompt
|
||||
.into_iter()
|
||||
.filter_map(|block| match block {
|
||||
acp::ContentBlock::Text(text) => {
|
||||
Some(acp_old::UserMessageChunk::Text { text: text.text })
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
|
||||
path: link.uri.into(),
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::SendUserMessageParams { chunks }.into_any());
|
||||
cx.foreground_executor().spawn(async move {
|
||||
task.await?;
|
||||
anyhow::Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
acp::PromptCapabilities {
|
||||
image: false,
|
||||
audio: false,
|
||||
embedded_context: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
|
||||
let task = self
|
||||
.connection
|
||||
.request_any(acp_old::CancelSendMessageParams.into_any());
|
||||
cx.foreground_executor()
|
||||
.spawn(async move {
|
||||
task.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
use acp_tools::AcpConnectionRegistry;
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use futures::AsyncBufReadExt as _;
|
||||
use futures::channel::oneshot;
|
||||
use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, cell::RefCell};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||
|
||||
use crate::{AgentServerCommand, acp::UnsupportedVersion};
|
||||
use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError};
|
||||
|
||||
pub struct AcpConnection {
|
||||
server_name: &'static str,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
_io_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
pub struct AcpSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
suppress_abort_err: bool,
|
||||
}
|
||||
|
||||
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
|
||||
|
||||
impl AcpConnection {
|
||||
pub async fn stdio(
|
||||
server_name: &'static str,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut child = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.envs(command.env.iter().flatten())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdout = child.stdout.take().context("Failed to take stdout")?;
|
||||
let stdin = child.stdin.take().context("Failed to take stdin")?;
|
||||
let stderr = child.stderr.take().context("Failed to take stderr")?;
|
||||
log::trace!("Spawned (pid: {})", child.id());
|
||||
|
||||
let sessions = Rc::new(RefCell::new(HashMap::default()));
|
||||
|
||||
let client = ClientDelegate {
|
||||
sessions: sessions.clone(),
|
||||
cx: cx.clone(),
|
||||
};
|
||||
let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
|
||||
let foreground_executor = cx.foreground_executor().clone();
|
||||
move |fut| {
|
||||
foreground_executor.spawn(fut).detach();
|
||||
}
|
||||
});
|
||||
|
||||
let io_task = cx.background_spawn(io_task);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut stderr = BufReader::new(stderr);
|
||||
let mut line = String::new();
|
||||
while let Ok(n) = stderr.read_line(&mut line).await
|
||||
&& n > 0
|
||||
{
|
||||
log::warn!("agent stderr: {}", &line);
|
||||
line.clear();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn({
|
||||
let sessions = sessions.clone();
|
||||
async move |cx| {
|
||||
let status = child.status().await?;
|
||||
|
||||
for session in sessions.borrow().values() {
|
||||
session
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.emit_load_error(LoadError::Exited { status }, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let connection = Rc::new(connection);
|
||||
|
||||
cx.update(|cx| {
|
||||
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
|
||||
registry.set_active_connection(server_name, &connection, cx)
|
||||
});
|
||||
})?;
|
||||
|
||||
let response = connection
|
||||
.initialize(acp::InitializeRequest {
|
||||
protocol_version: acp::VERSION,
|
||||
client_capabilities: acp::ClientCapabilities {
|
||||
fs: acp::FileSystemCapability {
|
||||
read_text_file: true,
|
||||
write_text_file: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
|
||||
return Err(UnsupportedVersion.into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
auth_methods: response.auth_methods,
|
||||
connection,
|
||||
server_name,
|
||||
sessions,
|
||||
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
|
||||
_io_task: io_task,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
cwd: &Path,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
cx.spawn(async move |cx| {
|
||||
let response = conn
|
||||
.new_session(acp::NewSessionRequest {
|
||||
mcp_servers: vec![],
|
||||
cwd,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
let mut error = AuthRequired::new();
|
||||
|
||||
if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
|
||||
error = error.with_description(err.message);
|
||||
}
|
||||
|
||||
anyhow!(error)
|
||||
} else {
|
||||
anyhow!(err)
|
||||
}
|
||||
})?;
|
||||
|
||||
let session_id = response.session_id;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||
let thread = cx.new(|_cx| {
|
||||
AcpThread::new(
|
||||
self.server_name,
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let session = AcpSession {
|
||||
thread: thread.downgrade(),
|
||||
suppress_abort_err: false,
|
||||
};
|
||||
sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
Ok(thread)
|
||||
})
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||
&self.auth_methods
|
||||
}
|
||||
|
||||
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
|
||||
let conn = self.connection.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = conn
|
||||
.authenticate(acp::AuthenticateRequest {
|
||||
method_id: method_id.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let session_id = params.session_id.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = conn.prompt(params).await;
|
||||
|
||||
let mut suppress_abort_err = false;
|
||||
|
||||
if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
|
||||
suppress_abort_err = session.suppress_abort_err;
|
||||
session.suppress_abort_err = false;
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) => {
|
||||
if err.code != ErrorCode::INTERNAL_ERROR.code {
|
||||
anyhow::bail!(err)
|
||||
}
|
||||
|
||||
let Some(data) = &err.data else {
|
||||
anyhow::bail!(err)
|
||||
};
|
||||
|
||||
// Temporary workaround until the following PR is generally available:
|
||||
// https://github.com/google-gemini/gemini-cli/pull/6656
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ErrorDetails {
|
||||
details: Box<str>,
|
||||
}
|
||||
|
||||
match serde_json::from_value(data.clone()) {
|
||||
Ok(ErrorDetails { details }) => {
|
||||
if suppress_abort_err && details.contains("This operation was aborted")
|
||||
{
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(details))
|
||||
}
|
||||
}
|
||||
Err(_) => Err(anyhow!(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
self.prompt_capabilities
|
||||
}
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
||||
session.suppress_abort_err = true;
|
||||
}
|
||||
let conn = self.connection.clone();
|
||||
let params = acp::CancelNotification {
|
||||
session_id: session_id.clone(),
|
||||
};
|
||||
cx.foreground_executor()
|
||||
.spawn(async move { conn.cancel(params).await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegate {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
cx: AsyncApp,
|
||||
}
|
||||
|
||||
impl acp::Client for ClientDelegate {
|
||||
async fn request_permission(
|
||||
&self,
|
||||
arguments: acp::RequestPermissionRequest,
|
||||
) -> Result<acp::RequestPermissionResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let rx = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
|
||||
})?;
|
||||
|
||||
let result = rx?.await;
|
||||
|
||||
let outcome = match result {
|
||||
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
|
||||
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
|
||||
};
|
||||
|
||||
Ok(acp::RequestPermissionResponse { outcome })
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
&self,
|
||||
arguments: acp::WriteTextFileRequest,
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.write_text_file(arguments.path, arguments.content, cx)
|
||||
})?;
|
||||
|
||||
task.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
arguments: acp::ReadTextFileRequest,
|
||||
) -> Result<acp::ReadTextFileResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
|
||||
})?;
|
||||
|
||||
let content = task.await?;
|
||||
|
||||
Ok(acp::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn session_notification(
|
||||
&self,
|
||||
notification: acp::SessionNotification,
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let sessions = self.sessions.borrow();
|
||||
let session = sessions
|
||||
.get(¬ification.session_id)
|
||||
.context("Failed to get session")?;
|
||||
|
||||
session.thread.update(cx, |thread, cx| {
|
||||
thread.handle_session_update(notification.update, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,8 @@ pub trait AgentServer: Send {
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>>;
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
|
||||
fn install_command(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
impl dyn AgentServer {
|
||||
|
||||
@@ -63,6 +63,10 @@ impl AgentServer for ClaudeCode {
|
||||
ui::IconName::AiClaude
|
||||
}
|
||||
|
||||
fn install_command(&self) -> Option<&'static str> {
|
||||
Some("npm install -g @anthropic-ai/claude-code@latest")
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
_root_dir: &Path,
|
||||
@@ -108,11 +112,7 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return Err(LoadError::NotInstalled {
|
||||
error_message: "Failed to find Claude Code binary".into(),
|
||||
install_message: "Install Claude Code".into(),
|
||||
install_command: "npm install -g @anthropic-ai/claude-code@latest".into(),
|
||||
}.into());
|
||||
return Err(LoadError::NotInstalled.into());
|
||||
};
|
||||
|
||||
let api_key =
|
||||
@@ -230,17 +230,8 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||
|| !help.contains("--session-id"))
|
||||
{
|
||||
LoadError::Unsupported {
|
||||
error_message: format!(
|
||||
"Your installed version of Claude Code ({}, version {}) does not have required features for use with Zed.",
|
||||
command.path.to_string_lossy(),
|
||||
version,
|
||||
)
|
||||
.into(),
|
||||
upgrade_message: "Upgrade Claude Code to latest".into(),
|
||||
upgrade_command: format!(
|
||||
"{} update",
|
||||
command.path.to_string_lossy()
|
||||
),
|
||||
command: command.path.to_string_lossy().to_string().into(),
|
||||
current_version: version.to_string().into(),
|
||||
}
|
||||
} else {
|
||||
LoadError::Exited { status }
|
||||
@@ -294,6 +285,7 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<acp_thread::UserMessageId>,
|
||||
params: acp::PromptRequest,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
|
||||
@@ -57,6 +57,10 @@ impl crate::AgentServer for CustomAgentServer {
|
||||
})
|
||||
}
|
||||
|
||||
fn install_command(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::AgentServer;
|
||||
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus, new_prompt_id};
|
||||
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
|
||||
use agent_client_protocol as acp;
|
||||
use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
||||
use gpui::{AppContext, Entity, TestAppContext};
|
||||
@@ -77,7 +77,6 @@ where
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
new_prompt_id(),
|
||||
vec![
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Read the file ".into(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, path::Path};
|
||||
|
||||
use crate::acp::AcpConnection;
|
||||
use crate::{AgentServer, AgentServerCommand};
|
||||
use acp_thread::{AgentConnection, LoadError};
|
||||
use anyhow::Result;
|
||||
@@ -37,6 +38,10 @@ impl AgentServer for Gemini {
|
||||
ui::IconName::AiGemini
|
||||
}
|
||||
|
||||
fn install_command(&self) -> Option<&'static str> {
|
||||
Some("npm install --engine-strict -g @google/gemini-cli@latest")
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
@@ -52,48 +57,73 @@ impl AgentServer for Gemini {
|
||||
})?;
|
||||
|
||||
let Some(mut command) =
|
||||
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await
|
||||
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx)
|
||||
.await
|
||||
else {
|
||||
return Err(LoadError::NotInstalled {
|
||||
error_message: "Failed to find Gemini CLI binary".into(),
|
||||
install_message: "Install Gemini CLI".into(),
|
||||
install_command: Self::install_command().into(),
|
||||
}.into());
|
||||
return Err(LoadError::NotInstalled.into());
|
||||
};
|
||||
|
||||
if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
|
||||
command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key);
|
||||
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
|
||||
}
|
||||
|
||||
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
|
||||
if result.is_err() {
|
||||
let version_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
match &result {
|
||||
Ok(connection) => {
|
||||
if let Some(connection) = connection.clone().downcast::<AcpConnection>()
|
||||
&& !connection.prompt_capabilities().image
|
||||
{
|
||||
let version_output = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output()
|
||||
.await;
|
||||
let current_version =
|
||||
String::from_utf8(version_output?.stdout)?.trim().to_owned();
|
||||
if !connection.prompt_capabilities().image {
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: format!(
|
||||
"{} {}",
|
||||
command.path.to_string_lossy(),
|
||||
command.args.join(" ")
|
||||
)
|
||||
.into(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
let version_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let help_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--help")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
let help_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--help")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
|
||||
let (version_output, help_output) =
|
||||
futures::future::join(version_fut, help_fut).await;
|
||||
|
||||
let current_version = String::from_utf8(version_output?.stdout)?;
|
||||
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
|
||||
let current_version = String::from_utf8(version_output?.stdout)?;
|
||||
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
|
||||
|
||||
if !supported {
|
||||
return Err(LoadError::Unsupported {
|
||||
error_message: format!(
|
||||
"Your installed version of Gemini CLI ({}, version {}) doesn't support the Agentic Coding Protocol (ACP).",
|
||||
command.path.to_string_lossy(),
|
||||
current_version
|
||||
).into(),
|
||||
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
||||
upgrade_command: Self::upgrade_command().into(),
|
||||
}.into())
|
||||
if !supported {
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: command.path.to_string_lossy().to_string().into(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
@@ -111,11 +141,11 @@ impl Gemini {
|
||||
}
|
||||
|
||||
pub fn install_command() -> &'static str {
|
||||
"npm install -g @google/gemini-cli@preview"
|
||||
"npm install --engine-strict -g @google/gemini-cli@latest"
|
||||
}
|
||||
|
||||
pub fn upgrade_command() -> &'static str {
|
||||
"npm install -g @google/gemini-cli@preview"
|
||||
"npm install -g @google/gemini-cli@latest"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use agent2::HistoryStore;
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||
use gpui::{
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
|
||||
TextStyleRefinement, WeakEntity, Window,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
@@ -67,7 +67,7 @@ impl EntryViewState {
|
||||
|
||||
match thread_entry {
|
||||
AgentThreadEntry::UserMessage(message) => {
|
||||
let has_id = message.prompt_id.is_some();
|
||||
let has_id = message.id.is_some();
|
||||
let chunks = message.chunks.clone();
|
||||
if let Some(Entry::UserMessage(editor)) = self.entries.get_mut(index) {
|
||||
if !editor.focus_handle(cx).is_focused(window) {
|
||||
@@ -154,10 +154,22 @@ impl EntryViewState {
|
||||
});
|
||||
}
|
||||
}
|
||||
AgentThreadEntry::AssistantMessage(_) => {
|
||||
if index == self.entries.len() {
|
||||
self.entries.push(Entry::empty())
|
||||
}
|
||||
AgentThreadEntry::AssistantMessage(message) => {
|
||||
let entry = if let Some(Entry::AssistantMessage(entry)) =
|
||||
self.entries.get_mut(index)
|
||||
{
|
||||
entry
|
||||
} else {
|
||||
self.set_entry(
|
||||
index,
|
||||
Entry::AssistantMessage(AssistantMessageEntry::default()),
|
||||
);
|
||||
let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
|
||||
unreachable!()
|
||||
};
|
||||
entry
|
||||
};
|
||||
entry.sync(message);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -177,7 +189,7 @@ impl EntryViewState {
|
||||
pub fn settings_changed(&mut self, cx: &mut App) {
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
Entry::UserMessage { .. } => {}
|
||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||
Entry::Content(response_views) => {
|
||||
for view in response_views.values() {
|
||||
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
|
||||
@@ -208,9 +220,29 @@ pub enum ViewEvent {
|
||||
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct AssistantMessageEntry {
|
||||
scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,
|
||||
}
|
||||
|
||||
impl AssistantMessageEntry {
|
||||
pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> {
|
||||
self.scroll_handles_by_chunk_index.get(&ix).cloned()
|
||||
}
|
||||
|
||||
pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
|
||||
if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
|
||||
let ix = message.chunks.len() - 1;
|
||||
let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
|
||||
handle.scroll_to_bottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Entry {
|
||||
UserMessage(Entity<MessageEditor>),
|
||||
AssistantMessage(AssistantMessageEntry),
|
||||
Content(HashMap<EntityId, AnyEntity>),
|
||||
}
|
||||
|
||||
@@ -218,7 +250,7 @@ impl Entry {
|
||||
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
||||
match self {
|
||||
Self::UserMessage(editor) => Some(editor),
|
||||
Entry::Content(_) => None,
|
||||
Self::AssistantMessage(_) | Self::Content(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +271,16 @@ impl Entry {
|
||||
.map(|entity| entity.downcast::<TerminalView>().unwrap())
|
||||
}
|
||||
|
||||
pub fn scroll_handle_for_assistant_message_chunk(
|
||||
&self,
|
||||
chunk_ix: usize,
|
||||
) -> Option<ScrollHandle> {
|
||||
match self {
|
||||
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
|
||||
Self::UserMessage(_) | Self::Content(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
|
||||
match self {
|
||||
Self::Content(map) => Some(map),
|
||||
@@ -254,7 +296,7 @@ impl Entry {
|
||||
pub fn has_content(&self) -> bool {
|
||||
match self {
|
||||
Self::Content(map) => !map.is_empty(),
|
||||
Self::UserMessage(_) => false,
|
||||
Self::UserMessage(_) | Self::AssistantMessage(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,20 +3,23 @@ mod configure_context_server_modal;
|
||||
mod manage_profiles_modal;
|
||||
mod tool_picker;
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
|
||||
use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
|
||||
use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use cloud_llm_client::Plan;
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
|
||||
use extension::ExtensionManifest;
|
||||
use extension_host::ExtensionStore;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||
Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
|
||||
WeakEntity, percentage,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
@@ -34,7 +37,7 @@ use ui::{
|
||||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
use workspace::{Workspace, create_and_open_local_file};
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
|
||||
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||
@@ -1058,10 +1061,39 @@ impl AgentConfiguration {
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("External Agents"))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Headline::new("External Agents"))
|
||||
.child(
|
||||
Button::new("add-agent", "Add Agent")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(
|
||||
move |_, window, cx| {
|
||||
if let Some(workspace) = window.root().flatten() {
|
||||
let workspace = workspace.downgrade();
|
||||
window
|
||||
.spawn(cx, async |cx| {
|
||||
open_new_agent_servers_entry_in_settings_editor(
|
||||
workspace,
|
||||
cx,
|
||||
).await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
|
||||
"Bring the agent of your choice to Zed via our new Agent Client Protocol.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
@@ -1324,3 +1356,109 @@ fn show_unable_to_uninstall_extension_with_context_server(
|
||||
|
||||
workspace.toggle_status_toast(status_toast, cx);
|
||||
}
|
||||
|
||||
async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let settings_editor = workspace
|
||||
.update_in(cx, |_, window, cx| {
|
||||
create_and_open_local_file(paths::settings_file(), window, cx, || {
|
||||
settings::initial_user_settings_content().as_ref().into()
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
settings_editor
|
||||
.downgrade()
|
||||
.update_in(cx, |item, window, cx| {
|
||||
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
let settings = cx.global::<SettingsStore>();
|
||||
|
||||
let mut unique_server_name = None;
|
||||
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
|
||||
let server_name: Option<SharedString> = (0..u8::MAX)
|
||||
.map(|i| {
|
||||
if i == 0 {
|
||||
"your_agent".into()
|
||||
} else {
|
||||
format!("your_agent_{}", i).into()
|
||||
}
|
||||
})
|
||||
.find(|name| !file.custom.contains_key(name));
|
||||
if let Some(server_name) = server_name {
|
||||
unique_server_name = Some(server_name.clone());
|
||||
file.custom.insert(
|
||||
server_name,
|
||||
AgentServerSettings {
|
||||
command: AgentServerCommand {
|
||||
path: "path_to_executable".into(),
|
||||
args: vec![],
|
||||
env: Some(HashMap::default()),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if edits.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ranges = edits
|
||||
.iter()
|
||||
.map(|(range, _)| range.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
item.edit(edits, cx);
|
||||
if let Some((unique_server_name, buffer)) =
|
||||
unique_server_name.zip(item.buffer().read(cx).as_singleton())
|
||||
{
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
if let Some(range) =
|
||||
find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
|
||||
{
|
||||
item.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::newest()),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
selections.select_ranges(vec![range]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn find_text_in_buffer(
|
||||
text: &str,
|
||||
start: usize,
|
||||
snapshot: &language::BufferSnapshot,
|
||||
) -> Option<Range<usize>> {
|
||||
let chars = text.chars().collect::<Vec<char>>();
|
||||
|
||||
let mut offset = start;
|
||||
let mut char_offset = 0;
|
||||
for c in snapshot.chars_at(start) {
|
||||
if char_offset >= chars.len() {
|
||||
break;
|
||||
}
|
||||
offset += 1;
|
||||
|
||||
if c == chars[char_offset] {
|
||||
char_offset += 1;
|
||||
} else {
|
||||
char_offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if char_offset == chars.len() {
|
||||
Some(offset.saturating_sub(chars.len())..offset)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use zed_actions::agent::ReauthenticateAgent;
|
||||
|
||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::ui::AcpOnboardingModal;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||
@@ -28,7 +29,6 @@ use crate::{
|
||||
slash_command::SlashCommandCompletionProvider,
|
||||
text_thread_editor::{
|
||||
AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate,
|
||||
render_remaining_tokens,
|
||||
},
|
||||
thread_history::{HistoryEntryElement, ThreadHistory},
|
||||
ui::{AgentOnboardingModal, EndTrialUpsell},
|
||||
@@ -77,7 +77,10 @@ use workspace::{
|
||||
};
|
||||
use zed_actions::{
|
||||
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
||||
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
|
||||
agent::{
|
||||
OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
|
||||
ToggleModelSelector,
|
||||
},
|
||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||
};
|
||||
|
||||
@@ -201,6 +204,9 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
|
||||
AgentOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
|
||||
AcpOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
||||
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
||||
window.refresh();
|
||||
@@ -591,17 +597,6 @@ impl AgentPanel {
|
||||
None
|
||||
};
|
||||
|
||||
// Wait for the Gemini/Native feature flag to be available.
|
||||
let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
|
||||
if !client.status().borrow().is_signed_out() {
|
||||
cx.update(|_, cx| {
|
||||
cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
}
|
||||
|
||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||
let panel = cx.new(|cx| {
|
||||
Self::new(
|
||||
@@ -622,6 +617,10 @@ impl AgentPanel {
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(AgentType::NativeAgent, window, cx);
|
||||
});
|
||||
}
|
||||
panel
|
||||
})?;
|
||||
@@ -1852,19 +1851,6 @@ impl AgentPanel {
|
||||
menu
|
||||
}
|
||||
|
||||
pub fn set_selected_agent(
|
||||
&mut self,
|
||||
agent: AgentType,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_agent != agent {
|
||||
self.selected_agent = agent.clone();
|
||||
self.serialize(cx);
|
||||
}
|
||||
self.new_agent_thread(agent, window, cx);
|
||||
}
|
||||
|
||||
pub fn selected_agent(&self) -> AgentType {
|
||||
self.selected_agent.clone()
|
||||
}
|
||||
@@ -1875,6 +1861,11 @@ impl AgentPanel {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_agent != agent {
|
||||
self.selected_agent = agent.clone();
|
||||
self.serialize(cx);
|
||||
}
|
||||
|
||||
match agent {
|
||||
AgentType::Zed => {
|
||||
window.dispatch_action(
|
||||
@@ -2555,7 +2546,7 @@ impl AgentPanel {
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
panel.new_agent_thread(
|
||||
AgentType::NativeAgent,
|
||||
window,
|
||||
cx,
|
||||
@@ -2581,7 +2572,7 @@ impl AgentPanel {
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
panel.new_agent_thread(
|
||||
AgentType::TextThread,
|
||||
window,
|
||||
cx,
|
||||
@@ -2609,7 +2600,7 @@ impl AgentPanel {
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
panel.new_agent_thread(
|
||||
AgentType::Gemini,
|
||||
window,
|
||||
cx,
|
||||
@@ -2636,7 +2627,7 @@ impl AgentPanel {
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
panel.new_agent_thread(
|
||||
AgentType::ClaudeCode,
|
||||
window,
|
||||
cx,
|
||||
@@ -2669,7 +2660,7 @@ impl AgentPanel {
|
||||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.set_selected_agent(
|
||||
panel.new_agent_thread(
|
||||
AgentType::Custom {
|
||||
name: agent_name
|
||||
.clone(),
|
||||
@@ -2693,9 +2684,9 @@ impl AgentPanel {
|
||||
})
|
||||
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
|
||||
menu.separator().link(
|
||||
"Add Your Own Agent",
|
||||
"Add Other Agents",
|
||||
OpenBrowser {
|
||||
url: "https://agentclientprotocol.com/".into(),
|
||||
url: zed_urls::external_agents_docs(cx),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
@@ -2883,12 +2874,8 @@ impl AgentPanel {
|
||||
|
||||
Some(token_count)
|
||||
}
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
let element = render_remaining_tokens(context_editor, cx)?;
|
||||
|
||||
Some(element.into_any_element())
|
||||
}
|
||||
ActiveView::ExternalAgentThread { .. }
|
||||
| ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => None,
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
EditorEvent::Edited { .. } => {
|
||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let is_via_ssh = workspace.project().read(cx).is_via_ssh();
|
||||
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
|
||||
|
||||
workspace
|
||||
.client()
|
||||
|
||||
@@ -6,7 +6,8 @@ use feature_flags::ZedProFeatureFlag;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||
LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
@@ -76,6 +77,7 @@ pub struct LanguageModelPickerDelegate {
|
||||
all_models: Arc<GroupedModels>,
|
||||
filtered_entries: Vec<LanguageModelPickerEntry>,
|
||||
selected_index: usize,
|
||||
_authenticate_all_providers_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -96,6 +98,7 @@ impl LanguageModelPickerDelegate {
|
||||
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
|
||||
filtered_entries: entries,
|
||||
get_active_model: Arc::new(get_active_model),
|
||||
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
|
||||
_subscriptions: vec![cx.subscribe_in(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
window,
|
||||
@@ -139,6 +142,56 @@ impl LanguageModelPickerDelegate {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Authenticates all providers in the [`LanguageModelRegistry`].
|
||||
///
|
||||
/// We do this so that we can populate the language selector with all of the
|
||||
/// models from the configured providers.
|
||||
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
|
||||
if let Err(err) = authenticate_task.await {
|
||||
if matches!(err, AuthenticateError::CredentialsNotFound) {
|
||||
// Since we're authenticating these providers in the
|
||||
// background for the purposes of populating the
|
||||
// language selector, we don't care about providers
|
||||
// where the credentials are not found.
|
||||
} else {
|
||||
// Some providers have noisy failure states that we
|
||||
// don't want to spam the logs with every time the
|
||||
// language model selector is initialized.
|
||||
//
|
||||
// Ideally these should have more clear failure modes
|
||||
// that we know are safe to ignore here, like what we do
|
||||
// with `CredentialsNotFound` above.
|
||||
match provider_id.0.as_ref() {
|
||||
"lmstudio" | "ollama" => {
|
||||
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
||||
//
|
||||
// These fail noisily, so we don't log them.
|
||||
}
|
||||
"copilot_chat" => {
|
||||
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"Failed to authenticate provider: {}: {err}",
|
||||
provider_name.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
(self.get_active_model)(cx)
|
||||
}
|
||||
|
||||
@@ -1857,6 +1857,53 @@ impl TextThreadEditor {
|
||||
.update(cx, |context, cx| context.summarize(true, cx));
|
||||
}
|
||||
|
||||
fn render_remaining_tokens(&self, cx: &App) -> Option<impl IntoElement + use<>> {
|
||||
let (token_count_color, token_count, max_token_count, tooltip) =
|
||||
match token_state(&self.context, cx)? {
|
||||
TokenState::NoTokensLeft {
|
||||
max_token_count,
|
||||
token_count,
|
||||
} => (
|
||||
Color::Error,
|
||||
token_count,
|
||||
max_token_count,
|
||||
Some("Token Limit Reached"),
|
||||
),
|
||||
TokenState::HasMoreTokens {
|
||||
max_token_count,
|
||||
token_count,
|
||||
over_warn_threshold,
|
||||
} => {
|
||||
let (color, tooltip) = if over_warn_threshold {
|
||||
(Color::Warning, Some("Token Limit is Close to Exhaustion"))
|
||||
} else {
|
||||
(Color::Muted, None)
|
||||
};
|
||||
(color, token_count, max_token_count, tooltip)
|
||||
}
|
||||
};
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.id("token-count")
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(humanize_token_count(token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(token_count_color),
|
||||
)
|
||||
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(humanize_token_count(max_token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.when_some(tooltip, |element, tooltip| {
|
||||
element.tooltip(Tooltip::text(tooltip))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
@@ -2420,9 +2467,14 @@ impl Render for TextThreadEditor {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_language_model_selector(window, cx))
|
||||
.child(self.render_send_button(window, cx)),
|
||||
.gap_2p5()
|
||||
.children(self.render_remaining_tokens(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_language_model_selector(window, cx))
|
||||
.child(self.render_send_button(window, cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -2710,58 +2762,6 @@ impl FollowableItem for TextThreadEditor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_remaining_tokens(
|
||||
context_editor: &Entity<TextThreadEditor>,
|
||||
cx: &App,
|
||||
) -> Option<impl IntoElement + use<>> {
|
||||
let context = &context_editor.read(cx).context;
|
||||
|
||||
let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)?
|
||||
{
|
||||
TokenState::NoTokensLeft {
|
||||
max_token_count,
|
||||
token_count,
|
||||
} => (
|
||||
Color::Error,
|
||||
token_count,
|
||||
max_token_count,
|
||||
Some("Token Limit Reached"),
|
||||
),
|
||||
TokenState::HasMoreTokens {
|
||||
max_token_count,
|
||||
token_count,
|
||||
over_warn_threshold,
|
||||
} => {
|
||||
let (color, tooltip) = if over_warn_threshold {
|
||||
(Color::Warning, Some("Token Limit is Close to Exhaustion"))
|
||||
} else {
|
||||
(Color::Muted, None)
|
||||
};
|
||||
(color, token_count, max_token_count, tooltip)
|
||||
}
|
||||
};
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
.id("token-count")
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(humanize_token_count(token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(token_count_color),
|
||||
)
|
||||
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(humanize_token_count(max_token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.when_some(tooltip, |element, tooltip| {
|
||||
element.tooltip(Tooltip::text(tooltip))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
enum PendingSlashCommand {}
|
||||
|
||||
fn invoked_slash_command_fold_placeholder(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod acp_onboarding_modal;
|
||||
mod agent_notification;
|
||||
mod burn_mode_tooltip;
|
||||
mod context_pill;
|
||||
@@ -6,6 +7,7 @@ mod onboarding_modal;
|
||||
pub mod preview;
|
||||
mod unavailable_editing_tooltip;
|
||||
|
||||
pub use acp_onboarding_modal::*;
|
||||
pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use context_pill::*;
|
||||
|
||||
254
crates/agent_ui/src/ui/acp_onboarding_modal.rs
Normal file
254
crates/agent_ui/src/ui/acp_onboarding_modal.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use client::zed_urls;
|
||||
use gpui::{
|
||||
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
|
||||
linear_color_stop, linear_gradient,
|
||||
};
|
||||
use ui::{TintColor, Vector, VectorName, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::agent_panel::{AgentPanel, AgentType};
|
||||
|
||||
macro_rules! acp_onboarding_event {
|
||||
($name:expr) => {
|
||||
telemetry::event!($name, source = "ACP Onboarding");
|
||||
};
|
||||
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
|
||||
telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+);
|
||||
};
|
||||
}
|
||||
|
||||
pub struct AcpOnboardingModal {
|
||||
focus_handle: FocusHandle,
|
||||
workspace: Entity<Workspace>,
|
||||
}
|
||||
|
||||
impl AcpOnboardingModal {
|
||||
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
let workspace_entity = cx.entity();
|
||||
workspace.toggle_modal(window, cx, |_window, cx| Self {
|
||||
workspace: workspace_entity,
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
}
|
||||
|
||||
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.workspace.update(cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(AgentType::Gemini, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
acp_onboarding_event!("Open Panel Clicked");
|
||||
}
|
||||
|
||||
fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url(&zed_urls::external_agents_docs(cx));
|
||||
cx.notify();
|
||||
|
||||
acp_onboarding_event!("Documentation Link Clicked");
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for AcpOnboardingModal {}
|
||||
|
||||
impl Focusable for AcpOnboardingModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for AcpOnboardingModal {}
|
||||
|
||||
impl Render for AcpOnboardingModal {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let illustration_element = |label: bool, opacity: f32| {
|
||||
h_flex()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().element_active.opacity(0.05))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_dashed()
|
||||
.child(
|
||||
Icon::new(IconName::Stop)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
|
||||
)
|
||||
.map(|this| {
|
||||
if label {
|
||||
this.child(
|
||||
Label::new("Your Agent Here")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
div().w_16().h_1().rounded_full().bg(cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_active
|
||||
.opacity(0.6)),
|
||||
)
|
||||
}
|
||||
})
|
||||
.opacity(opacity)
|
||||
};
|
||||
|
||||
let illustration = h_flex()
|
||||
.relative()
|
||||
.h(rems_from_px(126.))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_center()
|
||||
.gap_8()
|
||||
.rounded_t_md()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
|
||||
Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
|
||||
),
|
||||
)
|
||||
.child(div().absolute().inset_0().size_full().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.1),
|
||||
0.9,
|
||||
),
|
||||
linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.),
|
||||
0.,
|
||||
),
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
.bg(gpui::black().opacity(0.15)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
)
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AcpLogoSerif,
|
||||
rems_from_px(111.),
|
||||
rems_from_px(41.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(illustration_element(false, 0.15))
|
||||
.child(illustration_element(true, 0.3))
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().element_active.opacity(0.2))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Icon::new(IconName::AiGemini)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)),
|
||||
)
|
||||
.child(illustration_element(true, 0.3))
|
||||
.child(illustration_element(false, 0.15)),
|
||||
);
|
||||
|
||||
let heading = v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Now Available")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large));
|
||||
|
||||
let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
|
||||
|
||||
let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
|
||||
.icon_size(IconSize::Indicator)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::open_panel));
|
||||
|
||||
let docs_button = Button::new("add-other-agents", "Add Other Agents")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::view_docs));
|
||||
|
||||
let close_button = h_flex().absolute().top_2().right_2().child(
|
||||
IconButton::new("cancel", IconName::Close).on_click(cx.listener(
|
||||
|_, _: &ClickEvent, _window, cx| {
|
||||
acp_onboarding_event!("Canceled", trigger = "X click");
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.id("acp-onboarding")
|
||||
.key_context("AcpOnboardingModal")
|
||||
.relative()
|
||||
.w(rems(34.))
|
||||
.h_full()
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.overflow_hidden()
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
acp_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(illustration)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.child(heading)
|
||||
.child(Label::new(copy).color(Color::Muted))
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.child(open_panel_button)
|
||||
.child(docs_button),
|
||||
),
|
||||
)
|
||||
.child(close_button)
|
||||
}
|
||||
}
|
||||
@@ -1161,7 +1161,7 @@ impl Room {
|
||||
let request = self.client.request(proto::ShareProject {
|
||||
room_id: self.id(),
|
||||
worktrees: project.read(cx).worktree_metadata_protos(cx),
|
||||
is_ssh_project: project.read(cx).is_via_ssh(),
|
||||
is_ssh_project: project.read(cx).is_via_remote_server(),
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
|
||||
@@ -43,3 +43,11 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
|
||||
server_url = server_url(cx)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the URL to Zed AI's external agents documentation.
|
||||
pub fn external_agents_docs(cx: &App) -> String {
|
||||
format!(
|
||||
"{server_url}/docs/ai/external-agents",
|
||||
server_url = server_url(cx)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ use project::{
|
||||
debugger::session::ThreadId,
|
||||
lsp_store::{FormatTrigger, LspFormatTarget},
|
||||
};
|
||||
use remote::SshRemoteClient;
|
||||
use remote::RemoteClient;
|
||||
use remote_server::{HeadlessAppState, HeadlessProject};
|
||||
use rpc::proto;
|
||||
use serde_json::json;
|
||||
@@ -59,7 +59,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
.await;
|
||||
|
||||
// Set up project on remote FS
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
@@ -101,7 +101,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
|
||||
.await;
|
||||
@@ -235,7 +235,7 @@ async fn test_ssh_collaboration_git_branches(
|
||||
.await;
|
||||
|
||||
// Set up project on remote FS
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree("/project", serde_json::json!({ ".git":{} }))
|
||||
@@ -268,7 +268,7 @@ async fn test_ssh_collaboration_git_branches(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project("/project", client_ssh, cx_a)
|
||||
.await;
|
||||
@@ -420,7 +420,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
let buffer_text = "let one = \"two\"";
|
||||
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
|
||||
@@ -473,7 +473,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/project"), client_ssh, cx_a)
|
||||
.await;
|
||||
@@ -602,7 +602,7 @@ async fn test_remote_server_debugger(
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
dap_adapters::init(cx);
|
||||
});
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
@@ -633,7 +633,7 @@ async fn test_remote_server_debugger(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let mut server = TestServer::start(server_cx.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
cx_a.update(|cx| {
|
||||
@@ -711,7 +711,7 @@ async fn test_slow_adapter_startup_retries(
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
dap_adapters::init(cx);
|
||||
});
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
@@ -742,7 +742,7 @@ async fn test_slow_adapter_startup_retries(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let mut server = TestServer::start(server_cx.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
cx_a.update(|cx| {
|
||||
|
||||
@@ -26,7 +26,7 @@ use node_runtime::NodeRuntime;
|
||||
use notifications::NotificationStore;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use remote::SshRemoteClient;
|
||||
use remote::RemoteClient;
|
||||
use rpc::{
|
||||
RECEIVE_TIMEOUT,
|
||||
proto::{self, ChannelRole},
|
||||
@@ -765,11 +765,11 @@ impl TestClient {
|
||||
pub async fn build_ssh_project(
|
||||
&self,
|
||||
root_path: impl AsRef<Path>,
|
||||
ssh: Entity<SshRemoteClient>,
|
||||
ssh: Entity<RemoteClient>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Project>, WorktreeId) {
|
||||
let project = cx.update(|cx| {
|
||||
Project::ssh(
|
||||
Project::remote(
|
||||
ssh,
|
||||
self.client().clone(),
|
||||
self.app_state.node_runtime.clone(),
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use db::{
|
||||
define_connection, query,
|
||||
sqlez::{bindable::Column, statement::Statement},
|
||||
query,
|
||||
sqlez::{
|
||||
bindable::Column, domain::Domain, statement::Statement,
|
||||
thread_safe_connection::ThreadSafeConnection,
|
||||
},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -50,8 +53,11 @@ impl Column for SerializedCommandInvocation {
|
||||
}
|
||||
}
|
||||
|
||||
define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
|
||||
&[sql!(
|
||||
pub struct CommandPaletteDB(ThreadSafeConnection);
|
||||
|
||||
impl Domain for CommandPaletteDB {
|
||||
const NAME: &str = stringify!(CommandPaletteDB);
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE IF NOT EXISTS command_invocations(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
command_name TEXT NOT NULL,
|
||||
@@ -59,7 +65,9 @@ define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()>
|
||||
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
);
|
||||
}
|
||||
|
||||
db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
|
||||
|
||||
impl CommandPaletteDB {
|
||||
pub async fn write_command_invocation(
|
||||
|
||||
@@ -110,11 +110,14 @@ pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection {
|
||||
}
|
||||
|
||||
/// Implements a basic DB wrapper for a given domain
|
||||
///
|
||||
/// Arguments:
|
||||
/// - static variable name for connection
|
||||
/// - type of connection wrapper
|
||||
/// - dependencies, whose migrations should be run prior to this domain's migrations
|
||||
#[macro_export]
|
||||
macro_rules! define_connection {
|
||||
(pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
macro_rules! static_connection {
|
||||
($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => {
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
|
||||
@@ -123,16 +126,6 @@ macro_rules! define_connection {
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
impl $t {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn open_test_db(name: &'static str) -> Self {
|
||||
@@ -142,7 +135,8 @@ macro_rules! define_connection {
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
$t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id))))
|
||||
#[allow(unused_parens)]
|
||||
$t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id))))
|
||||
});
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
@@ -153,46 +147,10 @@ macro_rules! define_connection {
|
||||
} else {
|
||||
$crate::RELEASE_CHANNEL.dev_name()
|
||||
};
|
||||
$t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope)))
|
||||
#[allow(unused_parens)]
|
||||
$t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope)))
|
||||
});
|
||||
};
|
||||
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
$t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id))))
|
||||
});
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
let db_dir = $crate::database_dir();
|
||||
let scope = if false $(|| stringify!($global) == "global")? {
|
||||
"global"
|
||||
} else {
|
||||
$crate::RELEASE_CHANNEL.dev_name()
|
||||
};
|
||||
$t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope)))
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
|
||||
@@ -219,17 +177,12 @@ mod tests {
|
||||
enum BadDB {}
|
||||
|
||||
impl Domain for BadDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[
|
||||
sql!(CREATE TABLE test(value);),
|
||||
// failure because test already exists
|
||||
sql!(CREATE TABLE test(value);),
|
||||
]
|
||||
}
|
||||
const NAME: &str = "db_tests";
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql!(CREATE TABLE test(value);),
|
||||
// failure because test already exists
|
||||
sql!(CREATE TABLE test(value);),
|
||||
];
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
@@ -251,25 +204,15 @@ mod tests {
|
||||
enum CorruptedDB {}
|
||||
|
||||
impl Domain for CorruptedDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
const NAME: &str = "db_tests";
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
||||
}
|
||||
|
||||
enum GoodDB {}
|
||||
|
||||
impl Domain for GoodDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
const NAME: &str = "db_tests"; //Notice same name
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)];
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
@@ -305,25 +248,16 @@ mod tests {
|
||||
enum CorruptedDB {}
|
||||
|
||||
impl Domain for CorruptedDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
const NAME: &str = "db_tests";
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
||||
}
|
||||
|
||||
enum GoodDB {}
|
||||
|
||||
impl Domain for GoodDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
const NAME: &str = "db_tests"; //Notice same name
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
|
||||
@@ -2,16 +2,26 @@ use gpui::App;
|
||||
use sqlez_macros::sql;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{define_connection, query, write_and_log};
|
||||
use crate::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
write_and_log,
|
||||
};
|
||||
|
||||
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
|
||||
&[sql!(
|
||||
pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl Domain for KeyValueStore {
|
||||
const NAME: &str = stringify!(KeyValueStore);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE IF NOT EXISTS kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
);
|
||||
}
|
||||
|
||||
crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
|
||||
|
||||
pub trait Dismissable {
|
||||
const KEY: &'static str;
|
||||
@@ -91,15 +101,19 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> =
|
||||
&[sql!(
|
||||
pub struct GlobalKeyValueStore(ThreadSafeConnection);
|
||||
|
||||
impl Domain for GlobalKeyValueStore {
|
||||
const NAME: &str = stringify!(GlobalKeyValueStore);
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE IF NOT EXISTS kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
global
|
||||
);
|
||||
}
|
||||
|
||||
crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global);
|
||||
|
||||
impl GlobalKeyValueStore {
|
||||
query! {
|
||||
|
||||
@@ -916,10 +916,11 @@ impl RunningState {
|
||||
let task_store = project.read(cx).task_store().downgrade();
|
||||
let weak_project = project.downgrade();
|
||||
let weak_workspace = workspace.downgrade();
|
||||
let ssh_info = project
|
||||
let remote_shell = project
|
||||
.read(cx)
|
||||
.ssh_client()
|
||||
.and_then(|it| it.read(cx).ssh_info());
|
||||
.remote_client()
|
||||
.as_ref()
|
||||
.and_then(|remote| remote.read(cx).shell());
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let DebugScenario {
|
||||
@@ -1003,7 +1004,7 @@ impl RunningState {
|
||||
None
|
||||
};
|
||||
|
||||
let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell);
|
||||
let builder = ShellBuilder::new(remote_shell.as_deref(), &task.resolved.shell);
|
||||
let command_label = builder.command_label(&task.resolved.command_label);
|
||||
let (command, args) =
|
||||
builder.build(task.resolved.command.clone(), &task.resolved.args);
|
||||
|
||||
@@ -19,6 +19,10 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
|
||||
});
|
||||
|
||||
static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
|
||||
});
|
||||
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||
|
||||
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
||||
@@ -216,6 +220,7 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
|
||||
let keymap = match os {
|
||||
"macos" => &KEYMAP_MACOS,
|
||||
"linux" | "freebsd" => &KEYMAP_LINUX,
|
||||
"windows" => &KEYMAP_WINDOWS,
|
||||
_ => unreachable!("Not a valid OS: {}", os),
|
||||
};
|
||||
|
||||
|
||||
@@ -2588,7 +2588,7 @@ impl Editor {
|
||||
|| binding
|
||||
.keystrokes()
|
||||
.first()
|
||||
.is_some_and(|keystroke| keystroke.modifiers.modified())
|
||||
.is_some_and(|keystroke| keystroke.display_modifiers.modified())
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -7686,16 +7686,16 @@ impl Editor {
|
||||
.keystroke()
|
||||
{
|
||||
modifiers_held = modifiers_held
|
||||
|| (&accept_keystroke.modifiers == modifiers
|
||||
&& accept_keystroke.modifiers.modified());
|
||||
|| (&accept_keystroke.display_modifiers == modifiers
|
||||
&& accept_keystroke.display_modifiers.modified());
|
||||
};
|
||||
if let Some(accept_partial_keystroke) = self
|
||||
.accept_edit_prediction_keybind(true, window, cx)
|
||||
.keystroke()
|
||||
{
|
||||
modifiers_held = modifiers_held
|
||||
|| (&accept_partial_keystroke.modifiers == modifiers
|
||||
&& accept_partial_keystroke.modifiers.modified());
|
||||
|| (&accept_partial_keystroke.display_modifiers == modifiers
|
||||
&& accept_partial_keystroke.display_modifiers.modified());
|
||||
}
|
||||
|
||||
if modifiers_held {
|
||||
@@ -9044,7 +9044,7 @@ impl Editor {
|
||||
|
||||
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
|
||||
|
||||
let modifiers_color = if accept_keystroke.modifiers == window.modifiers() {
|
||||
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
@@ -9056,19 +9056,19 @@ impl Editor {
|
||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_size(TextSize::XSmall.rems(cx))
|
||||
.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.modifiers,
|
||||
&accept_keystroke.display_modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(modifiers_color),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
true,
|
||||
)))
|
||||
.when(is_platform_style_mac, |parent| {
|
||||
parent.child(accept_keystroke.key.clone())
|
||||
parent.child(accept_keystroke.display_key.clone())
|
||||
})
|
||||
.when(!is_platform_style_mac, |parent| {
|
||||
parent.child(
|
||||
Key::new(
|
||||
util::capitalize(&accept_keystroke.key),
|
||||
util::capitalize(&accept_keystroke.display_key),
|
||||
Some(Color::Default),
|
||||
)
|
||||
.size(Some(IconSize::XSmall.rems().into())),
|
||||
@@ -9171,7 +9171,7 @@ impl Editor {
|
||||
max_width: Pixels,
|
||||
cursor_point: Point,
|
||||
style: &EditorStyle,
|
||||
accept_keystroke: Option<&gpui::Keystroke>,
|
||||
accept_keystroke: Option<&gpui::KeybindingKeystroke>,
|
||||
_window: &Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
@@ -9249,7 +9249,7 @@ impl Editor {
|
||||
accept_keystroke.as_ref(),
|
||||
|el, accept_keystroke| {
|
||||
el.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.modifiers,
|
||||
&accept_keystroke.display_modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(Color::Default),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
@@ -9319,7 +9319,7 @@ impl Editor {
|
||||
.child(completion),
|
||||
)
|
||||
.when_some(accept_keystroke, |el, accept_keystroke| {
|
||||
if !accept_keystroke.modifiers.modified() {
|
||||
if !accept_keystroke.display_modifiers.modified() {
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -9338,7 +9338,7 @@ impl Editor {
|
||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.when(is_platform_style_mac, |parent| parent.gap_1())
|
||||
.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.modifiers,
|
||||
&accept_keystroke.display_modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(if !has_completion {
|
||||
Color::Muted
|
||||
@@ -20074,7 +20074,7 @@ impl Editor {
|
||||
let (telemetry, is_via_ssh) = {
|
||||
let project = project.read(cx);
|
||||
let telemetry = project.client().telemetry().clone();
|
||||
let is_via_ssh = project.is_via_ssh();
|
||||
let is_via_ssh = project.is_via_remote_server();
|
||||
(telemetry, is_via_ssh)
|
||||
};
|
||||
refresh_linked_ranges(self, window, cx);
|
||||
@@ -20642,7 +20642,7 @@ impl Editor {
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
edit_predictions_provider,
|
||||
is_via_ssh = project.is_via_ssh(),
|
||||
is_via_ssh = project.is_via_remote_server(),
|
||||
);
|
||||
} else {
|
||||
telemetry::event!(
|
||||
@@ -20652,7 +20652,7 @@ impl Editor {
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
edit_predictions_provider,
|
||||
is_via_ssh = project.is_via_ssh(),
|
||||
is_via_ssh = project.is_via_remote_server(),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ use gpui::{
|
||||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
||||
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
||||
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||
KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
||||
ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
|
||||
Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
||||
transparent_black,
|
||||
};
|
||||
@@ -7150,7 +7150,7 @@ fn header_jump_data(
|
||||
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
|
||||
|
||||
impl AcceptEditPredictionBinding {
|
||||
pub fn keystroke(&self) -> Option<&Keystroke> {
|
||||
pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
|
||||
if let Some(binding) = self.0.as_ref() {
|
||||
match &binding.keystrokes() {
|
||||
[keystroke, ..] => Some(keystroke),
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use anyhow::Result;
|
||||
use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
|
||||
use db::sqlez::statement::Statement;
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
domain::Domain,
|
||||
statement::Statement,
|
||||
},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use fs::MTime;
|
||||
use itertools::Itertools as _;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{define_connection, query};
|
||||
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
@@ -83,7 +87,11 @@ impl Column for SerializedEditor {
|
||||
}
|
||||
}
|
||||
|
||||
define_connection!(
|
||||
pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl Domain for EditorDb {
|
||||
const NAME: &str = stringify!(EditorDb);
|
||||
|
||||
// Current schema shape using pseudo-rust syntax:
|
||||
// editors(
|
||||
// item_id: usize,
|
||||
@@ -113,7 +121,8 @@ define_connection!(
|
||||
// start: usize,
|
||||
// end: usize,
|
||||
// )
|
||||
pub static ref DB: EditorDb<WorkspaceDb> = &[
|
||||
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql! (
|
||||
CREATE TABLE editors(
|
||||
item_id INTEGER NOT NULL,
|
||||
@@ -189,7 +198,9 @@ define_connection!(
|
||||
) STRICT;
|
||||
),
|
||||
];
|
||||
);
|
||||
}
|
||||
|
||||
db::static_connection!(DB, EditorDb, [WorkspaceDb]);
|
||||
|
||||
// https://www.sqlite.org/limits.html
|
||||
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
|
||||
|
||||
@@ -43,7 +43,7 @@ use language::{
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::SshRemoteClient;
|
||||
use remote::RemoteClient;
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -117,7 +117,7 @@ pub struct ExtensionStore {
|
||||
pub wasm_host: Arc<WasmHost>,
|
||||
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
|
||||
pub tasks: Vec<Task<()>>,
|
||||
pub ssh_clients: HashMap<String, WeakEntity<SshRemoteClient>>,
|
||||
pub remote_clients: HashMap<String, WeakEntity<RemoteClient>>,
|
||||
pub ssh_registered_tx: UnboundedSender<()>,
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ impl ExtensionStore {
|
||||
reload_tx,
|
||||
tasks: Vec::new(),
|
||||
|
||||
ssh_clients: HashMap::default(),
|
||||
remote_clients: HashMap::default(),
|
||||
ssh_registered_tx: connection_registered_tx,
|
||||
};
|
||||
|
||||
@@ -1693,7 +1693,7 @@ impl ExtensionStore {
|
||||
|
||||
async fn sync_extensions_over_ssh(
|
||||
this: &WeakEntity<Self>,
|
||||
client: WeakEntity<SshRemoteClient>,
|
||||
client: WeakEntity<RemoteClient>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let extensions = this.update(cx, |this, _cx| {
|
||||
@@ -1765,8 +1765,8 @@ impl ExtensionStore {
|
||||
|
||||
pub async fn update_ssh_clients(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
|
||||
let clients = this.update(cx, |this, _cx| {
|
||||
this.ssh_clients.retain(|_k, v| v.upgrade().is_some());
|
||||
this.ssh_clients.values().cloned().collect::<Vec<_>>()
|
||||
this.remote_clients.retain(|_k, v| v.upgrade().is_some());
|
||||
this.remote_clients.values().cloned().collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
for client in clients {
|
||||
@@ -1778,17 +1778,17 @@ impl ExtensionStore {
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) {
|
||||
pub fn register_remote_client(&mut self, client: Entity<RemoteClient>, cx: &mut Context<Self>) {
|
||||
let connection_options = client.read(cx).connection_options();
|
||||
let ssh_url = connection_options.ssh_url();
|
||||
|
||||
if let Some(existing_client) = self.ssh_clients.get(&ssh_url)
|
||||
if let Some(existing_client) = self.remote_clients.get(&ssh_url)
|
||||
&& existing_client.upgrade().is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.ssh_clients.insert(ssh_url, client.downgrade());
|
||||
self.remote_clients.insert(ssh_url, client.downgrade());
|
||||
self.ssh_registered_tx.unbounded_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag {
|
||||
// integration too, and we'd like to turn Gemini/Native on in new builds
|
||||
// without enabling Claude Code in old builds.
|
||||
const NAME: &'static str = "gemini-and-native";
|
||||
|
||||
fn enabled_for_all() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClaudeCodeFeatureFlag;
|
||||
@@ -201,7 +205,7 @@ impl FeatureFlagAppExt for App {
|
||||
fn has_flag<T: FeatureFlag>(&self) -> bool {
|
||||
self.try_global::<FeatureFlags>()
|
||||
.map(|flags| flags.has_flag::<T>())
|
||||
.unwrap_or(false)
|
||||
.unwrap_or(T::enabled_for_all())
|
||||
}
|
||||
|
||||
fn is_staff(&self) -> bool {
|
||||
|
||||
@@ -1381,7 +1381,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
project
|
||||
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||
.is_some()
|
||||
|| ((project.is_local() || project.is_via_ssh())
|
||||
|| ((project.is_local() || project.is_via_remote_server())
|
||||
&& history_item.absolute.is_some())
|
||||
}),
|
||||
self.currently_opened_path.as_ref(),
|
||||
|
||||
@@ -4466,7 +4466,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn Language
|
||||
is_enabled
|
||||
.then(|| {
|
||||
let ConfiguredModel { provider, model } =
|
||||
LanguageModelRegistry::read_global(cx).commit_message_model(cx)?;
|
||||
LanguageModelRegistry::read_global(cx).commit_message_model()?;
|
||||
|
||||
provider.is_authenticated(cx).then(|| model)
|
||||
})
|
||||
|
||||
@@ -37,10 +37,10 @@ use crate::{
|
||||
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
||||
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
|
||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
||||
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
|
||||
WindowHandle, WindowId, WindowInvalidator,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
|
||||
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
|
||||
Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
|
||||
Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||
colors::{Colors, GlobalColors},
|
||||
current_platform, hash, init_app_menus,
|
||||
};
|
||||
@@ -263,6 +263,7 @@ pub struct App {
|
||||
pub(crate) focus_handles: Arc<FocusMap>,
|
||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
||||
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
|
||||
pub(crate) global_action_listeners:
|
||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
@@ -312,6 +313,7 @@ impl App {
|
||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
let entities = EntityMap::new();
|
||||
let keyboard_layout = platform.keyboard_layout();
|
||||
let keyboard_mapper = platform.keyboard_mapper();
|
||||
|
||||
let app = Rc::new_cyclic(|this| AppCell {
|
||||
app: RefCell::new(App {
|
||||
@@ -337,6 +339,7 @@ impl App {
|
||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||
keyboard_layout,
|
||||
keyboard_mapper,
|
||||
global_action_listeners: FxHashMap::default(),
|
||||
pending_effects: VecDeque::new(),
|
||||
pending_notifications: FxHashSet::default(),
|
||||
@@ -376,6 +379,7 @@ impl App {
|
||||
if let Some(app) = app.upgrade() {
|
||||
let cx = &mut app.borrow_mut();
|
||||
cx.keyboard_layout = cx.platform.keyboard_layout();
|
||||
cx.keyboard_mapper = cx.platform.keyboard_mapper();
|
||||
cx.keyboard_layout_observers
|
||||
.clone()
|
||||
.retain(&(), move |callback| (callback)(cx));
|
||||
@@ -424,6 +428,11 @@ impl App {
|
||||
self.keyboard_layout.as_ref()
|
||||
}
|
||||
|
||||
/// Get the current keyboard mapper.
|
||||
pub fn keyboard_mapper(&self) -> &Rc<dyn PlatformKeyboardMapper> {
|
||||
&self.keyboard_mapper
|
||||
}
|
||||
|
||||
/// Invokes a handler when the current keyboard layout changes
|
||||
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
||||
where
|
||||
|
||||
@@ -4,7 +4,7 @@ mod context;
|
||||
pub use binding::*;
|
||||
pub use context::*;
|
||||
|
||||
use crate::{Action, Keystroke, is_no_action};
|
||||
use crate::{Action, AsKeystroke, Keystroke, is_no_action};
|
||||
use collections::{HashMap, HashSet};
|
||||
use smallvec::SmallVec;
|
||||
use std::any::TypeId;
|
||||
@@ -141,7 +141,7 @@ impl Keymap {
|
||||
/// only.
|
||||
pub fn bindings_for_input(
|
||||
&self,
|
||||
input: &[Keystroke],
|
||||
input: &[impl AsKeystroke],
|
||||
context_stack: &[KeyContext],
|
||||
) -> (SmallVec<[KeyBinding; 1]>, bool) {
|
||||
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
|
||||
@@ -192,7 +192,6 @@ impl Keymap {
|
||||
|
||||
(bindings, !pending.is_empty())
|
||||
}
|
||||
|
||||
/// Check if the given binding is enabled, given a certain key context.
|
||||
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
||||
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
||||
@@ -639,7 +638,7 @@ mod tests {
|
||||
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
||||
let actual = keymap
|
||||
.bindings_for_action(action)
|
||||
.map(|binding| binding.keystrokes[0].unparse())
|
||||
.map(|binding| binding.keystrokes[0].inner.unparse())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected, "{:?}", action);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
|
||||
use crate::{
|
||||
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
|
||||
KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A keybinding and its associated metadata, from the keymap.
|
||||
pub struct KeyBinding {
|
||||
pub(crate) action: Box<dyn Action>,
|
||||
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||
pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
|
||||
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
||||
/// The json input string used when building the keybinding, if any
|
||||
@@ -32,7 +33,15 @@ impl KeyBinding {
|
||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||
let context_predicate =
|
||||
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
|
||||
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
|
||||
Self::load(
|
||||
keystrokes,
|
||||
Box::new(action),
|
||||
context_predicate,
|
||||
false,
|
||||
None,
|
||||
&DummyKeyboardMapper,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Load a keybinding from the given raw data.
|
||||
@@ -40,24 +49,22 @@ impl KeyBinding {
|
||||
keystrokes: &str,
|
||||
action: Box<dyn Action>,
|
||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
use_key_equivalents: bool,
|
||||
action_input: Option<SharedString>,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||
let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
|
||||
.split_whitespace()
|
||||
.map(Keystroke::parse)
|
||||
.map(|source| {
|
||||
let keystroke = Keystroke::parse(source)?;
|
||||
Ok(KeybindingKeystroke::new(
|
||||
keystroke,
|
||||
use_key_equivalents,
|
||||
keyboard_mapper,
|
||||
))
|
||||
})
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
|
||||
if let Some(equivalents) = key_equivalents {
|
||||
for keystroke in keystrokes.iter_mut() {
|
||||
if keystroke.key.chars().count() == 1
|
||||
&& let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
|
||||
{
|
||||
keystroke.key = key.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
keystrokes,
|
||||
action,
|
||||
@@ -79,13 +86,13 @@ impl KeyBinding {
|
||||
}
|
||||
|
||||
/// Check if the given keystrokes match this binding.
|
||||
pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
|
||||
pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option<bool> {
|
||||
if self.keystrokes.len() < typed.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
|
||||
if !typed.should_match(target) {
|
||||
if !typed.as_keystroke().should_match(target) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -94,7 +101,7 @@ impl KeyBinding {
|
||||
}
|
||||
|
||||
/// Get the keystrokes associated with this binding
|
||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
||||
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
||||
self.keystrokes.as_slice()
|
||||
}
|
||||
|
||||
|
||||
@@ -231,7 +231,6 @@ pub(crate) trait Platform: 'static {
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
@@ -251,7 +250,6 @@ pub(crate) trait Platform: 'static {
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
""
|
||||
@@ -272,6 +270,10 @@ pub(crate) trait Platform: 'static {
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
}
|
||||
|
||||
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{KeybindingKeystroke, Keystroke};
|
||||
|
||||
/// A trait for platform-specific keyboard layouts
|
||||
pub trait PlatformKeyboardLayout {
|
||||
/// Get the keyboard layout ID, which should be unique to the layout
|
||||
@@ -5,3 +9,33 @@ pub trait PlatformKeyboardLayout {
|
||||
/// Get the keyboard layout display name
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
/// A trait for platform-specific keyboard mappings
|
||||
pub trait PlatformKeyboardMapper {
|
||||
/// Map a key equivalent to its platform-specific representation
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
keystroke: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke;
|
||||
/// Get the key equivalents for the current keyboard layout,
|
||||
/// only used on macOS
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>>;
|
||||
}
|
||||
|
||||
/// A dummy implementation of the platform keyboard mapper
|
||||
pub struct DummyKeyboardMapper;
|
||||
|
||||
impl PlatformKeyboardMapper for DummyKeyboardMapper {
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
keystroke: Keystroke,
|
||||
_use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke {
|
||||
KeybindingKeystroke::from_keystroke(keystroke)
|
||||
}
|
||||
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,14 @@ use std::{
|
||||
fmt::{Display, Write},
|
||||
};
|
||||
|
||||
use crate::PlatformKeyboardMapper;
|
||||
|
||||
/// This is a helper trait so that we can simplify the implementation of some functions
|
||||
pub trait AsKeystroke {
|
||||
/// Returns the GPUI representation of the keystroke.
|
||||
fn as_keystroke(&self) -> &Keystroke;
|
||||
}
|
||||
|
||||
/// A keystroke and associated metadata generated by the platform
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||
pub struct Keystroke {
|
||||
@@ -24,6 +32,17 @@ pub struct Keystroke {
|
||||
pub key_char: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents a keystroke that can be used in keybindings and displayed to the user.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct KeybindingKeystroke {
|
||||
/// The GPUI representation of the keystroke.
|
||||
pub inner: Keystroke,
|
||||
/// The modifiers to display.
|
||||
pub display_modifiers: Modifiers,
|
||||
/// The key to display.
|
||||
pub display_key: String,
|
||||
}
|
||||
|
||||
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
||||
/// markdown to display it.
|
||||
#[derive(Debug)]
|
||||
@@ -58,7 +77,7 @@ impl Keystroke {
|
||||
///
|
||||
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
||||
/// both possibilities for self against the target.
|
||||
pub fn should_match(&self, target: &Keystroke) -> bool {
|
||||
pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(key_char) = self
|
||||
.key_char
|
||||
@@ -71,7 +90,7 @@ impl Keystroke {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if &target.key == key_char && target.modifiers == ime_modifiers {
|
||||
if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -83,12 +102,12 @@ impl Keystroke {
|
||||
.filter(|key_char| key_char != &&self.key)
|
||||
{
|
||||
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
||||
if &target.key == key_char && target.modifiers == Modifiers::none() {
|
||||
if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
target.modifiers == self.modifiers && target.key == self.key
|
||||
target.inner.modifiers == self.modifiers && target.inner.key == self.key
|
||||
}
|
||||
|
||||
/// key syntax is:
|
||||
@@ -200,31 +219,7 @@ impl Keystroke {
|
||||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
let mut str = String::new();
|
||||
if self.modifiers.function {
|
||||
str.push_str("fn-");
|
||||
}
|
||||
if self.modifiers.control {
|
||||
str.push_str("ctrl-");
|
||||
}
|
||||
if self.modifiers.alt {
|
||||
str.push_str("alt-");
|
||||
}
|
||||
if self.modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
str.push_str("cmd-");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
str.push_str("super-");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
str.push_str("win-");
|
||||
}
|
||||
if self.modifiers.shift {
|
||||
str.push_str("shift-");
|
||||
}
|
||||
str.push_str(&self.key);
|
||||
str
|
||||
unparse(&self.modifiers, &self.key)
|
||||
}
|
||||
|
||||
/// Returns true if this keystroke left
|
||||
@@ -266,6 +261,32 @@ impl Keystroke {
|
||||
}
|
||||
}
|
||||
|
||||
impl KeybindingKeystroke {
|
||||
/// Create a new keybinding keystroke from the given keystroke
|
||||
pub fn new(
|
||||
inner: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> Self {
|
||||
keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
|
||||
}
|
||||
|
||||
pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self {
|
||||
let key = keystroke.key.clone();
|
||||
let modifiers = keystroke.modifiers;
|
||||
KeybindingKeystroke {
|
||||
inner: keystroke,
|
||||
display_modifiers: modifiers,
|
||||
display_key: key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
unparse(&self.display_modifiers, &self.display_key)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_printable_key(key: &str) -> bool {
|
||||
!matches!(
|
||||
key,
|
||||
@@ -322,65 +343,15 @@ fn is_printable_key(key: &str) -> bool {
|
||||
|
||||
impl std::fmt::Display for Keystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.modifiers.control {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('^')?;
|
||||
display_modifiers(&self.modifiers, f)?;
|
||||
display_key(&self.key, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "ctrl-")?;
|
||||
}
|
||||
if self.modifiers.alt {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌥')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "alt-")?;
|
||||
}
|
||||
if self.modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌘')?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
f.write_char('❖')?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
f.write_char('⊞')?;
|
||||
}
|
||||
if self.modifiers.shift {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⇧')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "shift-")?;
|
||||
}
|
||||
let key = match self.key.as_str() {
|
||||
#[cfg(target_os = "macos")]
|
||||
"backspace" => '⌫',
|
||||
#[cfg(target_os = "macos")]
|
||||
"up" => '↑',
|
||||
#[cfg(target_os = "macos")]
|
||||
"down" => '↓',
|
||||
#[cfg(target_os = "macos")]
|
||||
"left" => '←',
|
||||
#[cfg(target_os = "macos")]
|
||||
"right" => '→',
|
||||
#[cfg(target_os = "macos")]
|
||||
"tab" => '⇥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"escape" => '⎋',
|
||||
#[cfg(target_os = "macos")]
|
||||
"shift" => '⇧',
|
||||
#[cfg(target_os = "macos")]
|
||||
"control" => '⌃',
|
||||
#[cfg(target_os = "macos")]
|
||||
"alt" => '⌥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"platform" => '⌘',
|
||||
|
||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||
key => return f.write_str(key),
|
||||
};
|
||||
f.write_char(key)
|
||||
impl std::fmt::Display for KeybindingKeystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
display_modifiers(&self.display_modifiers, f)?;
|
||||
display_key(&self.display_key, f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,3 +571,110 @@ pub struct Capslock {
|
||||
#[serde(default)]
|
||||
pub on: bool,
|
||||
}
|
||||
|
||||
impl AsKeystroke for Keystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsKeystroke for KeybindingKeystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if modifiers.control {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('^')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "ctrl-")?;
|
||||
}
|
||||
if modifiers.alt {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌥')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "alt-")?;
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌘')?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
f.write_char('❖')?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
f.write_char('⊞')?;
|
||||
}
|
||||
if modifiers.shift {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⇧')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "shift-")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let key = match key {
|
||||
#[cfg(target_os = "macos")]
|
||||
"backspace" => '⌫',
|
||||
#[cfg(target_os = "macos")]
|
||||
"up" => '↑',
|
||||
#[cfg(target_os = "macos")]
|
||||
"down" => '↓',
|
||||
#[cfg(target_os = "macos")]
|
||||
"left" => '←',
|
||||
#[cfg(target_os = "macos")]
|
||||
"right" => '→',
|
||||
#[cfg(target_os = "macos")]
|
||||
"tab" => '⇥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"escape" => '⎋',
|
||||
#[cfg(target_os = "macos")]
|
||||
"shift" => '⇧',
|
||||
#[cfg(target_os = "macos")]
|
||||
"control" => '⌃',
|
||||
#[cfg(target_os = "macos")]
|
||||
"alt" => '⌥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"platform" => '⌘',
|
||||
|
||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||
key => return f.write_str(key),
|
||||
};
|
||||
f.write_char(key)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn unparse(modifiers: &Modifiers, key: &str) -> String {
|
||||
let mut result = String::new();
|
||||
if modifiers.function {
|
||||
result.push_str("fn-");
|
||||
}
|
||||
if modifiers.control {
|
||||
result.push_str("ctrl-");
|
||||
}
|
||||
if modifiers.alt {
|
||||
result.push_str("alt-");
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
result.push_str("cmd-");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
result.push_str("super-");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
result.push_str("win-");
|
||||
}
|
||||
if modifiers.shift {
|
||||
result.push_str("shift-");
|
||||
}
|
||||
result.push_str(&key);
|
||||
result
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||
use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
|
||||
Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
@@ -144,6 +144,10 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
self.keyboard_layout()
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(crate::DummyKeyboardMapper)
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
use super::{
|
||||
BoolExt, MacKeyboardLayout,
|
||||
BoolExt, MacKeyboardLayout, MacKeyboardMapper,
|
||||
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
||||
events::key_to_native,
|
||||
renderer,
|
||||
@@ -8,8 +8,9 @@ use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
|
||||
SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams,
|
||||
hash,
|
||||
};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use block::ConcreteBlock;
|
||||
@@ -171,6 +172,7 @@ pub(crate) struct MacPlatformState {
|
||||
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||
dock_menu: Option<id>,
|
||||
menus: Option<Vec<OwnedMenu>>,
|
||||
keyboard_mapper: Rc<MacKeyboardMapper>,
|
||||
}
|
||||
|
||||
impl Default for MacPlatform {
|
||||
@@ -189,6 +191,9 @@ impl MacPlatform {
|
||||
#[cfg(not(feature = "font-kit"))]
|
||||
let text_system = Arc::new(crate::NoopTextSystem::new());
|
||||
|
||||
let keyboard_layout = MacKeyboardLayout::new();
|
||||
let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||
|
||||
Self(Mutex::new(MacPlatformState {
|
||||
headless,
|
||||
text_system,
|
||||
@@ -209,6 +214,7 @@ impl MacPlatform {
|
||||
dock_menu: None,
|
||||
on_keyboard_layout_change: None,
|
||||
menus: None,
|
||||
keyboard_mapper,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -348,19 +354,19 @@ impl MacPlatform {
|
||||
let mut mask = NSEventModifierFlags::empty();
|
||||
for (modifier, flag) in &[
|
||||
(
|
||||
keystroke.modifiers.platform,
|
||||
keystroke.display_modifiers.platform,
|
||||
NSEventModifierFlags::NSCommandKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.control,
|
||||
keystroke.display_modifiers.control,
|
||||
NSEventModifierFlags::NSControlKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.alt,
|
||||
keystroke.display_modifiers.alt,
|
||||
NSEventModifierFlags::NSAlternateKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.shift,
|
||||
keystroke.display_modifiers.shift,
|
||||
NSEventModifierFlags::NSShiftKeyMask,
|
||||
),
|
||||
] {
|
||||
@@ -373,7 +379,7 @@ impl MacPlatform {
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector,
|
||||
ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||
ns_string(key_to_native(&keystroke.display_key).as_ref()),
|
||||
)
|
||||
.autorelease();
|
||||
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
|
||||
@@ -882,6 +888,10 @@ impl Platform for MacPlatform {
|
||||
Box::new(MacKeyboardLayout::new())
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
self.0.lock().keyboard_mapper.clone()
|
||||
}
|
||||
|
||||
fn app_path(&self) -> Result<PathBuf> {
|
||||
unsafe {
|
||||
let bundle: id = NSBundle::mainBundle();
|
||||
@@ -1393,6 +1403,8 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
|
||||
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
let mut lock = platform.0.lock();
|
||||
let keyboard_layout = MacKeyboardLayout::new();
|
||||
lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
|
||||
drop(lock);
|
||||
callback();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
||||
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
||||
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::VecDeque;
|
||||
@@ -237,6 +238,10 @@ impl Platform for TestPlatform {
|
||||
Box::new(TestKeyboardLayout)
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(DummyKeyboardMapper)
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use windows::Win32::UI::{
|
||||
Input::KeyboardAndMouse::{
|
||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
|
||||
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
|
||||
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
|
||||
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
|
||||
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
|
||||
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
|
||||
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||
},
|
||||
WindowsAndMessaging::KL_NAMELENGTH,
|
||||
};
|
||||
use windows_core::HSTRING;
|
||||
|
||||
use crate::{Modifiers, PlatformKeyboardLayout};
|
||||
use crate::{
|
||||
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
};
|
||||
|
||||
pub(crate) struct WindowsKeyboardLayout {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub(crate) struct WindowsKeyboardMapper {
|
||||
key_to_vkey: HashMap<String, (u16, bool)>,
|
||||
vkey_to_key: HashMap<u16, String>,
|
||||
vkey_to_shifted: HashMap<u16, String>,
|
||||
}
|
||||
|
||||
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
@@ -27,6 +36,65 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
mut keystroke: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke {
|
||||
let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
|
||||
else {
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
if shifted_key && keystroke.modifiers.shift {
|
||||
log::warn!(
|
||||
"Keystroke '{}' has both shift and a shifted key, this is likely a bug",
|
||||
keystroke.key
|
||||
);
|
||||
}
|
||||
|
||||
let shift = shifted_key || keystroke.modifiers.shift;
|
||||
keystroke.modifiers.shift = false;
|
||||
|
||||
let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
|
||||
log::error!(
|
||||
"Failed to map key equivalent '{:?}' to a valid key",
|
||||
keystroke
|
||||
);
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
|
||||
keystroke.key = if shift {
|
||||
let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
|
||||
log::error!(
|
||||
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
|
||||
keystroke,
|
||||
vkey
|
||||
);
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
shifted_key
|
||||
} else {
|
||||
key.clone()
|
||||
};
|
||||
|
||||
let modifiers = Modifiers {
|
||||
shift,
|
||||
..keystroke.modifiers
|
||||
};
|
||||
|
||||
KeybindingKeystroke {
|
||||
inner: keystroke,
|
||||
display_modifiers: modifiers,
|
||||
display_key: key,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowsKeyboardLayout {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
let mut buffer = [0u16; KL_NAMELENGTH as usize];
|
||||
@@ -48,6 +116,41 @@ impl WindowsKeyboardLayout {
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowsKeyboardMapper {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut key_to_vkey = HashMap::default();
|
||||
let mut vkey_to_key = HashMap::default();
|
||||
let mut vkey_to_shifted = HashMap::default();
|
||||
for vkey in CANDIDATE_VKEYS {
|
||||
if let Some(key) = get_key_from_vkey(*vkey) {
|
||||
key_to_vkey.insert(key.clone(), (vkey.0, false));
|
||||
vkey_to_key.insert(vkey.0, key);
|
||||
}
|
||||
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
|
||||
if scan_code == 0 {
|
||||
continue;
|
||||
}
|
||||
if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
|
||||
key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
|
||||
vkey_to_shifted.insert(vkey.0, shifted_key);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
key_to_vkey,
|
||||
vkey_to_key,
|
||||
vkey_to_shifted,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
|
||||
if use_key_equivalents {
|
||||
get_vkey_from_key_with_us_layout(key)
|
||||
} else {
|
||||
self.key_to_vkey.get(key).cloned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_keystroke_key(
|
||||
vkey: VIRTUAL_KEY,
|
||||
scan_code: u32,
|
||||
@@ -140,3 +243,134 @@ pub(crate) fn generate_key_char(
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
|
||||
match key {
|
||||
// ` => VK_OEM_3
|
||||
"`" => Some((VK_OEM_3.0, false)),
|
||||
"~" => Some((VK_OEM_3.0, true)),
|
||||
"1" => Some((VK_1.0, false)),
|
||||
"!" => Some((VK_1.0, true)),
|
||||
"2" => Some((VK_2.0, false)),
|
||||
"@" => Some((VK_2.0, true)),
|
||||
"3" => Some((VK_3.0, false)),
|
||||
"#" => Some((VK_3.0, true)),
|
||||
"4" => Some((VK_4.0, false)),
|
||||
"$" => Some((VK_4.0, true)),
|
||||
"5" => Some((VK_5.0, false)),
|
||||
"%" => Some((VK_5.0, true)),
|
||||
"6" => Some((VK_6.0, false)),
|
||||
"^" => Some((VK_6.0, true)),
|
||||
"7" => Some((VK_7.0, false)),
|
||||
"&" => Some((VK_7.0, true)),
|
||||
"8" => Some((VK_8.0, false)),
|
||||
"*" => Some((VK_8.0, true)),
|
||||
"9" => Some((VK_9.0, false)),
|
||||
"(" => Some((VK_9.0, true)),
|
||||
"0" => Some((VK_0.0, false)),
|
||||
")" => Some((VK_0.0, true)),
|
||||
"-" => Some((VK_OEM_MINUS.0, false)),
|
||||
"_" => Some((VK_OEM_MINUS.0, true)),
|
||||
"=" => Some((VK_OEM_PLUS.0, false)),
|
||||
"+" => Some((VK_OEM_PLUS.0, true)),
|
||||
"[" => Some((VK_OEM_4.0, false)),
|
||||
"{" => Some((VK_OEM_4.0, true)),
|
||||
"]" => Some((VK_OEM_6.0, false)),
|
||||
"}" => Some((VK_OEM_6.0, true)),
|
||||
"\\" => Some((VK_OEM_5.0, false)),
|
||||
"|" => Some((VK_OEM_5.0, true)),
|
||||
";" => Some((VK_OEM_1.0, false)),
|
||||
":" => Some((VK_OEM_1.0, true)),
|
||||
"'" => Some((VK_OEM_7.0, false)),
|
||||
"\"" => Some((VK_OEM_7.0, true)),
|
||||
"," => Some((VK_OEM_COMMA.0, false)),
|
||||
"<" => Some((VK_OEM_COMMA.0, true)),
|
||||
"." => Some((VK_OEM_PERIOD.0, false)),
|
||||
">" => Some((VK_OEM_PERIOD.0, true)),
|
||||
"/" => Some((VK_OEM_2.0, false)),
|
||||
"?" => Some((VK_OEM_2.0, true)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
|
||||
VK_OEM_3,
|
||||
VK_OEM_MINUS,
|
||||
VK_OEM_PLUS,
|
||||
VK_OEM_4,
|
||||
VK_OEM_5,
|
||||
VK_OEM_6,
|
||||
VK_OEM_1,
|
||||
VK_OEM_7,
|
||||
VK_OEM_COMMA,
|
||||
VK_OEM_PERIOD,
|
||||
VK_OEM_2,
|
||||
VK_OEM_102,
|
||||
VK_OEM_8,
|
||||
VK_ABNT_C1,
|
||||
VK_0,
|
||||
VK_1,
|
||||
VK_2,
|
||||
VK_3,
|
||||
VK_4,
|
||||
VK_5,
|
||||
VK_6,
|
||||
VK_7,
|
||||
VK_8,
|
||||
VK_9,
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
|
||||
|
||||
#[test]
|
||||
fn test_keyboard_mapper() {
|
||||
let mapper = WindowsKeyboardMapper::new();
|
||||
|
||||
// Normal case
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: "a".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||
assert_eq!(mapped.inner, keystroke);
|
||||
assert_eq!(mapped.display_key, "a");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control());
|
||||
|
||||
// Shifted case, ctrl-$
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: "$".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||
assert_eq!(mapped.inner, keystroke);
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
|
||||
// Shifted case, but shift is true
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control_shift(),
|
||||
key: "$".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
|
||||
// Windows style
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control_shift(),
|
||||
key: "4".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||
assert_eq!(mapped.inner.key, "$");
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +351,10 @@ impl Platform for WindowsPlatform {
|
||||
)
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(WindowsKeyboardMapper::new())
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ pub enum IconName {
|
||||
Tab,
|
||||
Terminal,
|
||||
TerminalAlt,
|
||||
TerminalGhost,
|
||||
TextSnippet,
|
||||
TextThread,
|
||||
Thread,
|
||||
|
||||
@@ -401,12 +401,19 @@ pub fn init(cx: &mut App) {
|
||||
mod persistence {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
define_connection! {
|
||||
pub static ref IMAGE_VIEWER: ImageViewerDb<WorkspaceDb> =
|
||||
&[sql!(
|
||||
pub struct ImageViewerDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for ImageViewerDb {
|
||||
const NAME: &str = stringify!(ImageViewerDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE image_viewers (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
@@ -417,9 +424,11 @@ mod persistence {
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)];
|
||||
)];
|
||||
}
|
||||
|
||||
db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]);
|
||||
|
||||
impl ImageViewerDb {
|
||||
query! {
|
||||
pub async fn save_image_path(
|
||||
|
||||
@@ -4,12 +4,16 @@ use crate::{
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyView, App, AsyncApp, Entity, Task, Window};
|
||||
use http_client::Result;
|
||||
use parking_lot::Mutex;
|
||||
use smol::stream::StreamExt;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FakeLanguageModelProvider {
|
||||
@@ -106,6 +110,7 @@ pub struct FakeLanguageModel {
|
||||
>,
|
||||
)>,
|
||||
>,
|
||||
forbid_requests: AtomicBool,
|
||||
}
|
||||
|
||||
impl Default for FakeLanguageModel {
|
||||
@@ -114,11 +119,20 @@ impl Default for FakeLanguageModel {
|
||||
provider_id: LanguageModelProviderId::from("fake".to_string()),
|
||||
provider_name: LanguageModelProviderName::from("Fake".to_string()),
|
||||
current_completion_txs: Mutex::new(Vec::new()),
|
||||
forbid_requests: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeLanguageModel {
|
||||
pub fn allow_requests(&self) {
|
||||
self.forbid_requests.store(false, SeqCst);
|
||||
}
|
||||
|
||||
pub fn forbid_requests(&self) {
|
||||
self.forbid_requests.store(true, SeqCst);
|
||||
}
|
||||
|
||||
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
|
||||
self.current_completion_txs
|
||||
.lock()
|
||||
@@ -251,9 +265,18 @@ impl LanguageModel for FakeLanguageModel {
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.current_completion_txs.lock().push((request, tx));
|
||||
async move { Ok(rx.boxed()) }.boxed()
|
||||
if self.forbid_requests.load(SeqCst) {
|
||||
async move {
|
||||
Err(LanguageModelCompletionError::Other(anyhow!(
|
||||
"requests are forbidden"
|
||||
)))
|
||||
}
|
||||
.boxed()
|
||||
} else {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.current_completion_txs.lock().push((request, tx));
|
||||
async move { Ok(rx.boxed()) }.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn as_fake(&self) -> &Self {
|
||||
|
||||
@@ -6,6 +6,7 @@ use collections::BTreeMap;
|
||||
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use thiserror::Error;
|
||||
use util::maybe;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let registry = cx.new(|_cx| LanguageModelRegistry::default());
|
||||
@@ -41,9 +42,7 @@ impl std::fmt::Debug for ConfigurationError {
|
||||
#[derive(Default)]
|
||||
pub struct LanguageModelRegistry {
|
||||
default_model: Option<ConfiguredModel>,
|
||||
/// This model is automatically configured by a user's environment after
|
||||
/// authenticating all providers. It's only used when default_model is not available.
|
||||
environment_fallback_model: Option<ConfiguredModel>,
|
||||
default_fast_model: Option<ConfiguredModel>,
|
||||
inline_assistant_model: Option<ConfiguredModel>,
|
||||
commit_message_model: Option<ConfiguredModel>,
|
||||
thread_summary_model: Option<ConfiguredModel>,
|
||||
@@ -99,6 +98,9 @@ impl ConfiguredModel {
|
||||
|
||||
pub enum Event {
|
||||
DefaultModelChanged,
|
||||
InlineAssistantModelChanged,
|
||||
CommitMessageModelChanged,
|
||||
ThreadSummaryModelChanged,
|
||||
ProviderStateChanged(LanguageModelProviderId),
|
||||
AddedProvider(LanguageModelProviderId),
|
||||
RemovedProvider(LanguageModelProviderId),
|
||||
@@ -224,7 +226,7 @@ impl LanguageModelRegistry {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||
self.set_inline_assistant_model(configured_model);
|
||||
self.set_inline_assistant_model(configured_model, cx);
|
||||
}
|
||||
|
||||
pub fn select_commit_message_model(
|
||||
@@ -233,7 +235,7 @@ impl LanguageModelRegistry {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||
self.set_commit_message_model(configured_model);
|
||||
self.set_commit_message_model(configured_model, cx);
|
||||
}
|
||||
|
||||
pub fn select_thread_summary_model(
|
||||
@@ -242,7 +244,7 @@ impl LanguageModelRegistry {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let configured_model = model.and_then(|model| self.select_model(model, cx));
|
||||
self.set_thread_summary_model(configured_model);
|
||||
self.set_thread_summary_model(configured_model, cx);
|
||||
}
|
||||
|
||||
/// Selects and sets the inline alternatives for language models based on
|
||||
@@ -276,60 +278,68 @@ impl LanguageModelRegistry {
|
||||
}
|
||||
|
||||
pub fn set_default_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
|
||||
match (self.default_model(), model.as_ref()) {
|
||||
match (self.default_model.as_ref(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::DefaultModelChanged),
|
||||
}
|
||||
self.default_fast_model = maybe!({
|
||||
let provider = &model.as_ref()?.provider;
|
||||
let fast_model = provider.default_fast_model(cx)?;
|
||||
Some(ConfiguredModel {
|
||||
provider: provider.clone(),
|
||||
model: fast_model,
|
||||
})
|
||||
});
|
||||
self.default_model = model;
|
||||
}
|
||||
|
||||
pub fn set_environment_fallback_model(
|
||||
pub fn set_inline_assistant_model(
|
||||
&mut self,
|
||||
model: Option<ConfiguredModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.default_model.is_none() {
|
||||
match (self.environment_fallback_model.as_ref(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::DefaultModelChanged),
|
||||
}
|
||||
match (self.inline_assistant_model.as_ref(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::InlineAssistantModelChanged),
|
||||
}
|
||||
self.environment_fallback_model = model;
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, model: Option<ConfiguredModel>) {
|
||||
self.inline_assistant_model = model;
|
||||
}
|
||||
|
||||
pub fn set_commit_message_model(&mut self, model: Option<ConfiguredModel>) {
|
||||
pub fn set_commit_message_model(
|
||||
&mut self,
|
||||
model: Option<ConfiguredModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match (self.commit_message_model.as_ref(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::CommitMessageModelChanged),
|
||||
}
|
||||
self.commit_message_model = model;
|
||||
}
|
||||
|
||||
pub fn set_thread_summary_model(&mut self, model: Option<ConfiguredModel>) {
|
||||
pub fn set_thread_summary_model(
|
||||
&mut self,
|
||||
model: Option<ConfiguredModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match (self.thread_summary_model.as_ref(), model.as_ref()) {
|
||||
(Some(old), Some(new)) if old.is_same_as(new) => {}
|
||||
(None, None) => {}
|
||||
_ => cx.emit(Event::ThreadSummaryModelChanged),
|
||||
}
|
||||
self.thread_summary_model = model;
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn default_model(&self) -> Option<ConfiguredModel> {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.default_model
|
||||
.clone()
|
||||
.or_else(|| self.environment_fallback_model.clone())
|
||||
}
|
||||
|
||||
pub fn default_fast_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
let provider = self.default_model()?.provider;
|
||||
let fast_model = provider.default_fast_model(cx)?;
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: fast_model,
|
||||
})
|
||||
self.default_model.clone()
|
||||
}
|
||||
|
||||
pub fn inline_assistant_model(&self) -> Option<ConfiguredModel> {
|
||||
@@ -343,7 +353,7 @@ impl LanguageModelRegistry {
|
||||
.or_else(|| self.default_model.clone())
|
||||
}
|
||||
|
||||
pub fn commit_message_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
pub fn commit_message_model(&self) -> Option<ConfiguredModel> {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||
return None;
|
||||
@@ -351,11 +361,11 @@ impl LanguageModelRegistry {
|
||||
|
||||
self.commit_message_model
|
||||
.clone()
|
||||
.or_else(|| self.default_fast_model(cx))
|
||||
.or_else(|| self.default_fast_model.clone())
|
||||
.or_else(|| self.default_model.clone())
|
||||
}
|
||||
|
||||
pub fn thread_summary_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
pub fn thread_summary_model(&self) -> Option<ConfiguredModel> {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() {
|
||||
return None;
|
||||
@@ -363,7 +373,7 @@ impl LanguageModelRegistry {
|
||||
|
||||
self.thread_summary_model
|
||||
.clone()
|
||||
.or_else(|| self.default_fast_model(cx))
|
||||
.or_else(|| self.default_fast_model.clone())
|
||||
.or_else(|| self.default_model.clone())
|
||||
}
|
||||
|
||||
@@ -400,34 +410,4 @@ mod tests {
|
||||
let providers = registry.read(cx).providers();
|
||||
assert!(providers.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) {
|
||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||
|
||||
let provider = FakeLanguageModelProvider::default();
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.register_provider(provider.clone(), cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| provider.authenticate(cx)).await.unwrap();
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
let provider = registry.provider(&provider.id()).unwrap();
|
||||
|
||||
registry.set_environment_fallback_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: provider.clone(),
|
||||
model: provider.default_model(cx).unwrap(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
let default_model = registry.default_model().unwrap();
|
||||
let fallback_model = registry.environment_fallback_model.clone().unwrap();
|
||||
|
||||
assert_eq!(default_model.model.id(), fallback_model.model.id());
|
||||
assert_eq!(default_model.provider.id(), fallback_model.provider.id());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
open_router = { workspace = true, features = ["schemars"] }
|
||||
partial-json-fixer.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -3,12 +3,8 @@ use std::sync::Arc;
|
||||
use ::settings::{Settings, SettingsStore};
|
||||
use client::{Client, UserStore};
|
||||
use collections::HashSet;
|
||||
use futures::future;
|
||||
use gpui::{App, AppContext as _, Context, Entity};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use gpui::{App, Context, Entity};
|
||||
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
|
||||
use provider::deepseek::DeepSeekLanguageModelProvider;
|
||||
|
||||
pub mod provider;
|
||||
@@ -17,7 +13,7 @@ pub mod ui;
|
||||
|
||||
use crate::provider::anthropic::AnthropicLanguageModelProvider;
|
||||
use crate::provider::bedrock::BedrockLanguageModelProvider;
|
||||
use crate::provider::cloud::{self, CloudLanguageModelProvider};
|
||||
use crate::provider::cloud::CloudLanguageModelProvider;
|
||||
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
|
||||
use crate::provider::google::GoogleLanguageModelProvider;
|
||||
use crate::provider::lmstudio::LmStudioLanguageModelProvider;
|
||||
@@ -52,13 +48,6 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let mut already_authenticated = false;
|
||||
if !DisableAiSettings::get_global(cx).disable_ai {
|
||||
authenticate_all_providers(registry.clone(), cx);
|
||||
already_authenticated = true;
|
||||
}
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx)
|
||||
.openai_compatible
|
||||
@@ -76,12 +65,6 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
||||
);
|
||||
});
|
||||
openai_compatible_providers = openai_compatible_providers_new;
|
||||
already_authenticated = false;
|
||||
}
|
||||
|
||||
if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated {
|
||||
authenticate_all_providers(registry.clone(), cx);
|
||||
already_authenticated = true;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -168,83 +151,3 @@ fn register_language_model_providers(
|
||||
registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx);
|
||||
registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
|
||||
}
|
||||
|
||||
/// Authenticates all providers in the [`LanguageModelRegistry`].
|
||||
///
|
||||
/// We do this so that we can populate the language selector with all of the
|
||||
/// models from the configured providers.
|
||||
///
|
||||
/// This function won't do anything if AI is disabled.
|
||||
fn authenticate_all_providers(registry: Entity<LanguageModelRegistry>, cx: &mut App) {
|
||||
let providers_to_authenticate = registry
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut tasks = Vec::with_capacity(providers_to_authenticate.len());
|
||||
|
||||
for (provider_id, provider_name, authenticate_task) in providers_to_authenticate {
|
||||
tasks.push(cx.background_spawn(async move {
|
||||
if let Err(err) = authenticate_task.await {
|
||||
if matches!(err, AuthenticateError::CredentialsNotFound) {
|
||||
// Since we're authenticating these providers in the
|
||||
// background for the purposes of populating the
|
||||
// language selector, we don't care about providers
|
||||
// where the credentials are not found.
|
||||
} else {
|
||||
// Some providers have noisy failure states that we
|
||||
// don't want to spam the logs with every time the
|
||||
// language model selector is initialized.
|
||||
//
|
||||
// Ideally these should have more clear failure modes
|
||||
// that we know are safe to ignore here, like what we do
|
||||
// with `CredentialsNotFound` above.
|
||||
match provider_id.0.as_ref() {
|
||||
"lmstudio" | "ollama" => {
|
||||
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
||||
//
|
||||
// These fail noisily, so we don't log them.
|
||||
}
|
||||
"copilot_chat" => {
|
||||
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"Failed to authenticate provider: {}: {err}",
|
||||
provider_name.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let all_authenticated_future = future::join_all(tasks);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
all_authenticated_future.await;
|
||||
|
||||
registry
|
||||
.update(cx, |registry, cx| {
|
||||
let cloud_provider = registry.provider(&cloud::PROVIDER_ID);
|
||||
let fallback_model = cloud_provider
|
||||
.iter()
|
||||
.chain(registry.providers().iter())
|
||||
.find(|provider| provider.is_authenticated(cx))
|
||||
.and_then(|provider| {
|
||||
Some(ConfiguredModel {
|
||||
provider: provider.clone(),
|
||||
model: provider
|
||||
.default_model(cx)
|
||||
.or_else(|| provider.recommended_models(cx).first().cloned())?,
|
||||
})
|
||||
});
|
||||
registry.set_environment_fallback_model(fallback_model, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i
|
||||
use crate::provider::google::{GoogleEventMapper, into_google};
|
||||
use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
|
||||
|
||||
pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
|
||||
pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
|
||||
const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
|
||||
const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
pub struct ZedDotDevSettings {
|
||||
@@ -146,7 +146,7 @@ impl State {
|
||||
default_fast_model: None,
|
||||
recommended_models: Vec::new(),
|
||||
_fetch_models_task: cx.spawn(async move |this, cx| {
|
||||
maybe!(async {
|
||||
maybe!(async move {
|
||||
let (client, llm_api_token) = this
|
||||
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;
|
||||
|
||||
|
||||
@@ -319,7 +319,7 @@ impl LanguageModel for XAiLanguageModel {
|
||||
}
|
||||
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
|
||||
let model_id = self.model.id().trim().to_lowercase();
|
||||
if model_id.eq(x_ai::Model::Grok4.id()) {
|
||||
if model_id.eq(x_ai::Model::Grok4.id()) || model_id.eq(x_ai::Model::GrokCodeFast1.id()) {
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset
|
||||
} else {
|
||||
LanguageModelToolSchemaFormat::JsonSchema
|
||||
|
||||
@@ -4,7 +4,6 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use serde_json::json;
|
||||
use settings::get_key_equivalents;
|
||||
use ui::{Button, ButtonStyle};
|
||||
use ui::{
|
||||
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
|
||||
@@ -169,7 +168,8 @@ impl Item for KeyContextView {
|
||||
impl Render for KeyContextView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
use itertools::Itertools;
|
||||
let key_equivalents = get_key_equivalents(cx.keyboard_layout().id());
|
||||
|
||||
let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
|
||||
v_flex()
|
||||
.id("key-context-view")
|
||||
.overflow_scroll()
|
||||
|
||||
@@ -222,7 +222,7 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
cx.observe_new(move |workspace: &mut Workspace, _, cx| {
|
||||
let project = workspace.project();
|
||||
if project.read(cx).is_local() || project.read(cx).is_via_ssh() {
|
||||
if project.read(cx).is_local() || project.read(cx).is_via_remote_server() {
|
||||
log_store.update(cx, |store, cx| {
|
||||
store.add_project(project, cx);
|
||||
});
|
||||
@@ -231,7 +231,7 @@ pub fn init(cx: &mut App) {
|
||||
let log_store = log_store.clone();
|
||||
workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
if project.is_local() || project.is_via_ssh() {
|
||||
if project.is_local() || project.is_via_remote_server() {
|
||||
let project = workspace.project().clone();
|
||||
let log_store = log_store.clone();
|
||||
get_or_create_tool(
|
||||
@@ -321,7 +321,7 @@ impl LogStore {
|
||||
.retain(|_, state| state.kind.project() != Some(&weak_project));
|
||||
}),
|
||||
cx.subscribe(project, |this, project, event, cx| {
|
||||
let server_kind = if project.read(cx).is_via_ssh() {
|
||||
let server_kind = if project.read(cx).is_via_remote_server() {
|
||||
LanguageServerKind::Remote {
|
||||
project: project.downgrade(),
|
||||
}
|
||||
|
||||
@@ -3,8 +3,27 @@
|
||||
(namespace_identifier) @namespace
|
||||
|
||||
(concept_definition
|
||||
(identifier) @concept)
|
||||
name: (identifier) @concept)
|
||||
|
||||
(requires_clause
|
||||
constraint: (template_type
|
||||
name: (type_identifier) @concept))
|
||||
|
||||
(module_name
|
||||
(identifier) @module)
|
||||
|
||||
(module_declaration
|
||||
name: (module_name
|
||||
(identifier) @module))
|
||||
|
||||
(import_declaration
|
||||
name: (module_name
|
||||
(identifier) @module))
|
||||
|
||||
(import_declaration
|
||||
partition: (module_partition
|
||||
(module_name
|
||||
(identifier) @module)))
|
||||
|
||||
(call_expression
|
||||
function: (qualified_identifier
|
||||
@@ -61,6 +80,9 @@
|
||||
(operator_name
|
||||
(identifier)? @operator) @function
|
||||
|
||||
(operator_name
|
||||
"<=>" @operator.spaceship)
|
||||
|
||||
(destructor_name (identifier) @function)
|
||||
|
||||
((namespace_identifier) @type
|
||||
@@ -68,21 +90,17 @@
|
||||
|
||||
(auto) @type
|
||||
(type_identifier) @type
|
||||
type :(primitive_type) @type.primitive
|
||||
(sized_type_specifier) @type.primitive
|
||||
|
||||
(requires_clause
|
||||
constraint: (template_type
|
||||
name: (type_identifier) @concept))
|
||||
type: (primitive_type) @type.builtin
|
||||
(sized_type_specifier) @type.builtin
|
||||
|
||||
(attribute
|
||||
name: (identifier) @keyword)
|
||||
name: (identifier) @attribute)
|
||||
|
||||
((identifier) @constant
|
||||
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
|
||||
((identifier) @constant.builtin
|
||||
(#match? @constant.builtin "^_*[A-Z][A-Z\\d_]*$"))
|
||||
|
||||
(statement_identifier) @label
|
||||
(this) @variable.special
|
||||
(this) @variable.builtin
|
||||
("static_assert") @function.builtin
|
||||
|
||||
[
|
||||
@@ -96,7 +114,9 @@ type :(primitive_type) @type.primitive
|
||||
"co_return"
|
||||
"co_yield"
|
||||
"concept"
|
||||
"consteval"
|
||||
"constexpr"
|
||||
"constinit"
|
||||
"continue"
|
||||
"decltype"
|
||||
"default"
|
||||
@@ -105,15 +125,20 @@ type :(primitive_type) @type.primitive
|
||||
"else"
|
||||
"enum"
|
||||
"explicit"
|
||||
"export"
|
||||
"extern"
|
||||
"final"
|
||||
"for"
|
||||
"friend"
|
||||
"goto"
|
||||
"if"
|
||||
"import"
|
||||
"inline"
|
||||
"module"
|
||||
"namespace"
|
||||
"new"
|
||||
"noexcept"
|
||||
"operator"
|
||||
"override"
|
||||
"private"
|
||||
"protected"
|
||||
@@ -124,6 +149,7 @@ type :(primitive_type) @type.primitive
|
||||
"struct"
|
||||
"switch"
|
||||
"template"
|
||||
"thread_local"
|
||||
"throw"
|
||||
"try"
|
||||
"typedef"
|
||||
@@ -146,7 +172,7 @@ type :(primitive_type) @type.primitive
|
||||
"#ifndef"
|
||||
"#include"
|
||||
(preproc_directive)
|
||||
] @keyword
|
||||
] @keyword.directive
|
||||
|
||||
(comment) @comment
|
||||
|
||||
@@ -224,10 +250,24 @@ type :(primitive_type) @type.primitive
|
||||
">"
|
||||
"<="
|
||||
">="
|
||||
"<=>"
|
||||
"||"
|
||||
"?"
|
||||
"and"
|
||||
"and_eq"
|
||||
"bitand"
|
||||
"bitor"
|
||||
"compl"
|
||||
"not"
|
||||
"not_eq"
|
||||
"or"
|
||||
"or_eq"
|
||||
"xor"
|
||||
"xor_eq"
|
||||
] @operator
|
||||
|
||||
"<=>" @operator.spaceship
|
||||
|
||||
(binary_expression
|
||||
operator: "<=>" @operator.spaceship)
|
||||
|
||||
(conditional_expression ":" @operator)
|
||||
(user_defined_literal (literal_suffix) @operator)
|
||||
|
||||
@@ -1323,7 +1323,7 @@ fn render_copy_code_block_button(
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Filled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Copy Code"))
|
||||
.tooltip(Tooltip::text("Copy"))
|
||||
.on_click({
|
||||
let markdown = markdown;
|
||||
move |_event, _window, cx| {
|
||||
|
||||
@@ -850,13 +850,19 @@ impl workspace::SerializableItem for Onboarding {
|
||||
}
|
||||
|
||||
mod persistence {
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use workspace::WorkspaceDb;
|
||||
|
||||
define_connection! {
|
||||
pub static ref ONBOARDING_PAGES: OnboardingPagesDb<WorkspaceDb> =
|
||||
&[
|
||||
sql!(
|
||||
pub struct OnboardingPagesDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for OnboardingPagesDb {
|
||||
const NAME: &str = stringify!(OnboardingPagesDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE onboarding_pages (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
@@ -866,10 +872,11 @@ mod persistence {
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
),
|
||||
];
|
||||
)];
|
||||
}
|
||||
|
||||
db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
|
||||
|
||||
impl OnboardingPagesDb {
|
||||
query! {
|
||||
pub async fn save_onboarding_page(
|
||||
|
||||
@@ -414,13 +414,19 @@ impl workspace::SerializableItem for WelcomePage {
|
||||
}
|
||||
|
||||
mod persistence {
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use workspace::WorkspaceDb;
|
||||
|
||||
define_connection! {
|
||||
pub static ref WELCOME_PAGES: WelcomePagesDb<WorkspaceDb> =
|
||||
&[
|
||||
sql!(
|
||||
pub struct WelcomePagesDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for WelcomePagesDb {
|
||||
const NAME: &str = stringify!(WelcomePagesDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = (&[sql!(
|
||||
CREATE TABLE welcome_pages (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
@@ -430,10 +436,11 @@ mod persistence {
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
),
|
||||
];
|
||||
)]);
|
||||
}
|
||||
|
||||
db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]);
|
||||
|
||||
impl WelcomePagesDb {
|
||||
query! {
|
||||
pub async fn save_welcome_page(
|
||||
|
||||
@@ -5102,9 +5102,9 @@ impl EventEmitter<PanelEvent> for OutlinePanel {}
|
||||
|
||||
impl Render for OutlinePanel {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (is_local, is_via_ssh) = self
|
||||
.project
|
||||
.read_with(cx, |project, _| (project.is_local(), project.is_via_ssh()));
|
||||
let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| {
|
||||
(project.is_local(), project.is_via_remote_server())
|
||||
});
|
||||
let query = self.query(cx);
|
||||
let pinned = self.pinned;
|
||||
let settings = OutlinePanelSettings::get_global(cx);
|
||||
|
||||
@@ -5,11 +5,8 @@ use super::{
|
||||
session::{self, Session, SessionStateEvent},
|
||||
};
|
||||
use crate::{
|
||||
InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
|
||||
debugger::session::SessionQuirks,
|
||||
project_settings::ProjectSettings,
|
||||
terminals::{SshCommand, wrap_for_ssh},
|
||||
worktree_store::WorktreeStore,
|
||||
InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, debugger::session::SessionQuirks,
|
||||
project_settings::ProjectSettings, worktree_store::WorktreeStore,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
@@ -34,7 +31,7 @@ use http_client::HttpClient;
|
||||
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
|
||||
use node_runtime::NodeRuntime;
|
||||
|
||||
use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs};
|
||||
use remote::RemoteClient;
|
||||
use rpc::{
|
||||
AnyProtoClient, TypedEnvelope,
|
||||
proto::{self},
|
||||
@@ -68,7 +65,7 @@ pub enum DapStoreEvent {
|
||||
|
||||
enum DapStoreMode {
|
||||
Local(LocalDapStore),
|
||||
Ssh(SshDapStore),
|
||||
Remote(RemoteDapStore),
|
||||
Collab,
|
||||
}
|
||||
|
||||
@@ -80,8 +77,8 @@ pub struct LocalDapStore {
|
||||
toolchain_store: Arc<dyn LanguageToolchainStore>,
|
||||
}
|
||||
|
||||
pub struct SshDapStore {
|
||||
ssh_client: Entity<SshRemoteClient>,
|
||||
pub struct RemoteDapStore {
|
||||
remote_client: Entity<RemoteClient>,
|
||||
upstream_client: AnyProtoClient,
|
||||
upstream_project_id: u64,
|
||||
}
|
||||
@@ -147,16 +144,16 @@ impl DapStore {
|
||||
Self::new(mode, breakpoint_store, worktree_store, cx)
|
||||
}
|
||||
|
||||
pub fn new_ssh(
|
||||
pub fn new_remote(
|
||||
project_id: u64,
|
||||
ssh_client: Entity<SshRemoteClient>,
|
||||
remote_client: Entity<RemoteClient>,
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let mode = DapStoreMode::Ssh(SshDapStore {
|
||||
upstream_client: ssh_client.read(cx).proto_client(),
|
||||
ssh_client,
|
||||
let mode = DapStoreMode::Remote(RemoteDapStore {
|
||||
upstream_client: remote_client.read(cx).proto_client(),
|
||||
remote_client,
|
||||
upstream_project_id: project_id,
|
||||
});
|
||||
|
||||
@@ -242,64 +239,51 @@ impl DapStore {
|
||||
Ok(binary)
|
||||
})
|
||||
}
|
||||
DapStoreMode::Ssh(ssh) => {
|
||||
let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
|
||||
session_id: session_id.to_proto(),
|
||||
project_id: ssh.upstream_project_id,
|
||||
worktree_id: worktree.read(cx).id().to_proto(),
|
||||
definition: Some(definition.to_proto()),
|
||||
});
|
||||
let ssh_client = ssh.ssh_client.clone();
|
||||
DapStoreMode::Remote(remote) => {
|
||||
let request = remote
|
||||
.upstream_client
|
||||
.request(proto::GetDebugAdapterBinary {
|
||||
session_id: session_id.to_proto(),
|
||||
project_id: remote.upstream_project_id,
|
||||
worktree_id: worktree.read(cx).id().to_proto(),
|
||||
definition: Some(definition.to_proto()),
|
||||
});
|
||||
let remote = remote.remote_client.clone();
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
let response = request.await?;
|
||||
let binary = DebugAdapterBinary::from_proto(response)?;
|
||||
let (mut ssh_command, envs, path_style, ssh_shell) =
|
||||
ssh_client.read_with(cx, |ssh, _| {
|
||||
let SshInfo {
|
||||
args: SshArgs { arguments, envs },
|
||||
path_style,
|
||||
shell,
|
||||
} = ssh.ssh_info().context("SSH arguments not found")?;
|
||||
anyhow::Ok((
|
||||
SshCommand { arguments },
|
||||
envs.unwrap_or_default(),
|
||||
path_style,
|
||||
shell,
|
||||
))
|
||||
})??;
|
||||
|
||||
let mut connection = None;
|
||||
let port_forwarding;
|
||||
let connection;
|
||||
if let Some(c) = binary.connection {
|
||||
let local_bind_addr = Ipv4Addr::LOCALHOST;
|
||||
let port =
|
||||
dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
|
||||
|
||||
ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
|
||||
let host = Ipv4Addr::LOCALHOST;
|
||||
let port = dap::transport::TcpTransport::unused_port(host).await?;
|
||||
port_forwarding = Some((port, c.host.to_string(), c.port));
|
||||
connection = Some(TcpArguments {
|
||||
port,
|
||||
host: local_bind_addr,
|
||||
host,
|
||||
timeout: c.timeout,
|
||||
})
|
||||
} else {
|
||||
port_forwarding = None;
|
||||
connection = None;
|
||||
}
|
||||
|
||||
let (program, args) = wrap_for_ssh(
|
||||
&ssh_shell,
|
||||
&ssh_command,
|
||||
binary
|
||||
.command
|
||||
.as_ref()
|
||||
.map(|command| (command, &binary.arguments)),
|
||||
binary.cwd.as_deref(),
|
||||
binary.envs,
|
||||
None,
|
||||
path_style,
|
||||
);
|
||||
let command = remote.read_with(cx, |remote, _cx| {
|
||||
remote.build_command(
|
||||
binary.command,
|
||||
&binary.arguments,
|
||||
&binary.envs,
|
||||
binary.cwd.map(|path| path.display().to_string()),
|
||||
port_forwarding,
|
||||
)
|
||||
})??;
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some(program),
|
||||
arguments: args,
|
||||
envs,
|
||||
command: Some(command.program),
|
||||
arguments: command.args,
|
||||
envs: command.env,
|
||||
cwd: None,
|
||||
connection,
|
||||
request_args: binary.request_args,
|
||||
@@ -365,9 +349,9 @@ impl DapStore {
|
||||
)))
|
||||
}
|
||||
}
|
||||
DapStoreMode::Ssh(ssh) => {
|
||||
let request = ssh.upstream_client.request(proto::RunDebugLocators {
|
||||
project_id: ssh.upstream_project_id,
|
||||
DapStoreMode::Remote(remote) => {
|
||||
let request = remote.upstream_client.request(proto::RunDebugLocators {
|
||||
project_id: remote.upstream_project_id,
|
||||
build_command: Some(build_command.to_proto()),
|
||||
locator: locator_name.to_owned(),
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ use parking_lot::Mutex;
|
||||
use postage::stream::Stream as _;
|
||||
use rpc::{
|
||||
AnyProtoClient, TypedEnvelope,
|
||||
proto::{self, FromProto, SSH_PROJECT_ID, ToProto, git_reset, split_repository_update},
|
||||
proto::{self, FromProto, ToProto, git_reset, split_repository_update},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
@@ -141,14 +141,10 @@ enum GitStoreState {
|
||||
project_environment: Entity<ProjectEnvironment>,
|
||||
fs: Arc<dyn Fs>,
|
||||
},
|
||||
Ssh {
|
||||
upstream_client: AnyProtoClient,
|
||||
upstream_project_id: ProjectId,
|
||||
downstream: Option<(AnyProtoClient, ProjectId)>,
|
||||
},
|
||||
Remote {
|
||||
upstream_client: AnyProtoClient,
|
||||
upstream_project_id: ProjectId,
|
||||
upstream_project_id: u64,
|
||||
downstream: Option<(AnyProtoClient, ProjectId)>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -355,7 +351,7 @@ impl GitStore {
|
||||
worktree_store: &Entity<WorktreeStore>,
|
||||
buffer_store: Entity<BufferStore>,
|
||||
upstream_client: AnyProtoClient,
|
||||
project_id: ProjectId,
|
||||
project_id: u64,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
@@ -364,23 +360,6 @@ impl GitStore {
|
||||
GitStoreState::Remote {
|
||||
upstream_client,
|
||||
upstream_project_id: project_id,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ssh(
|
||||
worktree_store: &Entity<WorktreeStore>,
|
||||
buffer_store: Entity<BufferStore>,
|
||||
upstream_client: AnyProtoClient,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
worktree_store.clone(),
|
||||
buffer_store,
|
||||
GitStoreState::Ssh {
|
||||
upstream_client,
|
||||
upstream_project_id: ProjectId(SSH_PROJECT_ID),
|
||||
downstream: None,
|
||||
},
|
||||
cx,
|
||||
@@ -451,7 +430,7 @@ impl GitStore {
|
||||
|
||||
pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
|
||||
match &mut self.state {
|
||||
GitStoreState::Ssh {
|
||||
GitStoreState::Remote {
|
||||
downstream: downstream_client,
|
||||
..
|
||||
} => {
|
||||
@@ -527,9 +506,6 @@ impl GitStore {
|
||||
}),
|
||||
});
|
||||
}
|
||||
GitStoreState::Remote { .. } => {
|
||||
debug_panic!("shared called on remote store");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,15 +517,12 @@ impl GitStore {
|
||||
} => {
|
||||
downstream_client.take();
|
||||
}
|
||||
GitStoreState::Ssh {
|
||||
GitStoreState::Remote {
|
||||
downstream: downstream_client,
|
||||
..
|
||||
} => {
|
||||
downstream_client.take();
|
||||
}
|
||||
GitStoreState::Remote { .. } => {
|
||||
debug_panic!("unshared called on remote store");
|
||||
}
|
||||
}
|
||||
self.shared_diffs.clear();
|
||||
}
|
||||
@@ -1047,21 +1020,17 @@ impl GitStore {
|
||||
} => downstream_client
|
||||
.as_ref()
|
||||
.map(|state| (state.client.clone(), state.project_id)),
|
||||
GitStoreState::Ssh {
|
||||
GitStoreState::Remote {
|
||||
downstream: downstream_client,
|
||||
..
|
||||
} => downstream_client.clone(),
|
||||
GitStoreState::Remote { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn upstream_client(&self) -> Option<AnyProtoClient> {
|
||||
match &self.state {
|
||||
GitStoreState::Local { .. } => None,
|
||||
GitStoreState::Ssh {
|
||||
upstream_client, ..
|
||||
}
|
||||
| GitStoreState::Remote {
|
||||
GitStoreState::Remote {
|
||||
upstream_client, ..
|
||||
} => Some(upstream_client.clone()),
|
||||
}
|
||||
@@ -1432,12 +1401,7 @@ impl GitStore {
|
||||
cx.background_executor()
|
||||
.spawn(async move { fs.git_init(&path, fallback_branch_name) })
|
||||
}
|
||||
GitStoreState::Ssh {
|
||||
upstream_client,
|
||||
upstream_project_id: project_id,
|
||||
..
|
||||
}
|
||||
| GitStoreState::Remote {
|
||||
GitStoreState::Remote {
|
||||
upstream_client,
|
||||
upstream_project_id: project_id,
|
||||
..
|
||||
@@ -1447,7 +1411,7 @@ impl GitStore {
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::GitInit {
|
||||
project_id: project_id.0,
|
||||
project_id: project_id,
|
||||
abs_path: path.to_string_lossy().to_string(),
|
||||
fallback_branch_name,
|
||||
})
|
||||
@@ -1471,13 +1435,18 @@ impl GitStore {
|
||||
cx.background_executor()
|
||||
.spawn(async move { fs.git_clone(&repo, &path).await })
|
||||
}
|
||||
GitStoreState::Ssh {
|
||||
GitStoreState::Remote {
|
||||
upstream_client,
|
||||
upstream_project_id,
|
||||
..
|
||||
} => {
|
||||
if upstream_client.is_via_collab() {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Git Clone isn't supported for project guests"
|
||||
)));
|
||||
}
|
||||
let request = upstream_client.request(proto::GitClone {
|
||||
project_id: upstream_project_id.0,
|
||||
project_id: *upstream_project_id,
|
||||
abs_path: path.to_string_lossy().to_string(),
|
||||
remote_repo: repo,
|
||||
});
|
||||
@@ -1491,9 +1460,6 @@ impl GitStore {
|
||||
}
|
||||
})
|
||||
}
|
||||
GitStoreState::Remote { .. } => {
|
||||
Task::ready(Err(anyhow!("Git Clone isn't supported for remote users")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11703,135 +11703,108 @@ impl LspStore {
|
||||
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
|
||||
}
|
||||
"workspace/symbol" => {
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.workspace_symbol_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"workspace/fileOperations" => {
|
||||
if let Some(options) = reg.register_options {
|
||||
let caps = serde_json::from_value(options)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities
|
||||
.workspace
|
||||
.get_or_insert_default()
|
||||
.file_operations = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let caps = parse_register_or_default(®)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities
|
||||
.workspace
|
||||
.get_or_insert_default()
|
||||
.file_operations = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"workspace/executeCommand" => {
|
||||
if let Some(options) = reg.register_options {
|
||||
let options = serde_json::from_value(options)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.execute_command_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let options = parse_register_or_default(®)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.execute_command_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/rangeFormatting" => {
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_range_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/onTypeFormatting" => {
|
||||
if let Some(options) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_on_type_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let options = parse_register_or_default(®)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_on_type_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/formatting" => {
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_formatting_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/rename" => {
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.rename_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/inlayHint" => {
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.inlay_hint_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/documentSymbol" => {
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.document_symbol_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/codeAction" => {
|
||||
if let Some(options) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.code_action_provider =
|
||||
Some(lsp::CodeActionProviderCapability::Options(options));
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let provider = parse_register_or(®, || {
|
||||
lsp::CodeActionProviderCapability::Simple(true)
|
||||
})?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.code_action_provider = Some(provider);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/definition" => {
|
||||
let options = parse_register_capabilities(reg)?;
|
||||
let options = parse_register_or(®, || OneOf::Left(true))?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.definition_provider = Some(options);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/completion" => {
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.completion_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let caps = parse_register_or_default(®)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.completion_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/hover" => {
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.hover_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let provider =
|
||||
parse_register_or(®, || lsp::HoverProviderCapability::Simple(true))?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.hover_provider = Some(provider);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/signatureHelp" => {
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.signature_help_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let caps = parse_register_or_default(®)?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.signature_help_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/didChange" => {
|
||||
if let Some(sync_kind) = reg
|
||||
@@ -11880,40 +11853,30 @@ impl LspStore {
|
||||
}
|
||||
}
|
||||
"textDocument/codeLens" => {
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.code_lens_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let caps = parse_register_or(®, || lsp::CodeLensOptions {
|
||||
resolve_provider: None,
|
||||
})?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.code_lens_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/diagnostic" => {
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.diagnostic_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let caps = parse_register_or(®, || {
|
||||
lsp::DiagnosticServerCapabilities::Options(lsp::DiagnosticOptions::default())
|
||||
})?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.diagnostic_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/documentColor" => {
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.color_provider = Some(caps);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
let provider =
|
||||
parse_register_or(®, || lsp::ColorProviderCapability::Simple(true))?;
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.color_provider = Some(provider);
|
||||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
_ => log::warn!("unhandled capability registration: {reg:?}"),
|
||||
}
|
||||
@@ -12170,17 +12133,27 @@ impl LspStore {
|
||||
}
|
||||
}
|
||||
|
||||
// Registration with registerOptions as null, should fallback to true.
|
||||
// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133
|
||||
fn parse_register_capabilities<T: serde::de::DeserializeOwned>(
|
||||
reg: lsp::Registration,
|
||||
) -> Result<OneOf<bool, T>> {
|
||||
Ok(match reg.register_options {
|
||||
Some(options) => OneOf::Right(serde_json::from_value::<T>(options)?),
|
||||
None => OneOf::Left(true),
|
||||
// Parse register_options into T or return a provided default if None.
|
||||
fn parse_register_or<T, F>(reg: &lsp::Registration, default: F) -> Result<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
Ok(match reg.register_options.as_ref() {
|
||||
Some(options) => serde_json::from_value::<T>(options.clone())?,
|
||||
None => default(),
|
||||
})
|
||||
}
|
||||
|
||||
// Parse register_options into T or default() if None.
|
||||
fn parse_register_or_default<T>(reg: &lsp::Registration) -> Result<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned + Default,
|
||||
{
|
||||
parse_register_or(reg, T::default)
|
||||
}
|
||||
|
||||
fn subscribe_to_binary_statuses(
|
||||
languages: &Arc<LanguageRegistry>,
|
||||
cx: &mut Context<'_, LspStore>,
|
||||
|
||||
@@ -42,9 +42,7 @@ pub use manifest_tree::ManifestTree;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use buffer_store::{BufferStore, BufferStoreEvent};
|
||||
use client::{
|
||||
Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore, proto,
|
||||
};
|
||||
use client::{Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore, proto};
|
||||
use clock::ReplicaId;
|
||||
|
||||
use dap::client::DebugAdapterClient;
|
||||
@@ -89,10 +87,10 @@ use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
pub use prettier_store::PrettierStore;
|
||||
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
|
||||
use remote::{SshConnectionOptions, SshRemoteClient};
|
||||
use remote::{RemoteClient, SshConnectionOptions};
|
||||
use rpc::{
|
||||
AnyProtoClient, ErrorCode,
|
||||
proto::{FromProto, LanguageServerPromptResponse, SSH_PROJECT_ID, ToProto},
|
||||
proto::{FromProto, LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID, ToProto},
|
||||
};
|
||||
use search::{SearchInputKind, SearchQuery, SearchResult};
|
||||
use search_history::SearchHistory;
|
||||
@@ -177,12 +175,12 @@ pub struct Project {
|
||||
dap_store: Entity<DapStore>,
|
||||
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
client: Arc<client::Client>,
|
||||
collab_client: Arc<client::Client>,
|
||||
join_project_response_message_id: u32,
|
||||
task_store: Entity<TaskStore>,
|
||||
user_store: Entity<UserStore>,
|
||||
fs: Arc<dyn Fs>,
|
||||
ssh_client: Option<Entity<SshRemoteClient>>,
|
||||
remote_client: Option<Entity<RemoteClient>>,
|
||||
client_state: ProjectClientState,
|
||||
git_store: Entity<GitStore>,
|
||||
collaborators: HashMap<proto::PeerId, Collaborator>,
|
||||
@@ -1154,12 +1152,12 @@ impl Project {
|
||||
active_entry: None,
|
||||
snippets,
|
||||
languages,
|
||||
client,
|
||||
collab_client: client,
|
||||
task_store,
|
||||
user_store,
|
||||
settings_observer,
|
||||
fs,
|
||||
ssh_client: None,
|
||||
remote_client: None,
|
||||
breakpoint_store,
|
||||
dap_store,
|
||||
|
||||
@@ -1183,8 +1181,8 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ssh(
|
||||
ssh: Entity<SshRemoteClient>,
|
||||
pub fn remote(
|
||||
remote: Entity<RemoteClient>,
|
||||
client: Arc<Client>,
|
||||
node: NodeRuntime,
|
||||
user_store: Entity<UserStore>,
|
||||
@@ -1200,10 +1198,15 @@ impl Project {
|
||||
let snippets =
|
||||
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
|
||||
|
||||
let (ssh_proto, path_style) =
|
||||
ssh.read_with(cx, |ssh, _| (ssh.proto_client(), ssh.path_style()));
|
||||
let (remote_proto, path_style) =
|
||||
remote.read_with(cx, |remote, _| (remote.proto_client(), remote.path_style()));
|
||||
let worktree_store = cx.new(|_| {
|
||||
WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID, path_style)
|
||||
WorktreeStore::remote(
|
||||
false,
|
||||
remote_proto.clone(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
path_style,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
@@ -1215,31 +1218,32 @@ impl Project {
|
||||
let buffer_store = cx.new(|cx| {
|
||||
BufferStore::remote(
|
||||
worktree_store.clone(),
|
||||
ssh.read(cx).proto_client(),
|
||||
SSH_PROJECT_ID,
|
||||
remote.read(cx).proto_client(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let image_store = cx.new(|cx| {
|
||||
ImageStore::remote(
|
||||
worktree_store.clone(),
|
||||
ssh.read(cx).proto_client(),
|
||||
SSH_PROJECT_ID,
|
||||
remote.read(cx).proto_client(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
|
||||
.detach();
|
||||
let toolchain_store = cx
|
||||
.new(|cx| ToolchainStore::remote(SSH_PROJECT_ID, ssh.read(cx).proto_client(), cx));
|
||||
let toolchain_store = cx.new(|cx| {
|
||||
ToolchainStore::remote(REMOTE_SERVER_PROJECT_ID, remote.read(cx).proto_client(), cx)
|
||||
});
|
||||
let task_store = cx.new(|cx| {
|
||||
TaskStore::remote(
|
||||
fs.clone(),
|
||||
buffer_store.downgrade(),
|
||||
worktree_store.clone(),
|
||||
toolchain_store.read(cx).as_language_toolchain_store(),
|
||||
ssh.read(cx).proto_client(),
|
||||
SSH_PROJECT_ID,
|
||||
remote.read(cx).proto_client(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1262,8 +1266,8 @@ impl Project {
|
||||
buffer_store.clone(),
|
||||
worktree_store.clone(),
|
||||
languages.clone(),
|
||||
ssh_proto.clone(),
|
||||
SSH_PROJECT_ID,
|
||||
remote_proto.clone(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
fs.clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -1271,12 +1275,12 @@ impl Project {
|
||||
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
|
||||
|
||||
let breakpoint_store =
|
||||
cx.new(|_| BreakpointStore::remote(SSH_PROJECT_ID, ssh_proto.clone()));
|
||||
cx.new(|_| BreakpointStore::remote(REMOTE_SERVER_PROJECT_ID, remote_proto.clone()));
|
||||
|
||||
let dap_store = cx.new(|cx| {
|
||||
DapStore::new_ssh(
|
||||
SSH_PROJECT_ID,
|
||||
ssh.clone(),
|
||||
DapStore::new_remote(
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
remote.clone(),
|
||||
breakpoint_store.clone(),
|
||||
worktree_store.clone(),
|
||||
cx,
|
||||
@@ -1284,10 +1288,16 @@ impl Project {
|
||||
});
|
||||
|
||||
let git_store = cx.new(|cx| {
|
||||
GitStore::ssh(&worktree_store, buffer_store.clone(), ssh_proto.clone(), cx)
|
||||
GitStore::remote(
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
remote_proto.clone(),
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.subscribe(&ssh, Self::on_ssh_event).detach();
|
||||
cx.subscribe(&remote, Self::on_remote_client_event).detach();
|
||||
|
||||
let this = Self {
|
||||
buffer_ordered_messages_tx: tx,
|
||||
@@ -1306,11 +1316,13 @@ impl Project {
|
||||
_subscriptions: vec![
|
||||
cx.on_release(Self::release),
|
||||
cx.on_app_quit(|this, cx| {
|
||||
let shutdown = this.ssh_client.take().and_then(|client| {
|
||||
client.read(cx).shutdown_processes(
|
||||
Some(proto::ShutdownRemoteServer {}),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
let shutdown = this.remote_client.take().and_then(|client| {
|
||||
client.update(cx, |client, cx| {
|
||||
client.shutdown_processes(
|
||||
Some(proto::ShutdownRemoteServer {}),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
@@ -1323,12 +1335,12 @@ impl Project {
|
||||
active_entry: None,
|
||||
snippets,
|
||||
languages,
|
||||
client,
|
||||
collab_client: client,
|
||||
task_store,
|
||||
user_store,
|
||||
settings_observer,
|
||||
fs,
|
||||
ssh_client: Some(ssh.clone()),
|
||||
remote_client: Some(remote.clone()),
|
||||
buffers_needing_diff: Default::default(),
|
||||
git_diff_debouncer: DebouncedDelay::new(),
|
||||
terminals: Terminals {
|
||||
@@ -1346,52 +1358,34 @@ impl Project {
|
||||
agent_location: None,
|
||||
};
|
||||
|
||||
// ssh -> local machine handlers
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity());
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store);
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store);
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store);
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer);
|
||||
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store);
|
||||
// remote server -> local machine handlers
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity());
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.buffer_store);
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.worktree_store);
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.lsp_store);
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.dap_store);
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.settings_observer);
|
||||
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.git_store);
|
||||
|
||||
ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_update_worktree);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_update_project);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_toast);
|
||||
ssh_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_hide_toast);
|
||||
ssh_proto.add_entity_request_handler(Self::handle_update_buffer_from_ssh);
|
||||
BufferStore::init(&ssh_proto);
|
||||
LspStore::init(&ssh_proto);
|
||||
SettingsObserver::init(&ssh_proto);
|
||||
TaskStore::init(Some(&ssh_proto));
|
||||
ToolchainStore::init(&ssh_proto);
|
||||
DapStore::init(&ssh_proto, cx);
|
||||
GitStore::init(&ssh_proto);
|
||||
remote_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer);
|
||||
remote_proto.add_entity_message_handler(Self::handle_update_worktree);
|
||||
remote_proto.add_entity_message_handler(Self::handle_update_project);
|
||||
remote_proto.add_entity_message_handler(Self::handle_toast);
|
||||
remote_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
|
||||
remote_proto.add_entity_message_handler(Self::handle_hide_toast);
|
||||
remote_proto.add_entity_request_handler(Self::handle_update_buffer_from_remote_server);
|
||||
BufferStore::init(&remote_proto);
|
||||
LspStore::init(&remote_proto);
|
||||
SettingsObserver::init(&remote_proto);
|
||||
TaskStore::init(Some(&remote_proto));
|
||||
ToolchainStore::init(&remote_proto);
|
||||
DapStore::init(&remote_proto, cx);
|
||||
GitStore::init(&remote_proto);
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn remote(
|
||||
remote_id: u64,
|
||||
client: Arc<Client>,
|
||||
user_store: Entity<UserStore>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Entity<Self>> {
|
||||
let project =
|
||||
Self::in_room(remote_id, client, user_store, languages, fs, cx.clone()).await?;
|
||||
cx.update(|cx| {
|
||||
connection_manager::Manager::global(cx).update(cx, |manager, cx| {
|
||||
manager.maintain_project_connection(&project, cx)
|
||||
})
|
||||
})?;
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
pub async fn in_room(
|
||||
remote_id: u64,
|
||||
client: Arc<Client>,
|
||||
@@ -1523,7 +1517,7 @@ impl Project {
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
client.clone().into(),
|
||||
ProjectId(remote_id),
|
||||
remote_id,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -1574,11 +1568,11 @@ impl Project {
|
||||
task_store,
|
||||
snippets,
|
||||
fs,
|
||||
ssh_client: None,
|
||||
remote_client: None,
|
||||
settings_observer: settings_observer.clone(),
|
||||
client_subscriptions: Default::default(),
|
||||
_subscriptions: vec![cx.on_release(Self::release)],
|
||||
client: client.clone(),
|
||||
collab_client: client.clone(),
|
||||
client_state: ProjectClientState::Remote {
|
||||
sharing_has_stopped: false,
|
||||
capability: Capability::ReadWrite,
|
||||
@@ -1661,11 +1655,13 @@ impl Project {
|
||||
}
|
||||
|
||||
fn release(&mut self, cx: &mut App) {
|
||||
if let Some(client) = self.ssh_client.take() {
|
||||
let shutdown = client.read(cx).shutdown_processes(
|
||||
Some(proto::ShutdownRemoteServer {}),
|
||||
cx.background_executor().clone(),
|
||||
);
|
||||
if let Some(client) = self.remote_client.take() {
|
||||
let shutdown = client.update(cx, |client, cx| {
|
||||
client.shutdown_processes(
|
||||
Some(proto::ShutdownRemoteServer {}),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Some(shutdown) = shutdown {
|
||||
@@ -1681,7 +1677,7 @@ impl Project {
|
||||
let _ = self.unshare_internal(cx);
|
||||
}
|
||||
ProjectClientState::Remote { remote_id, .. } => {
|
||||
let _ = self.client.send(proto::LeaveProject {
|
||||
let _ = self.collab_client.send(proto::LeaveProject {
|
||||
project_id: *remote_id,
|
||||
});
|
||||
self.disconnected_from_host_internal(cx);
|
||||
@@ -1808,11 +1804,11 @@ impl Project {
|
||||
}
|
||||
|
||||
pub fn client(&self) -> Arc<Client> {
|
||||
self.client.clone()
|
||||
self.collab_client.clone()
|
||||
}
|
||||
|
||||
pub fn ssh_client(&self) -> Option<Entity<SshRemoteClient>> {
|
||||
self.ssh_client.clone()
|
||||
pub fn remote_client(&self) -> Option<Entity<RemoteClient>> {
|
||||
self.remote_client.clone()
|
||||
}
|
||||
|
||||
pub fn user_store(&self) -> Entity<UserStore> {
|
||||
@@ -1893,30 +1889,30 @@ impl Project {
|
||||
if self.is_local() {
|
||||
return true;
|
||||
}
|
||||
if self.is_via_ssh() {
|
||||
if self.is_via_remote_server() {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn ssh_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> {
|
||||
self.ssh_client
|
||||
pub fn remote_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> {
|
||||
self.remote_client
|
||||
.as_ref()
|
||||
.map(|ssh| ssh.read(cx).connection_state())
|
||||
.map(|remote| remote.read(cx).connection_state())
|
||||
}
|
||||
|
||||
pub fn ssh_connection_options(&self, cx: &App) -> Option<SshConnectionOptions> {
|
||||
self.ssh_client
|
||||
pub fn remote_connection_options(&self, cx: &App) -> Option<SshConnectionOptions> {
|
||||
self.remote_client
|
||||
.as_ref()
|
||||
.map(|ssh| ssh.read(cx).connection_options())
|
||||
.map(|remote| remote.read(cx).connection_options())
|
||||
}
|
||||
|
||||
pub fn replica_id(&self) -> ReplicaId {
|
||||
match self.client_state {
|
||||
ProjectClientState::Remote { replica_id, .. } => replica_id,
|
||||
_ => {
|
||||
if self.ssh_client.is_some() {
|
||||
if self.remote_client.is_some() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
@@ -2220,55 +2216,55 @@ impl Project {
|
||||
);
|
||||
|
||||
self.client_subscriptions.extend([
|
||||
self.client
|
||||
self.collab_client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&cx.entity(), &cx.to_async()),
|
||||
self.client
|
||||
self.collab_client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.worktree_store, &cx.to_async()),
|
||||
self.client
|
||||
self.collab_client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.buffer_store, &cx.to_async()),
|
||||
self.client
|
||||
self.collab_client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.lsp_store, &cx.to_async()),
|
||||
self.client
|
||||
self.collab_client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.settings_observer, &cx.to_async()),
|
||||
self.client
|
||||
self.collab_client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.dap_store, &cx.to_async()),
|
||||
self.client
|
||||
self.collab_client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.breakpoint_store, &cx.to_async()),
|
||||
self.client
|
||||
self.collab_client
|
||||
.subscribe_to_entity(project_id)?
|
||||
.set_entity(&self.git_store, &cx.to_async()),
|
||||
]);
|
||||
|
||||
self.buffer_store.update(cx, |buffer_store, cx| {
|
||||
buffer_store.shared(project_id, self.client.clone().into(), cx)
|
||||
buffer_store.shared(project_id, self.collab_client.clone().into(), cx)
|
||||
});
|
||||
self.worktree_store.update(cx, |worktree_store, cx| {
|
||||
worktree_store.shared(project_id, self.client.clone().into(), cx);
|
||||
worktree_store.shared(project_id, self.collab_client.clone().into(), cx);
|
||||
});
|
||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.shared(project_id, self.client.clone().into(), cx)
|
||||
lsp_store.shared(project_id, self.collab_client.clone().into(), cx)
|
||||
});
|
||||
self.breakpoint_store.update(cx, |breakpoint_store, _| {
|
||||
breakpoint_store.shared(project_id, self.client.clone().into())
|
||||
breakpoint_store.shared(project_id, self.collab_client.clone().into())
|
||||
});
|
||||
self.dap_store.update(cx, |dap_store, cx| {
|
||||
dap_store.shared(project_id, self.client.clone().into(), cx);
|
||||
dap_store.shared(project_id, self.collab_client.clone().into(), cx);
|
||||
});
|
||||
self.task_store.update(cx, |task_store, cx| {
|
||||
task_store.shared(project_id, self.client.clone().into(), cx);
|
||||
task_store.shared(project_id, self.collab_client.clone().into(), cx);
|
||||
});
|
||||
self.settings_observer.update(cx, |settings_observer, cx| {
|
||||
settings_observer.shared(project_id, self.client.clone().into(), cx)
|
||||
settings_observer.shared(project_id, self.collab_client.clone().into(), cx)
|
||||
});
|
||||
self.git_store.update(cx, |git_store, cx| {
|
||||
git_store.shared(project_id, self.client.clone().into(), cx)
|
||||
git_store.shared(project_id, self.collab_client.clone().into(), cx)
|
||||
});
|
||||
|
||||
self.client_state = ProjectClientState::Shared {
|
||||
@@ -2293,7 +2289,7 @@ impl Project {
|
||||
});
|
||||
if let Some(remote_id) = self.remote_id() {
|
||||
self.git_store.update(cx, |git_store, cx| {
|
||||
git_store.shared(remote_id, self.client.clone().into(), cx)
|
||||
git_store.shared(remote_id, self.collab_client.clone().into(), cx)
|
||||
});
|
||||
}
|
||||
cx.emit(Event::Reshared);
|
||||
@@ -2370,7 +2366,7 @@ impl Project {
|
||||
git_store.unshared(cx);
|
||||
});
|
||||
|
||||
self.client
|
||||
self.collab_client
|
||||
.send(proto::UnshareProject {
|
||||
project_id: remote_id,
|
||||
})
|
||||
@@ -2437,15 +2433,17 @@ impl Project {
|
||||
sharing_has_stopped,
|
||||
..
|
||||
} => *sharing_has_stopped,
|
||||
ProjectClientState::Local if self.is_via_ssh() => self.ssh_is_disconnected(cx),
|
||||
ProjectClientState::Local if self.is_via_remote_server() => {
|
||||
self.remote_client_is_disconnected(cx)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn ssh_is_disconnected(&self, cx: &App) -> bool {
|
||||
self.ssh_client
|
||||
fn remote_client_is_disconnected(&self, cx: &App) -> bool {
|
||||
self.remote_client
|
||||
.as_ref()
|
||||
.map(|ssh| ssh.read(cx).is_disconnected())
|
||||
.map(|remote| remote.read(cx).is_disconnected())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -2463,16 +2461,16 @@ impl Project {
|
||||
pub fn is_local(&self) -> bool {
|
||||
match &self.client_state {
|
||||
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
|
||||
self.ssh_client.is_none()
|
||||
self.remote_client.is_none()
|
||||
}
|
||||
ProjectClientState::Remote { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_via_ssh(&self) -> bool {
|
||||
pub fn is_via_remote_server(&self) -> bool {
|
||||
match &self.client_state {
|
||||
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
|
||||
self.ssh_client.is_some()
|
||||
self.remote_client.is_some()
|
||||
}
|
||||
ProjectClientState::Remote { .. } => false,
|
||||
}
|
||||
@@ -2496,7 +2494,7 @@ impl Project {
|
||||
language: Option<Arc<Language>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<Buffer> {
|
||||
if self.is_via_collab() || self.is_via_ssh() {
|
||||
if self.is_via_collab() || self.is_via_remote_server() {
|
||||
panic!("called create_local_buffer on a remote project")
|
||||
}
|
||||
self.buffer_store.update(cx, |buffer_store, cx| {
|
||||
@@ -2620,10 +2618,10 @@ impl Project {
|
||||
) -> Task<Result<Entity<Buffer>>> {
|
||||
if let Some(buffer) = self.buffer_for_id(id, cx) {
|
||||
Task::ready(Ok(buffer))
|
||||
} else if self.is_local() || self.is_via_ssh() {
|
||||
} else if self.is_local() || self.is_via_remote_server() {
|
||||
Task::ready(Err(anyhow!("buffer {id} does not exist")))
|
||||
} else if let Some(project_id) = self.remote_id() {
|
||||
let request = self.client.request(proto::OpenBufferById {
|
||||
let request = self.collab_client.request(proto::OpenBufferById {
|
||||
project_id,
|
||||
id: id.into(),
|
||||
});
|
||||
@@ -2741,7 +2739,7 @@ impl Project {
|
||||
for (buffer_id, operations) in operations_by_buffer_id.drain() {
|
||||
let request = this.read_with(cx, |this, _| {
|
||||
let project_id = this.remote_id()?;
|
||||
Some(this.client.request(proto::UpdateBuffer {
|
||||
Some(this.collab_client.request(proto::UpdateBuffer {
|
||||
buffer_id: buffer_id.into(),
|
||||
project_id,
|
||||
operations,
|
||||
@@ -2808,7 +2806,7 @@ impl Project {
|
||||
project.read_with(cx, |project, _| {
|
||||
if let Some(project_id) = project.remote_id() {
|
||||
project
|
||||
.client
|
||||
.collab_client
|
||||
.send(proto::UpdateLanguageServer {
|
||||
project_id,
|
||||
server_name: name.map(|name| String::from(name.0)),
|
||||
@@ -2846,8 +2844,8 @@ impl Project {
|
||||
self.register_buffer(buffer, cx).log_err();
|
||||
}
|
||||
BufferStoreEvent::BufferDropped(buffer_id) => {
|
||||
if let Some(ref ssh_client) = self.ssh_client {
|
||||
ssh_client
|
||||
if let Some(ref remote_client) = self.remote_client {
|
||||
remote_client
|
||||
.read(cx)
|
||||
.proto_client()
|
||||
.send(proto::CloseBuffer {
|
||||
@@ -2995,16 +2993,14 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ssh_event(
|
||||
fn on_remote_client_event(
|
||||
&mut self,
|
||||
_: Entity<SshRemoteClient>,
|
||||
event: &remote::SshRemoteEvent,
|
||||
_: Entity<RemoteClient>,
|
||||
event: &remote::RemoteClientEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
remote::SshRemoteEvent::Disconnected => {
|
||||
// if self.is_via_ssh() {
|
||||
// self.collaborators.clear();
|
||||
remote::RemoteClientEvent::Disconnected => {
|
||||
self.worktree_store.update(cx, |store, cx| {
|
||||
store.disconnected_from_host(cx);
|
||||
});
|
||||
@@ -3110,8 +3106,9 @@ impl Project {
|
||||
}
|
||||
|
||||
fn on_worktree_released(&mut self, id_to_remove: WorktreeId, cx: &mut Context<Self>) {
|
||||
if let Some(ssh) = &self.ssh_client {
|
||||
ssh.read(cx)
|
||||
if let Some(remote) = &self.remote_client {
|
||||
remote
|
||||
.read(cx)
|
||||
.proto_client()
|
||||
.send(proto::RemoveWorktree {
|
||||
worktree_id: id_to_remove.to_proto(),
|
||||
@@ -3144,8 +3141,9 @@ impl Project {
|
||||
} => {
|
||||
let operation = language::proto::serialize_operation(operation);
|
||||
|
||||
if let Some(ssh) = &self.ssh_client {
|
||||
ssh.read(cx)
|
||||
if let Some(remote) = &self.remote_client {
|
||||
remote
|
||||
.read(cx)
|
||||
.proto_client()
|
||||
.send(proto::UpdateBuffer {
|
||||
project_id: 0,
|
||||
@@ -3552,16 +3550,16 @@ impl Project {
|
||||
|
||||
pub fn open_server_settings(&mut self, cx: &mut Context<Self>) -> Task<Result<Entity<Buffer>>> {
|
||||
let guard = self.retain_remotely_created_models(cx);
|
||||
let Some(ssh_client) = self.ssh_client.as_ref() else {
|
||||
let Some(remote) = self.remote_client.as_ref() else {
|
||||
return Task::ready(Err(anyhow!("not an ssh project")));
|
||||
};
|
||||
|
||||
let proto_client = ssh_client.read(cx).proto_client();
|
||||
let proto_client = remote.read(cx).proto_client();
|
||||
|
||||
cx.spawn(async move |project, cx| {
|
||||
let buffer = proto_client
|
||||
.request(proto::OpenServerSettings {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
project_id: REMOTE_SERVER_PROJECT_ID,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -3948,10 +3946,11 @@ impl Project {
|
||||
) -> Receiver<Entity<Buffer>> {
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
|
||||
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client {
|
||||
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.remote_client
|
||||
{
|
||||
(ssh_client.read(cx).proto_client(), 0)
|
||||
} else if let Some(remote_id) = self.remote_id() {
|
||||
(self.client.clone().into(), remote_id)
|
||||
(self.collab_client.clone().into(), remote_id)
|
||||
} else {
|
||||
return rx;
|
||||
};
|
||||
@@ -4095,14 +4094,14 @@ impl Project {
|
||||
is_dir: metadata.is_dir,
|
||||
})
|
||||
})
|
||||
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
|
||||
} else if let Some(ssh_client) = self.remote_client.as_ref() {
|
||||
let path_style = ssh_client.read(cx).path_style();
|
||||
let request_path = RemotePathBuf::from_str(path, path_style);
|
||||
let request = ssh_client
|
||||
.read(cx)
|
||||
.proto_client()
|
||||
.request(proto::GetPathMetadata {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
project_id: REMOTE_SERVER_PROJECT_ID,
|
||||
path: request_path.to_proto(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
@@ -4202,10 +4201,10 @@ impl Project {
|
||||
) -> Task<Result<Vec<DirectoryItem>>> {
|
||||
if self.is_local() {
|
||||
DirectoryLister::Local(cx.entity(), self.fs.clone()).list_directory(query, cx)
|
||||
} else if let Some(session) = self.ssh_client.as_ref() {
|
||||
} else if let Some(session) = self.remote_client.as_ref() {
|
||||
let path_buf = PathBuf::from(query);
|
||||
let request = proto::ListRemoteDirectory {
|
||||
dev_server_id: SSH_PROJECT_ID,
|
||||
dev_server_id: REMOTE_SERVER_PROJECT_ID,
|
||||
path: path_buf.to_proto(),
|
||||
config: Some(proto::ListRemoteDirectoryConfig { is_dir: true }),
|
||||
};
|
||||
@@ -4420,7 +4419,7 @@ impl Project {
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.is_local() || this.is_via_ssh() {
|
||||
if this.is_local() || this.is_via_remote_server() {
|
||||
this.unshare(cx)?;
|
||||
} else {
|
||||
this.disconnected_from_host(cx);
|
||||
@@ -4629,7 +4628,7 @@ impl Project {
|
||||
})?
|
||||
}
|
||||
|
||||
async fn handle_update_buffer_from_ssh(
|
||||
async fn handle_update_buffer_from_remote_server(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateBuffer>,
|
||||
cx: AsyncApp,
|
||||
@@ -4638,7 +4637,7 @@ impl Project {
|
||||
if let Some(remote_id) = this.remote_id() {
|
||||
let mut payload = envelope.payload.clone();
|
||||
payload.project_id = remote_id;
|
||||
cx.background_spawn(this.client.request(payload))
|
||||
cx.background_spawn(this.collab_client.request(payload))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
this.buffer_store.clone()
|
||||
@@ -4652,9 +4651,9 @@ impl Project {
|
||||
cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let buffer_store = this.read_with(&cx, |this, cx| {
|
||||
if let Some(ssh) = &this.ssh_client {
|
||||
if let Some(ssh) = &this.remote_client {
|
||||
let mut payload = envelope.payload.clone();
|
||||
payload.project_id = SSH_PROJECT_ID;
|
||||
payload.project_id = REMOTE_SERVER_PROJECT_ID;
|
||||
cx.background_spawn(ssh.read(cx).proto_client().request(payload))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -4704,7 +4703,7 @@ impl Project {
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::SynchronizeBuffersResponse> {
|
||||
let response = this.update(&mut cx, |this, cx| {
|
||||
let client = this.client.clone();
|
||||
let client = this.collab_client.clone();
|
||||
this.buffer_store.update(cx, |this, cx| {
|
||||
this.handle_synchronize_buffers(envelope, cx, client)
|
||||
})
|
||||
@@ -4841,7 +4840,7 @@ impl Project {
|
||||
}
|
||||
};
|
||||
|
||||
let client = self.client.clone();
|
||||
let client = self.collab_client.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (buffers, incomplete_buffer_ids) = this.update(cx, |this, cx| {
|
||||
this.buffer_store.read(cx).buffer_version_info(cx)
|
||||
|
||||
@@ -2,13 +2,11 @@ use crate::{Project, ProjectPath};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
|
||||
use itertools::Itertools;
|
||||
use language::LanguageName;
|
||||
use remote::{SshInfo, ssh_session::SshArgs};
|
||||
use remote::RemoteClient;
|
||||
use settings::{Settings, SettingsLocation};
|
||||
use smol::channel::bounded;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
env::{self},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
@@ -18,10 +16,7 @@ use terminal::{
|
||||
TaskState, TaskStatus, Terminal, TerminalBuilder,
|
||||
terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings},
|
||||
};
|
||||
use util::{
|
||||
ResultExt,
|
||||
paths::{PathStyle, RemotePathBuf},
|
||||
};
|
||||
use util::{ResultExt, paths::RemotePathBuf};
|
||||
|
||||
/// The directory inside a Python virtual environment that contains executables
|
||||
const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") {
|
||||
@@ -44,29 +39,6 @@ pub enum TerminalKind {
|
||||
Task(SpawnInTerminal),
|
||||
}
|
||||
|
||||
/// SshCommand describes how to connect to a remote server
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SshCommand {
|
||||
pub arguments: Vec<String>,
|
||||
}
|
||||
|
||||
impl SshCommand {
|
||||
pub fn add_port_forwarding(&mut self, local_port: u16, host: String, remote_port: u16) {
|
||||
self.arguments.push("-L".to_string());
|
||||
self.arguments
|
||||
.push(format!("{}:{}:{}", local_port, host, remote_port));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SshDetails {
|
||||
pub host: String,
|
||||
pub ssh_command: SshCommand,
|
||||
pub envs: Option<HashMap<String, String>>,
|
||||
pub path_style: PathStyle,
|
||||
pub shell: String,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
|
||||
self.active_entry()
|
||||
@@ -86,28 +58,6 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ssh_details(&self, cx: &App) -> Option<SshDetails> {
|
||||
if let Some(ssh_client) = &self.ssh_client {
|
||||
let ssh_client = ssh_client.read(cx);
|
||||
if let Some(SshInfo {
|
||||
args: SshArgs { arguments, envs },
|
||||
path_style,
|
||||
shell,
|
||||
}) = ssh_client.ssh_info()
|
||||
{
|
||||
return Some(SshDetails {
|
||||
host: ssh_client.connection_options().host,
|
||||
ssh_command: SshCommand { arguments },
|
||||
envs,
|
||||
path_style,
|
||||
shell,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn create_terminal(
|
||||
&mut self,
|
||||
kind: TerminalKind,
|
||||
@@ -168,14 +118,14 @@ impl Project {
|
||||
TerminalSettings::get(settings_location, cx)
|
||||
}
|
||||
|
||||
pub fn exec_in_shell(&self, command: String, cx: &App) -> std::process::Command {
|
||||
pub fn exec_in_shell(&self, command: String, cx: &App) -> Result<std::process::Command> {
|
||||
let path = self.first_project_directory(cx);
|
||||
let ssh_details = self.ssh_details(cx);
|
||||
let remote_client = self.remote_client.as_ref();
|
||||
let settings = self.terminal_settings(&path, cx).clone();
|
||||
|
||||
let builder =
|
||||
ShellBuilder::new(ssh_details.as_ref().map(|ssh| &*ssh.shell), &settings.shell)
|
||||
.non_interactive();
|
||||
let remote_shell = remote_client
|
||||
.as_ref()
|
||||
.and_then(|remote_client| remote_client.read(cx).shell());
|
||||
let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive();
|
||||
let (command, args) = builder.build(Some(command), &Vec::new());
|
||||
|
||||
let mut env = self
|
||||
@@ -185,29 +135,16 @@ impl Project {
|
||||
.unwrap_or_default();
|
||||
env.extend(settings.env);
|
||||
|
||||
match self.ssh_details(cx) {
|
||||
Some(SshDetails {
|
||||
ssh_command,
|
||||
envs,
|
||||
path_style,
|
||||
shell,
|
||||
..
|
||||
}) => {
|
||||
let (command, args) = wrap_for_ssh(
|
||||
&shell,
|
||||
&ssh_command,
|
||||
Some((&command, &args)),
|
||||
path.as_deref(),
|
||||
env,
|
||||
None,
|
||||
path_style,
|
||||
);
|
||||
let mut command = std::process::Command::new(command);
|
||||
command.args(args);
|
||||
if let Some(envs) = envs {
|
||||
command.envs(envs);
|
||||
}
|
||||
command
|
||||
match remote_client {
|
||||
Some(remote_client) => {
|
||||
let command_template =
|
||||
remote_client
|
||||
.read(cx)
|
||||
.build_command(Some(command), &args, &env, None, None)?;
|
||||
let mut command = std::process::Command::new(command_template.program);
|
||||
command.args(command_template.args);
|
||||
command.envs(command_template.env);
|
||||
Ok(command)
|
||||
}
|
||||
None => {
|
||||
let mut command = std::process::Command::new(command);
|
||||
@@ -216,7 +153,7 @@ impl Project {
|
||||
if let Some(path) = path {
|
||||
command.current_dir(path);
|
||||
}
|
||||
command
|
||||
Ok(command)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,13 +164,13 @@ impl Project {
|
||||
python_venv_directory: Option<PathBuf>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<Entity<Terminal>> {
|
||||
let this = &mut *self;
|
||||
let ssh_details = this.ssh_details(cx);
|
||||
let is_via_remote = self.remote_client.is_some();
|
||||
|
||||
let path: Option<Arc<Path>> = match &kind {
|
||||
TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())),
|
||||
TerminalKind::Task(spawn_task) => {
|
||||
if let Some(cwd) = &spawn_task.cwd {
|
||||
if ssh_details.is_some() {
|
||||
if is_via_remote {
|
||||
Some(Arc::from(cwd.as_ref()))
|
||||
} else {
|
||||
let cwd = cwd.to_string_lossy();
|
||||
@@ -241,16 +178,14 @@ impl Project {
|
||||
Some(Arc::from(Path::new(tilde_substituted.as_ref())))
|
||||
}
|
||||
} else {
|
||||
this.active_project_directory(cx)
|
||||
self.active_project_directory(cx)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let is_ssh_terminal = ssh_details.is_some();
|
||||
|
||||
let mut settings_location = None;
|
||||
if let Some(path) = path.as_ref()
|
||||
&& let Some((worktree, _)) = this.find_worktree(path, cx)
|
||||
&& let Some((worktree, _)) = self.find_worktree(path, cx)
|
||||
{
|
||||
settings_location = Some(SettingsLocation {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
@@ -262,7 +197,7 @@ impl Project {
|
||||
let (completion_tx, completion_rx) = bounded(1);
|
||||
|
||||
// Start with the environment that we might have inherited from the Zed CLI.
|
||||
let mut env = this
|
||||
let mut env = self
|
||||
.environment
|
||||
.read(cx)
|
||||
.get_cli_environment()
|
||||
@@ -271,14 +206,17 @@ impl Project {
|
||||
// precedence.
|
||||
env.extend(settings.env);
|
||||
|
||||
let local_path = if is_ssh_terminal { None } else { path.clone() };
|
||||
let local_path = if is_via_remote { None } else { path.clone() };
|
||||
|
||||
let mut python_venv_activate_command = Task::ready(None);
|
||||
|
||||
let (spawn_task, shell) = match kind {
|
||||
let remote_client = self.remote_client.clone();
|
||||
let spawn_task;
|
||||
let shell;
|
||||
match kind {
|
||||
TerminalKind::Shell(_) => {
|
||||
if let Some(python_venv_directory) = &python_venv_directory {
|
||||
python_venv_activate_command = this.python_activate_command(
|
||||
python_venv_activate_command = self.python_activate_command(
|
||||
python_venv_directory,
|
||||
&settings.detect_venv,
|
||||
&settings.shell,
|
||||
@@ -286,63 +224,16 @@ impl Project {
|
||||
);
|
||||
}
|
||||
|
||||
match ssh_details {
|
||||
Some(SshDetails {
|
||||
host,
|
||||
ssh_command,
|
||||
envs,
|
||||
path_style,
|
||||
shell,
|
||||
}) => {
|
||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||
|
||||
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
|
||||
// to properly display colors.
|
||||
// We do not have the luxury of assuming the host has it installed,
|
||||
// so we set it to a default that does not break the highlighting via ssh.
|
||||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
|
||||
let (program, args) = wrap_for_ssh(
|
||||
&shell,
|
||||
&ssh_command,
|
||||
None,
|
||||
path.as_deref(),
|
||||
env,
|
||||
None,
|
||||
path_style,
|
||||
);
|
||||
env = HashMap::default();
|
||||
if let Some(envs) = envs {
|
||||
env.extend(envs);
|
||||
}
|
||||
(
|
||||
Option::<TaskState>::None,
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args,
|
||||
title_override: Some(format!("{} — Terminal", host).into()),
|
||||
},
|
||||
)
|
||||
spawn_task = None;
|
||||
shell = match remote_client {
|
||||
Some(remote_client) => {
|
||||
create_remote_shell(None, &mut env, path, remote_client, cx)?
|
||||
}
|
||||
None => (None, settings.shell),
|
||||
}
|
||||
None => settings.shell,
|
||||
};
|
||||
}
|
||||
TerminalKind::Task(spawn_task) => {
|
||||
let task_state = Some(TaskState {
|
||||
id: spawn_task.id,
|
||||
full_label: spawn_task.full_label,
|
||||
label: spawn_task.label,
|
||||
command_label: spawn_task.command_label,
|
||||
hide: spawn_task.hide,
|
||||
status: TaskStatus::Running,
|
||||
show_summary: spawn_task.show_summary,
|
||||
show_command: spawn_task.show_command,
|
||||
show_rerun: spawn_task.show_rerun,
|
||||
completion_rx,
|
||||
});
|
||||
|
||||
env.extend(spawn_task.env);
|
||||
TerminalKind::Task(task) => {
|
||||
env.extend(task.env);
|
||||
|
||||
if let Some(venv_path) = &python_venv_directory {
|
||||
env.insert(
|
||||
@@ -351,41 +242,38 @@ impl Project {
|
||||
);
|
||||
}
|
||||
|
||||
match ssh_details {
|
||||
Some(SshDetails {
|
||||
host,
|
||||
ssh_command,
|
||||
envs,
|
||||
path_style,
|
||||
shell,
|
||||
}) => {
|
||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
let (program, args) = wrap_for_ssh(
|
||||
&shell,
|
||||
&ssh_command,
|
||||
spawn_task
|
||||
.command
|
||||
.as_ref()
|
||||
.map(|command| (command, &spawn_task.args)),
|
||||
path.as_deref(),
|
||||
env,
|
||||
python_venv_directory.as_deref(),
|
||||
path_style,
|
||||
);
|
||||
env = HashMap::default();
|
||||
if let Some(envs) = envs {
|
||||
env.extend(envs);
|
||||
spawn_task = Some(TaskState {
|
||||
id: task.id,
|
||||
full_label: task.full_label,
|
||||
label: task.label,
|
||||
command_label: task.command_label,
|
||||
hide: task.hide,
|
||||
status: TaskStatus::Running,
|
||||
show_summary: task.show_summary,
|
||||
show_command: task.show_command,
|
||||
show_rerun: task.show_rerun,
|
||||
completion_rx,
|
||||
});
|
||||
shell = match remote_client {
|
||||
Some(remote_client) => {
|
||||
let path_style = remote_client.read(cx).path_style();
|
||||
if let Some(venv_directory) = &python_venv_directory
|
||||
&& let Ok(str) =
|
||||
shlex::try_quote(venv_directory.to_string_lossy().as_ref())
|
||||
{
|
||||
let path =
|
||||
RemotePathBuf::new(PathBuf::from(str.to_string()), path_style)
|
||||
.to_string();
|
||||
env.insert("PATH".into(), format!("{}:$PATH ", path));
|
||||
}
|
||||
(
|
||||
task_state,
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args,
|
||||
title_override: Some(format!("{} — Terminal", host).into()),
|
||||
},
|
||||
)
|
||||
|
||||
create_remote_shell(
|
||||
task.command.as_ref().map(|command| (command, &task.args)),
|
||||
&mut env,
|
||||
path,
|
||||
remote_client,
|
||||
cx,
|
||||
)?
|
||||
}
|
||||
None => {
|
||||
if let Some(venv_path) = &python_venv_directory {
|
||||
@@ -393,18 +281,17 @@ impl Project {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
let shell = if let Some(program) = spawn_task.command {
|
||||
if let Some(program) = task.command {
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args: spawn_task.args,
|
||||
args: task.args,
|
||||
title_override: None,
|
||||
}
|
||||
} else {
|
||||
Shell::System
|
||||
};
|
||||
(task_state, shell)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
TerminalBuilder::new(
|
||||
@@ -416,7 +303,7 @@ impl Project {
|
||||
settings.cursor_shape.unwrap_or_default(),
|
||||
settings.alternate_scroll,
|
||||
settings.max_scroll_history_lines,
|
||||
is_ssh_terminal,
|
||||
is_via_remote,
|
||||
cx.entity_id().as_u64(),
|
||||
completion_tx,
|
||||
cx,
|
||||
@@ -424,7 +311,7 @@ impl Project {
|
||||
.map(|builder| {
|
||||
let terminal_handle = cx.new(|cx| builder.subscribe(cx));
|
||||
|
||||
this.terminals
|
||||
self.terminals
|
||||
.local_handles
|
||||
.push(terminal_handle.downgrade());
|
||||
|
||||
@@ -442,7 +329,7 @@ impl Project {
|
||||
})
|
||||
.detach();
|
||||
|
||||
this.activate_python_virtual_environment(
|
||||
self.activate_python_virtual_environment(
|
||||
python_venv_activate_command,
|
||||
&terminal_handle,
|
||||
cx,
|
||||
@@ -652,62 +539,42 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wrap_for_ssh(
|
||||
shell: &str,
|
||||
ssh_command: &SshCommand,
|
||||
command: Option<(&String, &Vec<String>)>,
|
||||
path: Option<&Path>,
|
||||
env: HashMap<String, String>,
|
||||
venv_directory: Option<&Path>,
|
||||
path_style: PathStyle,
|
||||
) -> (String, Vec<String>) {
|
||||
let to_run = if let Some((command, args)) = command {
|
||||
let command: Option<Cow<str>> = shlex::try_quote(command).ok();
|
||||
let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok());
|
||||
command.into_iter().chain(args).join(" ")
|
||||
} else {
|
||||
format!("exec {shell} -l")
|
||||
fn create_remote_shell(
|
||||
spawn_command: Option<(&String, &Vec<String>)>,
|
||||
env: &mut HashMap<String, String>,
|
||||
working_directory: Option<Arc<Path>>,
|
||||
remote_client: Entity<RemoteClient>,
|
||||
cx: &mut App,
|
||||
) -> Result<Shell> {
|
||||
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
|
||||
// to properly display colors.
|
||||
// We do not have the luxury of assuming the host has it installed,
|
||||
// so we set it to a default that does not break the highlighting via ssh.
|
||||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
|
||||
let (program, args) = match spawn_command {
|
||||
Some((program, args)) => (Some(program.clone()), args),
|
||||
None => (None, &Vec::new()),
|
||||
};
|
||||
|
||||
let mut env_changes = String::new();
|
||||
for (k, v) in env.iter() {
|
||||
if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) {
|
||||
env_changes.push_str(&format!("{}={} ", k, v));
|
||||
}
|
||||
}
|
||||
if let Some(venv_directory) = venv_directory
|
||||
&& let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref())
|
||||
{
|
||||
let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string();
|
||||
env_changes.push_str(&format!("PATH={}:$PATH ", path));
|
||||
}
|
||||
let command = remote_client.read(cx).build_command(
|
||||
program,
|
||||
args.as_slice(),
|
||||
env,
|
||||
working_directory.map(|path| path.display().to_string()),
|
||||
None,
|
||||
)?;
|
||||
*env = command.env;
|
||||
|
||||
let commands = if let Some(path) = path {
|
||||
let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string();
|
||||
// shlex will wrap the command in single quotes (''), disabling ~ expansion,
|
||||
// replace ith with something that works
|
||||
let tilde_prefix = "~/";
|
||||
if path.starts_with(tilde_prefix) {
|
||||
let trimmed_path = path
|
||||
.trim_start_matches("/")
|
||||
.trim_start_matches("~")
|
||||
.trim_start_matches("/");
|
||||
log::debug!("Connecting to a remote server: {:?}", command.program);
|
||||
let host = remote_client.read(cx).connection_options().host;
|
||||
|
||||
format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}")
|
||||
} else {
|
||||
format!("cd \"{path}\"; {env_changes} {to_run}")
|
||||
}
|
||||
} else {
|
||||
format!("cd; {env_changes} {to_run}")
|
||||
};
|
||||
let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap());
|
||||
|
||||
let program = "ssh".to_string();
|
||||
let mut args = ssh_command.arguments.clone();
|
||||
|
||||
args.push("-t".to_string());
|
||||
args.push(shell_invocation);
|
||||
(program, args)
|
||||
Ok(Shell::WithArguments {
|
||||
program: command.program,
|
||||
args: command.args,
|
||||
title_override: Some(format!("{} — Terminal", host).into()),
|
||||
})
|
||||
}
|
||||
|
||||
fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> Result<()> {
|
||||
|
||||
@@ -18,7 +18,7 @@ use gpui::{
|
||||
use postage::oneshot;
|
||||
use rpc::{
|
||||
AnyProtoClient, ErrorExt, TypedEnvelope,
|
||||
proto::{self, FromProto, SSH_PROJECT_ID, ToProto},
|
||||
proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
|
||||
};
|
||||
use smol::{
|
||||
channel::{Receiver, Sender},
|
||||
@@ -278,7 +278,7 @@ impl WorktreeStore {
|
||||
let path = RemotePathBuf::new(abs_path.into(), path_style);
|
||||
let response = client
|
||||
.request(proto::AddWorktree {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
project_id: REMOTE_SERVER_PROJECT_ID,
|
||||
path: path.to_proto(),
|
||||
visible,
|
||||
})
|
||||
@@ -298,7 +298,7 @@ impl WorktreeStore {
|
||||
|
||||
let worktree = cx.update(|cx| {
|
||||
Worktree::remote(
|
||||
SSH_PROJECT_ID,
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
0,
|
||||
proto::WorktreeMetadata {
|
||||
id: response.worktree_id,
|
||||
|
||||
@@ -653,7 +653,7 @@ impl ProjectPanel {
|
||||
let file_path = entry.path.clone();
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
let entry_id = entry.id;
|
||||
let is_via_ssh = project.read(cx).is_via_ssh();
|
||||
let is_via_ssh = project.read(cx).is_via_remote_server();
|
||||
|
||||
workspace
|
||||
.open_path_preview(
|
||||
@@ -4089,6 +4089,7 @@ impl ProjectPanel {
|
||||
.when(!is_sticky, |this| {
|
||||
this
|
||||
.when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
|
||||
.when(settings.drag_and_drop, |this| this
|
||||
.on_drag_move::<ExternalPaths>(cx.listener(
|
||||
move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
|
||||
let is_current_target = this.drag_target_entry.as_ref()
|
||||
@@ -4222,7 +4223,7 @@ impl ProjectPanel {
|
||||
}
|
||||
this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
|
||||
}),
|
||||
)
|
||||
))
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
@@ -4433,6 +4434,7 @@ impl ProjectPanel {
|
||||
div()
|
||||
.when(!is_sticky, |div| {
|
||||
div
|
||||
.when(settings.drag_and_drop, |div| div
|
||||
.on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
|
||||
this.hover_scroll_task.take();
|
||||
this.drag_target_entry = None;
|
||||
@@ -4464,7 +4466,7 @@ impl ProjectPanel {
|
||||
}
|
||||
|
||||
},
|
||||
))
|
||||
)))
|
||||
})
|
||||
.child(
|
||||
Label::new(DELIMITER.clone())
|
||||
@@ -4484,6 +4486,7 @@ impl ProjectPanel {
|
||||
.when(index != components_len - 1, |div|{
|
||||
let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
|
||||
div
|
||||
.when(settings.drag_and_drop, |div| div
|
||||
.on_drag_move(cx.listener(
|
||||
move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
|
||||
if event.bounds.contains(&event.event.position) {
|
||||
@@ -4521,7 +4524,7 @@ impl ProjectPanel {
|
||||
target.index == index
|
||||
), |this| {
|
||||
this.bg(item_colors.drag_over)
|
||||
})
|
||||
}))
|
||||
})
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
@@ -5029,7 +5032,8 @@ impl ProjectPanel {
|
||||
|
||||
sticky_parents.reverse();
|
||||
|
||||
let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status;
|
||||
let panel_settings = ProjectPanelSettings::get_global(cx);
|
||||
let git_status_enabled = panel_settings.git_status;
|
||||
let root_name = OsStr::new(worktree.root_name());
|
||||
|
||||
let git_summaries_by_id = if git_status_enabled {
|
||||
@@ -5113,11 +5117,11 @@ impl Render for ProjectPanel {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let has_worktree = !self.visible_entries.is_empty();
|
||||
let project = self.project.read(cx);
|
||||
let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
|
||||
let show_indent_guides =
|
||||
ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
|
||||
let panel_settings = ProjectPanelSettings::get_global(cx);
|
||||
let indent_size = panel_settings.indent_size;
|
||||
let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always;
|
||||
let show_sticky_entries = {
|
||||
if ProjectPanelSettings::get_global(cx).sticky_scroll {
|
||||
if panel_settings.sticky_scroll {
|
||||
let is_scrollable = self.scroll_handle.is_scrollable();
|
||||
let is_scrolled = self.scroll_handle.offset().y < px(0.);
|
||||
is_scrollable && is_scrolled
|
||||
@@ -5205,8 +5209,10 @@ impl Render for ProjectPanel {
|
||||
h_flex()
|
||||
.id("project-panel")
|
||||
.group("project-panel")
|
||||
.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
|
||||
.on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
|
||||
.when(panel_settings.drag_and_drop, |this| {
|
||||
this.on_drag_move(cx.listener(handle_drag_move::<ExternalPaths>))
|
||||
.on_drag_move(cx.listener(handle_drag_move::<DraggedSelection>))
|
||||
})
|
||||
.size_full()
|
||||
.relative()
|
||||
.on_modifiers_changed(cx.listener(
|
||||
@@ -5295,7 +5301,7 @@ impl Render for ProjectPanel {
|
||||
.on_action(cx.listener(Self::open_system))
|
||||
.on_action(cx.listener(Self::open_in_terminal))
|
||||
})
|
||||
.when(project.is_via_ssh(), |el| {
|
||||
.when(project.is_via_remote_server(), |el| {
|
||||
el.on_action(cx.listener(Self::open_in_terminal))
|
||||
})
|
||||
.on_mouse_down(
|
||||
@@ -5544,30 +5550,32 @@ impl Render for ProjectPanel {
|
||||
})),
|
||||
)
|
||||
.when(is_local, |div| {
|
||||
div.drag_over::<ExternalPaths>(|style, _, _, cx| {
|
||||
style.bg(cx.theme().colors().drop_target_background)
|
||||
div.when(panel_settings.drag_and_drop, |div| {
|
||||
div.drag_over::<ExternalPaths>(|style, _, _, cx| {
|
||||
style.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
move |this, external_paths: &ExternalPaths, window, cx| {
|
||||
this.drag_target_entry = None;
|
||||
this.hover_scroll_task.take();
|
||||
if let Some(task) = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
true,
|
||||
external_paths.paths().to_owned(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
},
|
||||
))
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
move |this, external_paths: &ExternalPaths, window, cx| {
|
||||
this.drag_target_entry = None;
|
||||
this.hover_scroll_task.take();
|
||||
if let Some(task) = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
true,
|
||||
external_paths.paths().to_owned(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
},
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ pub struct ProjectPanelSettings {
|
||||
pub scrollbar: ScrollbarSettings,
|
||||
pub show_diagnostics: ShowDiagnostics,
|
||||
pub hide_root: bool,
|
||||
pub drag_and_drop: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
@@ -160,6 +161,10 @@ pub struct ProjectPanelSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub sticky_scroll: Option<bool>,
|
||||
/// Whether to enable drag-and-drop operations in the project panel.
|
||||
///
|
||||
/// Default: true
|
||||
pub drag_and_drop: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for ProjectPanelSettings {
|
||||
|
||||
@@ -16,8 +16,8 @@ pub use typed_envelope::*;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
|
||||
|
||||
pub const SSH_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
|
||||
pub const SSH_PROJECT_ID: u64 = 0;
|
||||
pub const REMOTE_SERVER_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
|
||||
pub const REMOTE_SERVER_PROJECT_ID: u64 = 0;
|
||||
|
||||
messages!(
|
||||
(Ack, Foreground),
|
||||
|
||||
@@ -64,8 +64,8 @@ impl DisconnectedOverlay {
|
||||
}
|
||||
let handle = cx.entity().downgrade();
|
||||
|
||||
let ssh_connection_options = project.read(cx).ssh_connection_options(cx);
|
||||
let host = if let Some(ssh_connection_options) = ssh_connection_options {
|
||||
let remote_connection_options = project.read(cx).remote_connection_options(cx);
|
||||
let host = if let Some(ssh_connection_options) = remote_connection_options {
|
||||
Host::SshRemoteProject(ssh_connection_options)
|
||||
} else {
|
||||
Host::RemoteProject
|
||||
|
||||
@@ -28,8 +28,8 @@ use paths::user_ssh_config_file;
|
||||
use picker::Picker;
|
||||
use project::Fs;
|
||||
use project::Project;
|
||||
use remote::ssh_session::ConnectionIdentifier;
|
||||
use remote::{SshConnectionOptions, SshRemoteClient};
|
||||
use remote::remote_client::ConnectionIdentifier;
|
||||
use remote::{RemoteClient, SshConnectionOptions};
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use settings::update_settings_file;
|
||||
@@ -69,7 +69,7 @@ pub struct RemoteServerProjects {
|
||||
mode: Mode,
|
||||
focus_handle: FocusHandle,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
retained_connections: Vec<Entity<SshRemoteClient>>,
|
||||
retained_connections: Vec<Entity<RemoteClient>>,
|
||||
ssh_config_updates: Task<()>,
|
||||
ssh_config_servers: BTreeSet<SharedString>,
|
||||
create_new_window: bool,
|
||||
@@ -597,7 +597,7 @@ impl RemoteServerProjects {
|
||||
let (path_style, project) = cx.update(|_, cx| {
|
||||
(
|
||||
session.read(cx).path_style(),
|
||||
project::Project::ssh(
|
||||
project::Project::remote(
|
||||
session,
|
||||
app_state.client.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
|
||||
@@ -15,8 +15,9 @@ use gpui::{
|
||||
use language::CursorShape;
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::ssh_session::{ConnectionIdentifier, SshPortForwardOption};
|
||||
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
|
||||
use remote::{
|
||||
ConnectionIdentifier, RemoteClient, RemotePlatform, SshConnectionOptions, SshPortForwardOption,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
@@ -451,7 +452,7 @@ pub struct SshClientDelegate {
|
||||
known_password: Option<String>,
|
||||
}
|
||||
|
||||
impl remote::SshClientDelegate for SshClientDelegate {
|
||||
impl remote::RemoteClientDelegate for SshClientDelegate {
|
||||
fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp) {
|
||||
let mut known_password = self.known_password.clone();
|
||||
if let Some(password) = known_password.take() {
|
||||
@@ -473,7 +474,7 @@ impl remote::SshClientDelegate for SshClientDelegate {
|
||||
|
||||
fn download_server_binary_locally(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
platform: RemotePlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -503,7 +504,7 @@ impl remote::SshClientDelegate for SshClientDelegate {
|
||||
|
||||
fn get_download_params(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
platform: RemotePlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -543,13 +544,13 @@ pub fn connect_over_ssh(
|
||||
ui: Entity<SshPrompt>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<Entity<SshRemoteClient>>>> {
|
||||
) -> Task<Result<Option<Entity<RemoteClient>>>> {
|
||||
let window = window.window_handle();
|
||||
let known_password = connection_options.password.clone();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx));
|
||||
|
||||
remote::SshRemoteClient::new(
|
||||
remote::RemoteClient::ssh(
|
||||
unique_identifier,
|
||||
connection_options,
|
||||
rx,
|
||||
@@ -681,9 +682,9 @@ pub async fn open_ssh_project(
|
||||
|
||||
window
|
||||
.update(cx, |workspace, _, cx| {
|
||||
if let Some(client) = workspace.project().read(cx).ssh_client() {
|
||||
if let Some(client) = workspace.project().read(cx).remote_client() {
|
||||
ExtensionStore::global(cx)
|
||||
.update(cx, |store, cx| store.register_ssh_client(client, cx));
|
||||
.update(cx, |store, cx| store.register_remote_client(client, cx));
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
@@ -51,6 +51,16 @@ pub async fn write_message<S: AsyncWrite + Unpin>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>(
|
||||
stream: &mut S,
|
||||
buffer: &mut Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let len = buffer.len() as u32;
|
||||
stream.write_all(len.to_le_bytes().as_slice()).await?;
|
||||
stream.write_all(buffer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_message_raw<S: AsyncRead + Unpin>(
|
||||
stream: &mut S,
|
||||
buffer: &mut Vec<u8>,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
pub mod json_log;
|
||||
pub mod protocol;
|
||||
pub mod proxy;
|
||||
pub mod ssh_session;
|
||||
pub mod remote_client;
|
||||
mod transport;
|
||||
|
||||
pub use ssh_session::{
|
||||
ConnectionState, SshClientDelegate, SshConnectionOptions, SshInfo, SshPlatform,
|
||||
SshRemoteClient, SshRemoteEvent,
|
||||
pub use remote_client::{
|
||||
ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent,
|
||||
RemotePlatform,
|
||||
};
|
||||
pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption};
|
||||
|
||||
1478
crates/remote/src/remote_client.rs
Normal file
1478
crates/remote/src/remote_client.rs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1
crates/remote/src/transport.rs
Normal file
1
crates/remote/src/transport.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod ssh;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user