Merge remote-tracking branch 'origin/main' into acp-onb-modal
This commit is contained in:
commit
51ebaa82b0
74 changed files with 2052 additions and 2158 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -13521,6 +13521,7 @@ dependencies = [
|
||||||
"smol",
|
"smol",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"telemetry_events",
|
"telemetry_events",
|
||||||
|
"thiserror 2.0.12",
|
||||||
"toml 0.8.20",
|
"toml 0.8.20",
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
|
|
|
@ -428,11 +428,13 @@
|
||||||
"g h": "vim::StartOfLine",
|
"g h": "vim::StartOfLine",
|
||||||
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
||||||
"g e": "vim::EndOfDocument",
|
"g e": "vim::EndOfDocument",
|
||||||
|
"g .": "vim::HelixGotoLastModification", // go to last modification
|
||||||
"g r": "editor::FindAllReferences", // zed specific
|
"g r": "editor::FindAllReferences", // zed specific
|
||||||
"g t": "vim::WindowTop",
|
"g t": "vim::WindowTop",
|
||||||
"g c": "vim::WindowMiddle",
|
"g c": "vim::WindowMiddle",
|
||||||
"g b": "vim::WindowBottom",
|
"g b": "vim::WindowBottom",
|
||||||
|
|
||||||
|
"shift-r": "editor::Paste",
|
||||||
"x": "editor::SelectLine",
|
"x": "editor::SelectLine",
|
||||||
"shift-x": "editor::SelectLine",
|
"shift-x": "editor::SelectLine",
|
||||||
"%": "editor::SelectAll",
|
"%": "editor::SelectAll",
|
||||||
|
|
|
@ -162,6 +162,12 @@
|
||||||
// 2. Always quit the application
|
// 2. Always quit the application
|
||||||
// "on_last_window_closed": "quit_app",
|
// "on_last_window_closed": "quit_app",
|
||||||
"on_last_window_closed": "platform_default",
|
"on_last_window_closed": "platform_default",
|
||||||
|
// Whether to show padding for zoomed panels.
|
||||||
|
// When enabled, zoomed center panels (e.g. code editor) will have padding all around,
|
||||||
|
// while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
|
||||||
|
//
|
||||||
|
// Default: true
|
||||||
|
"zoomed_padding": true,
|
||||||
// Whether to use the system provided dialogs for Open and Save As.
|
// Whether to use the system provided dialogs for Open and Save As.
|
||||||
// When set to false, Zed will use the built-in keyboard-first pickers.
|
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||||
"use_system_path_prompts": true,
|
"use_system_path_prompts": true,
|
||||||
|
@ -1629,6 +1635,9 @@
|
||||||
"allowed": true
|
"allowed": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Kotlin": {
|
||||||
|
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
|
||||||
|
},
|
||||||
"LaTeX": {
|
"LaTeX": {
|
||||||
"formatter": "language_server",
|
"formatter": "language_server",
|
||||||
"language_servers": ["texlab", "..."],
|
"language_servers": ["texlab", "..."],
|
||||||
|
|
|
@ -183,16 +183,15 @@ impl ToolCall {
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
|
||||||
|
first_line.to_owned() + "…"
|
||||||
|
} else {
|
||||||
|
tool_call.title
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
id: tool_call.id,
|
id: tool_call.id,
|
||||||
label: cx.new(|cx| {
|
label: cx
|
||||||
Markdown::new(
|
.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
|
||||||
tool_call.title.into(),
|
|
||||||
Some(language_registry.clone()),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
kind: tool_call.kind,
|
kind: tool_call.kind,
|
||||||
content: tool_call
|
content: tool_call
|
||||||
.content
|
.content
|
||||||
|
@ -233,7 +232,11 @@ impl ToolCall {
|
||||||
|
|
||||||
if let Some(title) = title {
|
if let Some(title) = title {
|
||||||
self.label.update(cx, |label, cx| {
|
self.label.update(cx, |label, cx| {
|
||||||
label.replace(title, cx);
|
if let Some((first_line, _)) = title.split_once("\n") {
|
||||||
|
label.replace(first_line.to_owned() + "…", cx)
|
||||||
|
} else {
|
||||||
|
label.replace(title, cx);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -756,6 +759,8 @@ pub struct AcpThread {
|
||||||
connection: Rc<dyn AgentConnection>,
|
connection: Rc<dyn AgentConnection>,
|
||||||
session_id: acp::SessionId,
|
session_id: acp::SessionId,
|
||||||
token_usage: Option<TokenUsage>,
|
token_usage: Option<TokenUsage>,
|
||||||
|
prompt_capabilities: acp::PromptCapabilities,
|
||||||
|
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -770,11 +775,12 @@ pub enum AcpThreadEvent {
|
||||||
Stopped,
|
Stopped,
|
||||||
Error,
|
Error,
|
||||||
LoadError(LoadError),
|
LoadError(LoadError),
|
||||||
|
PromptCapabilitiesUpdated,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
pub enum ThreadStatus {
|
pub enum ThreadStatus {
|
||||||
Idle,
|
Idle,
|
||||||
WaitingForToolConfirmation,
|
WaitingForToolConfirmation,
|
||||||
|
@ -821,7 +827,20 @@ impl AcpThread {
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
action_log: Entity<ActionLog>,
|
action_log: Entity<ActionLog>,
|
||||||
session_id: acp::SessionId,
|
session_id: acp::SessionId,
|
||||||
|
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let prompt_capabilities = *prompt_capabilities_rx.borrow();
|
||||||
|
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
|
||||||
|
loop {
|
||||||
|
let caps = prompt_capabilities_rx.recv().await?;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.prompt_capabilities = caps;
|
||||||
|
cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
action_log,
|
action_log,
|
||||||
shared_buffers: Default::default(),
|
shared_buffers: Default::default(),
|
||||||
|
@ -833,9 +852,15 @@ impl AcpThread {
|
||||||
connection,
|
connection,
|
||||||
session_id,
|
session_id,
|
||||||
token_usage: None,
|
token_usage: None,
|
||||||
|
prompt_capabilities,
|
||||||
|
_observe_prompt_capabilities: task,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||||
|
self.prompt_capabilities
|
||||||
|
}
|
||||||
|
|
||||||
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
||||||
&self.connection
|
&self.connection
|
||||||
}
|
}
|
||||||
|
@ -2599,13 +2624,19 @@ mod tests {
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
let thread = cx.new(|_cx| {
|
let thread = cx.new(|cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
"Test",
|
"Test",
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
|
watch::Receiver::constant(acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: true,
|
||||||
|
embedded_context: true,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
self.sessions.lock().insert(session_id, thread.downgrade());
|
self.sessions.lock().insert(session_id, thread.downgrade());
|
||||||
|
@ -2639,14 +2670,6 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: true,
|
|
||||||
embedded_context: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||||
let sessions = self.sessions.lock();
|
let sessions = self.sessions.lock();
|
||||||
let thread = sessions.get(session_id).unwrap().clone();
|
let thread = sessions.get(session_id).unwrap().clone();
|
||||||
|
|
|
@ -38,8 +38,6 @@ pub trait AgentConnection {
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<acp::PromptResponse>>;
|
) -> Task<Result<acp::PromptResponse>>;
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities;
|
|
||||||
|
|
||||||
fn resume(
|
fn resume(
|
||||||
&self,
|
&self,
|
||||||
_session_id: &acp::SessionId,
|
_session_id: &acp::SessionId,
|
||||||
|
@ -329,13 +327,19 @@ mod test_support {
|
||||||
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||||
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
|
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
let thread = cx.new(|_cx| {
|
let thread = cx.new(|cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
"Test",
|
"Test",
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
|
watch::Receiver::constant(acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: true,
|
||||||
|
embedded_context: true,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
self.sessions.lock().insert(
|
self.sessions.lock().insert(
|
||||||
|
@ -348,14 +352,6 @@ mod test_support {
|
||||||
Task::ready(Ok(thread))
|
Task::ready(Ok(thread))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: true,
|
|
||||||
embedded_context: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn authenticate(
|
fn authenticate(
|
||||||
&self,
|
&self,
|
||||||
_method_id: acp::AuthMethodId,
|
_method_id: acp::AuthMethodId,
|
||||||
|
|
|
@ -21,12 +21,12 @@ use ui::prelude::*;
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::{Item, Workspace};
|
use workspace::{Item, Workspace};
|
||||||
|
|
||||||
actions!(acp, [OpenDebugTools]);
|
actions!(dev, [OpenAcpLogs]);
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
cx.observe_new(
|
cx.observe_new(
|
||||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||||
workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| {
|
workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
|
||||||
let acp_tools =
|
let acp_tools =
|
||||||
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
|
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
|
||||||
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
|
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
|
||||||
|
|
|
@ -180,7 +180,7 @@ impl NativeAgent {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<Entity<NativeAgent>> {
|
) -> Result<Entity<NativeAgent>> {
|
||||||
log::info!("Creating new NativeAgent");
|
log::debug!("Creating new NativeAgent");
|
||||||
|
|
||||||
let project_context = cx
|
let project_context = cx
|
||||||
.update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?
|
.update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?
|
||||||
|
@ -240,13 +240,16 @@ impl NativeAgent {
|
||||||
let title = thread.title();
|
let title = thread.title();
|
||||||
let project = thread.project.clone();
|
let project = thread.project.clone();
|
||||||
let action_log = thread.action_log.clone();
|
let action_log = thread.action_log.clone();
|
||||||
let acp_thread = cx.new(|_cx| {
|
let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone();
|
||||||
|
let acp_thread = cx.new(|cx| {
|
||||||
acp_thread::AcpThread::new(
|
acp_thread::AcpThread::new(
|
||||||
title,
|
title,
|
||||||
connection,
|
connection,
|
||||||
project.clone(),
|
project.clone(),
|
||||||
action_log.clone(),
|
action_log.clone(),
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
|
prompt_capabilities_rx,
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let subscriptions = vec![
|
let subscriptions = vec![
|
||||||
|
@ -756,7 +759,7 @@ impl NativeAgentConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("Response stream completed");
|
log::debug!("Response stream completed");
|
||||||
anyhow::Ok(acp::PromptResponse {
|
anyhow::Ok(acp::PromptResponse {
|
||||||
stop_reason: acp::StopReason::EndTurn,
|
stop_reason: acp::StopReason::EndTurn,
|
||||||
})
|
})
|
||||||
|
@ -781,7 +784,7 @@ impl AgentModelSelector for NativeAgentConnection {
|
||||||
model_id: acp_thread::AgentModelId,
|
model_id: acp_thread::AgentModelId,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
log::info!("Setting model for session {}: {}", session_id, model_id);
|
log::debug!("Setting model for session {}: {}", session_id, model_id);
|
||||||
let Some(thread) = self
|
let Some(thread) = self
|
||||||
.0
|
.0
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -852,7 +855,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<Entity<acp_thread::AcpThread>>> {
|
) -> Task<Result<Entity<acp_thread::AcpThread>>> {
|
||||||
let agent = self.0.clone();
|
let agent = self.0.clone();
|
||||||
log::info!("Creating new thread for project at: {:?}", cwd);
|
log::debug!("Creating new thread for project at: {:?}", cwd);
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
log::debug!("Starting thread creation in async context");
|
log::debug!("Starting thread creation in async context");
|
||||||
|
@ -917,7 +920,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
log::info!("Converted prompt to message: {} chars", content.len());
|
log::debug!("Converted prompt to message: {} chars", content.len());
|
||||||
log::debug!("Message id: {:?}", id);
|
log::debug!("Message id: {:?}", id);
|
||||||
log::debug!("Message content: {:?}", content);
|
log::debug!("Message content: {:?}", content);
|
||||||
|
|
||||||
|
@ -925,14 +928,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: false,
|
|
||||||
embedded_context: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resume(
|
fn resume(
|
||||||
&self,
|
&self,
|
||||||
session_id: &acp::SessionId,
|
session_id: &acp::SessionId,
|
||||||
|
|
|
@ -22,6 +22,10 @@ impl NativeAgentServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentServer for NativeAgentServer {
|
impl AgentServer for NativeAgentServer {
|
||||||
|
fn telemetry_id(&self) -> &'static str {
|
||||||
|
"zed"
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Zed Agent".into()
|
"Zed Agent".into()
|
||||||
}
|
}
|
||||||
|
@ -44,7 +48,7 @@ impl AgentServer for NativeAgentServer {
|
||||||
project: &Entity<Project>,
|
project: &Entity<Project>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
|
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
|
||||||
log::info!(
|
log::debug!(
|
||||||
"NativeAgentServer::connect called for path: {:?}",
|
"NativeAgentServer::connect called for path: {:?}",
|
||||||
_root_dir
|
_root_dir
|
||||||
);
|
);
|
||||||
|
@ -63,7 +67,7 @@ impl AgentServer for NativeAgentServer {
|
||||||
|
|
||||||
// Create the connection wrapper
|
// Create the connection wrapper
|
||||||
let connection = NativeAgentConnection(agent);
|
let connection = NativeAgentConnection(agent);
|
||||||
log::info!("NativeAgentServer connection established successfully");
|
log::debug!("NativeAgentServer connection established successfully");
|
||||||
|
|
||||||
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
|
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
|
||||||
})
|
})
|
||||||
|
|
|
@ -72,6 +72,7 @@ async fn test_echo(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||||
async fn test_thinking(cx: &mut TestAppContext) {
|
async fn test_thinking(cx: &mut TestAppContext) {
|
||||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||||
let fake_model = model.as_fake();
|
let fake_model = model.as_fake();
|
||||||
|
@ -1347,6 +1348,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||||
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
||||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||||
let fake_model = model.as_fake();
|
let fake_model = model.as_fake();
|
||||||
|
@ -1685,6 +1687,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||||
async fn test_title_generation(cx: &mut TestAppContext) {
|
async fn test_title_generation(cx: &mut TestAppContext) {
|
||||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||||
let fake_model = model.as_fake();
|
let fake_model = model.as_fake();
|
||||||
|
|
|
@ -575,11 +575,22 @@ pub struct Thread {
|
||||||
templates: Arc<Templates>,
|
templates: Arc<Templates>,
|
||||||
model: Option<Arc<dyn LanguageModel>>,
|
model: Option<Arc<dyn LanguageModel>>,
|
||||||
summarization_model: Option<Arc<dyn LanguageModel>>,
|
summarization_model: Option<Arc<dyn LanguageModel>>,
|
||||||
|
prompt_capabilities_tx: watch::Sender<acp::PromptCapabilities>,
|
||||||
|
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||||
pub(crate) project: Entity<Project>,
|
pub(crate) project: Entity<Project>,
|
||||||
pub(crate) action_log: Entity<ActionLog>,
|
pub(crate) action_log: Entity<ActionLog>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Thread {
|
impl Thread {
|
||||||
|
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
|
||||||
|
let image = model.map_or(true, |model| model.supports_images());
|
||||||
|
acp::PromptCapabilities {
|
||||||
|
image,
|
||||||
|
audio: false,
|
||||||
|
embedded_context: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
project_context: Entity<ProjectContext>,
|
project_context: Entity<ProjectContext>,
|
||||||
|
@ -590,6 +601,8 @@ impl Thread {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
||||||
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
||||||
|
let (prompt_capabilities_tx, prompt_capabilities_rx) =
|
||||||
|
watch::channel(Self::prompt_capabilities(model.as_deref()));
|
||||||
Self {
|
Self {
|
||||||
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
|
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
|
||||||
prompt_id: PromptId::new(),
|
prompt_id: PromptId::new(),
|
||||||
|
@ -617,6 +630,8 @@ impl Thread {
|
||||||
templates,
|
templates,
|
||||||
model,
|
model,
|
||||||
summarization_model: None,
|
summarization_model: None,
|
||||||
|
prompt_capabilities_tx,
|
||||||
|
prompt_capabilities_rx,
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
}
|
}
|
||||||
|
@ -750,6 +765,8 @@ impl Thread {
|
||||||
.or_else(|| registry.default_model())
|
.or_else(|| registry.default_model())
|
||||||
.map(|model| model.model)
|
.map(|model| model.model)
|
||||||
});
|
});
|
||||||
|
let (prompt_capabilities_tx, prompt_capabilities_rx) =
|
||||||
|
watch::channel(Self::prompt_capabilities(model.as_deref()));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
|
@ -779,6 +796,8 @@ impl Thread {
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
updated_at: db_thread.updated_at,
|
updated_at: db_thread.updated_at,
|
||||||
|
prompt_capabilities_tx,
|
||||||
|
prompt_capabilities_rx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -946,10 +965,12 @@ impl Thread {
|
||||||
pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
||||||
let old_usage = self.latest_token_usage();
|
let old_usage = self.latest_token_usage();
|
||||||
self.model = Some(model);
|
self.model = Some(model);
|
||||||
|
let new_caps = Self::prompt_capabilities(self.model.as_deref());
|
||||||
let new_usage = self.latest_token_usage();
|
let new_usage = self.latest_token_usage();
|
||||||
if old_usage != new_usage {
|
if old_usage != new_usage {
|
||||||
cx.emit(TokenUsageUpdated(new_usage));
|
cx.emit(TokenUsageUpdated(new_usage));
|
||||||
}
|
}
|
||||||
|
self.prompt_capabilities_tx.send(new_caps).log_err();
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1088,7 +1109,7 @@ impl Thread {
|
||||||
self.messages.push(Message::Resume);
|
self.messages.push(Message::Resume);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
log::info!("Total messages in thread: {}", self.messages.len());
|
log::debug!("Total messages in thread: {}", self.messages.len());
|
||||||
self.run_turn(cx)
|
self.run_turn(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1106,7 +1127,7 @@ impl Thread {
|
||||||
{
|
{
|
||||||
let model = self.model().context("No language model configured")?;
|
let model = self.model().context("No language model configured")?;
|
||||||
|
|
||||||
log::info!("Thread::send called with model: {:?}", model.name());
|
log::info!("Thread::send called with model: {}", model.name().0);
|
||||||
self.advance_prompt_id();
|
self.advance_prompt_id();
|
||||||
|
|
||||||
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||||
|
@ -1116,7 +1137,7 @@ impl Thread {
|
||||||
.push(Message::User(UserMessage { id, content }));
|
.push(Message::User(UserMessage { id, content }));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
log::info!("Total messages in thread: {}", self.messages.len());
|
log::debug!("Total messages in thread: {}", self.messages.len());
|
||||||
self.run_turn(cx)
|
self.run_turn(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1140,44 +1161,14 @@ impl Thread {
|
||||||
event_stream: event_stream.clone(),
|
event_stream: event_stream.clone(),
|
||||||
tools: self.enabled_tools(profile, &model, cx),
|
tools: self.enabled_tools(profile, &model, cx),
|
||||||
_task: cx.spawn(async move |this, cx| {
|
_task: cx.spawn(async move |this, cx| {
|
||||||
log::info!("Starting agent turn execution");
|
log::debug!("Starting agent turn execution");
|
||||||
|
|
||||||
let turn_result: Result<()> = async {
|
let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await;
|
||||||
let mut intent = CompletionIntent::UserPrompt;
|
|
||||||
loop {
|
|
||||||
Self::stream_completion(&this, &model, intent, &event_stream, cx).await?;
|
|
||||||
|
|
||||||
let mut end_turn = true;
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
// Generate title if needed.
|
|
||||||
if this.title.is_none() && this.pending_title_generation.is_none() {
|
|
||||||
this.generate_title(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// End the turn if the model didn't use tools.
|
|
||||||
let message = this.pending_message.as_ref();
|
|
||||||
end_turn =
|
|
||||||
message.map_or(true, |message| message.tool_results.is_empty());
|
|
||||||
this.flush_pending_message(cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
|
|
||||||
log::info!("Tool use limit reached, completing turn");
|
|
||||||
return Err(language_model::ToolUseLimitReachedError.into());
|
|
||||||
} else if end_turn {
|
|
||||||
log::info!("No tool uses found, completing turn");
|
|
||||||
return Ok(());
|
|
||||||
} else {
|
|
||||||
intent = CompletionIntent::ToolResults;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.await;
|
|
||||||
_ = this.update(cx, |this, cx| this.flush_pending_message(cx));
|
_ = this.update(cx, |this, cx| this.flush_pending_message(cx));
|
||||||
|
|
||||||
match turn_result {
|
match turn_result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log::info!("Turn execution completed");
|
log::debug!("Turn execution completed");
|
||||||
event_stream.send_stop(acp::StopReason::EndTurn);
|
event_stream.send_stop(acp::StopReason::EndTurn);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
@ -1203,20 +1194,17 @@ impl Thread {
|
||||||
Ok(events_rx)
|
Ok(events_rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stream_completion(
|
async fn run_turn_internal(
|
||||||
this: &WeakEntity<Self>,
|
this: &WeakEntity<Self>,
|
||||||
model: &Arc<dyn LanguageModel>,
|
model: Arc<dyn LanguageModel>,
|
||||||
completion_intent: CompletionIntent,
|
|
||||||
event_stream: &ThreadEventStream,
|
event_stream: &ThreadEventStream,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
log::debug!("Stream completion started successfully");
|
let mut attempt = 0;
|
||||||
|
let mut intent = CompletionIntent::UserPrompt;
|
||||||
let mut attempt = None;
|
|
||||||
loop {
|
loop {
|
||||||
let request = this.update(cx, |this, cx| {
|
let request =
|
||||||
this.build_completion_request(completion_intent, cx)
|
this.update(cx, |this, cx| this.build_completion_request(intent, cx))??;
|
||||||
})??;
|
|
||||||
|
|
||||||
telemetry::event!(
|
telemetry::event!(
|
||||||
"Agent Thread Completion",
|
"Agent Thread Completion",
|
||||||
|
@ -1227,23 +1215,19 @@ impl Thread {
|
||||||
attempt
|
attempt
|
||||||
);
|
);
|
||||||
|
|
||||||
log::info!(
|
log::debug!("Calling model.stream_completion, attempt {}", attempt);
|
||||||
"Calling model.stream_completion, attempt {}",
|
|
||||||
attempt.unwrap_or(0)
|
|
||||||
);
|
|
||||||
let mut events = model
|
let mut events = model
|
||||||
.stream_completion(request, cx)
|
.stream_completion(request, cx)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| anyhow!(error))?;
|
.map_err(|error| anyhow!(error))?;
|
||||||
let mut tool_results = FuturesUnordered::new();
|
let mut tool_results = FuturesUnordered::new();
|
||||||
let mut error = None;
|
let mut error = None;
|
||||||
|
|
||||||
while let Some(event) = events.next().await {
|
while let Some(event) = events.next().await {
|
||||||
|
log::trace!("Received completion event: {:?}", event);
|
||||||
match event {
|
match event {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
log::trace!("Received completion event: {:?}", event);
|
|
||||||
tool_results.extend(this.update(cx, |this, cx| {
|
tool_results.extend(this.update(cx, |this, cx| {
|
||||||
this.handle_streamed_completion_event(event, event_stream, cx)
|
this.handle_completion_event(event, event_stream, cx)
|
||||||
})??);
|
})??);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
@ -1253,8 +1237,9 @@ impl Thread {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let end_turn = tool_results.is_empty();
|
||||||
while let Some(tool_result) = tool_results.next().await {
|
while let Some(tool_result) = tool_results.next().await {
|
||||||
log::info!("Tool finished {:?}", tool_result);
|
log::debug!("Tool finished {:?}", tool_result);
|
||||||
|
|
||||||
event_stream.update_tool_call_fields(
|
event_stream.update_tool_call_fields(
|
||||||
&tool_result.tool_use_id,
|
&tool_result.tool_use_id,
|
||||||
|
@ -1275,65 +1260,83 @@ impl Thread {
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.flush_pending_message(cx);
|
||||||
|
if this.title.is_none() && this.pending_title_generation.is_none() {
|
||||||
|
this.generate_title(cx);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
if let Some(error) = error {
|
if let Some(error) = error {
|
||||||
let completion_mode = this.read_with(cx, |thread, _cx| thread.completion_mode())?;
|
attempt += 1;
|
||||||
if completion_mode == CompletionMode::Normal {
|
let retry =
|
||||||
return Err(anyhow!(error))?;
|
this.update(cx, |this, _| this.handle_completion_error(error, attempt))??;
|
||||||
}
|
let timer = cx.background_executor().timer(retry.duration);
|
||||||
|
event_stream.send_retry(retry);
|
||||||
let Some(strategy) = Self::retry_strategy_for(&error) else {
|
timer.await;
|
||||||
return Err(anyhow!(error))?;
|
this.update(cx, |this, _cx| {
|
||||||
};
|
|
||||||
|
|
||||||
let max_attempts = match &strategy {
|
|
||||||
RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
|
|
||||||
RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
|
|
||||||
};
|
|
||||||
|
|
||||||
let attempt = attempt.get_or_insert(0u8);
|
|
||||||
|
|
||||||
*attempt += 1;
|
|
||||||
|
|
||||||
let attempt = *attempt;
|
|
||||||
if attempt > max_attempts {
|
|
||||||
return Err(anyhow!(error))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let delay = match &strategy {
|
|
||||||
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
|
|
||||||
let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
|
|
||||||
Duration::from_secs(delay_secs)
|
|
||||||
}
|
|
||||||
RetryStrategy::Fixed { delay, .. } => *delay,
|
|
||||||
};
|
|
||||||
log::debug!("Retry attempt {attempt} with delay {delay:?}");
|
|
||||||
|
|
||||||
event_stream.send_retry(acp_thread::RetryStatus {
|
|
||||||
last_error: error.to_string().into(),
|
|
||||||
attempt: attempt as usize,
|
|
||||||
max_attempts: max_attempts as usize,
|
|
||||||
started_at: Instant::now(),
|
|
||||||
duration: delay,
|
|
||||||
});
|
|
||||||
cx.background_executor().timer(delay).await;
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.flush_pending_message(cx);
|
|
||||||
if let Some(Message::Agent(message)) = this.messages.last() {
|
if let Some(Message::Agent(message)) = this.messages.last() {
|
||||||
if message.tool_results.is_empty() {
|
if message.tool_results.is_empty() {
|
||||||
|
intent = CompletionIntent::UserPrompt;
|
||||||
this.messages.push(Message::Resume);
|
this.messages.push(Message::Resume);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
} else {
|
} else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
|
||||||
|
return Err(language_model::ToolUseLimitReachedError.into());
|
||||||
|
} else if end_turn {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
intent = CompletionIntent::ToolResults;
|
||||||
|
attempt = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_completion_error(
|
||||||
|
&mut self,
|
||||||
|
error: LanguageModelCompletionError,
|
||||||
|
attempt: u8,
|
||||||
|
) -> Result<acp_thread::RetryStatus> {
|
||||||
|
if self.completion_mode == CompletionMode::Normal {
|
||||||
|
return Err(anyhow!(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(strategy) = Self::retry_strategy_for(&error) else {
|
||||||
|
return Err(anyhow!(error));
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_attempts = match &strategy {
|
||||||
|
RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
|
||||||
|
RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
|
||||||
|
};
|
||||||
|
|
||||||
|
if attempt > max_attempts {
|
||||||
|
return Err(anyhow!(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
let delay = match &strategy {
|
||||||
|
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
|
||||||
|
let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
|
||||||
|
Duration::from_secs(delay_secs)
|
||||||
|
}
|
||||||
|
RetryStrategy::Fixed { delay, .. } => *delay,
|
||||||
|
};
|
||||||
|
log::debug!("Retry attempt {attempt} with delay {delay:?}");
|
||||||
|
|
||||||
|
Ok(acp_thread::RetryStatus {
|
||||||
|
last_error: error.to_string().into(),
|
||||||
|
attempt: attempt as usize,
|
||||||
|
max_attempts: max_attempts as usize,
|
||||||
|
started_at: Instant::now(),
|
||||||
|
duration: delay,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// A helper method that's called on every streamed completion event.
|
/// A helper method that's called on every streamed completion event.
|
||||||
/// Returns an optional tool result task, which the main agentic loop will
|
/// Returns an optional tool result task, which the main agentic loop will
|
||||||
/// send back to the model when it resolves.
|
/// send back to the model when it resolves.
|
||||||
fn handle_streamed_completion_event(
|
fn handle_completion_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: LanguageModelCompletionEvent,
|
event: LanguageModelCompletionEvent,
|
||||||
event_stream: &ThreadEventStream,
|
event_stream: &ThreadEventStream,
|
||||||
|
@ -1528,7 +1531,7 @@ impl Thread {
|
||||||
});
|
});
|
||||||
let supports_images = self.model().is_some_and(|model| model.supports_images());
|
let supports_images = self.model().is_some_and(|model| model.supports_images());
|
||||||
let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
|
let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
|
||||||
log::info!("Running tool {}", tool_use.name);
|
log::debug!("Running tool {}", tool_use.name);
|
||||||
Some(cx.foreground_executor().spawn(async move {
|
Some(cx.foreground_executor().spawn(async move {
|
||||||
let tool_result = tool_result.await.and_then(|output| {
|
let tool_result = tool_result.await.and_then(|output| {
|
||||||
if let LanguageModelToolResultContent::Image(_) = &output.llm_output
|
if let LanguageModelToolResultContent::Image(_) = &output.llm_output
|
||||||
|
@ -1640,7 +1643,7 @@ impl Thread {
|
||||||
summary.extend(lines.next());
|
summary.extend(lines.next());
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("Setting summary: {}", summary);
|
log::debug!("Setting summary: {}", summary);
|
||||||
let summary = SharedString::from(summary);
|
let summary = SharedString::from(summary);
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
@ -1657,7 +1660,7 @@ impl Thread {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!(
|
log::debug!(
|
||||||
"Generating title with model: {:?}",
|
"Generating title with model: {:?}",
|
||||||
self.summarization_model.as_ref().map(|model| model.name())
|
self.summarization_model.as_ref().map(|model| model.name())
|
||||||
);
|
);
|
||||||
|
@ -1799,8 +1802,8 @@ impl Thread {
|
||||||
log::debug!("Completion mode: {:?}", self.completion_mode);
|
log::debug!("Completion mode: {:?}", self.completion_mode);
|
||||||
|
|
||||||
let messages = self.build_request_messages(cx);
|
let messages = self.build_request_messages(cx);
|
||||||
log::info!("Request will include {} messages", messages.len());
|
log::debug!("Request will include {} messages", messages.len());
|
||||||
log::info!("Request includes {} tools", tools.len());
|
log::debug!("Request includes {} tools", tools.len());
|
||||||
|
|
||||||
let request = LanguageModelRequest {
|
let request = LanguageModelRequest {
|
||||||
thread_id: Some(self.id.to_string()),
|
thread_id: Some(self.id.to_string()),
|
||||||
|
|
|
@ -136,12 +136,17 @@ impl AgentTool for FetchTool {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
input: Self::Input,
|
input: Self::Input,
|
||||||
_event_stream: ToolCallEventStream,
|
event_stream: ToolCallEventStream,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<Self::Output>> {
|
) -> Task<Result<Self::Output>> {
|
||||||
|
let authorize = event_stream.authorize(input.url.clone(), cx);
|
||||||
|
|
||||||
let text = cx.background_spawn({
|
let text = cx.background_spawn({
|
||||||
let http_client = self.http_client.clone();
|
let http_client = self.http_client.clone();
|
||||||
async move { Self::build_message(http_client, &input.url).await }
|
async move {
|
||||||
|
authorize.await?;
|
||||||
|
Self::build_message(http_client, &input.url).await
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.foreground_executor().spawn(async move {
|
cx.foreground_executor().spawn(async move {
|
||||||
|
|
|
@ -165,16 +165,17 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
Ok(snapshots
|
let mut results = Vec::new();
|
||||||
.iter()
|
for snapshot in snapshots {
|
||||||
.flat_map(|snapshot| {
|
for entry in snapshot.entries(false, 0) {
|
||||||
let root_name = PathBuf::from(snapshot.root_name());
|
let root_name = PathBuf::from(snapshot.root_name());
|
||||||
snapshot
|
if path_matcher.is_match(root_name.join(&entry.path)) {
|
||||||
.entries(false, 0)
|
results.push(snapshot.abs_path().join(entry.path.as_ref()));
|
||||||
.map(move |entry| root_name.join(&entry.path))
|
}
|
||||||
.filter(|path| path_matcher.is_match(&path))
|
}
|
||||||
})
|
}
|
||||||
.collect())
|
|
||||||
|
Ok(results)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,8 +216,8 @@ mod test {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
matches,
|
matches,
|
||||||
&[
|
&[
|
||||||
PathBuf::from("root/apple/banana/carrot"),
|
PathBuf::from(path!("/root/apple/banana/carrot")),
|
||||||
PathBuf::from("root/apple/bandana/carbonara")
|
PathBuf::from(path!("/root/apple/bandana/carbonara"))
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -227,8 +228,8 @@ mod test {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
matches,
|
matches,
|
||||||
&[
|
&[
|
||||||
PathBuf::from("root/apple/banana/carrot"),
|
PathBuf::from(path!("/root/apple/banana/carrot")),
|
||||||
PathBuf::from("root/apple/bandana/carbonara")
|
PathBuf::from(path!("/root/apple/bandana/carbonara"))
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{path::Path, sync::Arc};
|
use std::{path::Path, sync::Arc};
|
||||||
|
use util::markdown::MarkdownCodeBlock;
|
||||||
|
|
||||||
use crate::{AgentTool, ToolCallEventStream};
|
use crate::{AgentTool, ToolCallEventStream};
|
||||||
|
|
||||||
|
@ -243,6 +244,19 @@ impl AgentTool for ReadFileTool {
|
||||||
}]),
|
}]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
|
||||||
|
let markdown = MarkdownCodeBlock {
|
||||||
|
tag: &input.path,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
event_stream.update_fields(ToolCallUpdateFields {
|
||||||
|
content: Some(vec![acp::ToolCallContent::Content {
|
||||||
|
content: markdown.into(),
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
|
@ -162,12 +162,34 @@ impl AgentConnection for AcpConnection {
|
||||||
let conn = self.connection.clone();
|
let conn = self.connection.clone();
|
||||||
let sessions = self.sessions.clone();
|
let sessions = self.sessions.clone();
|
||||||
let cwd = cwd.to_path_buf();
|
let cwd = cwd.to_path_buf();
|
||||||
|
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||||
|
let mcp_servers = context_server_store
|
||||||
|
.configured_server_ids()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| {
|
||||||
|
let configuration = context_server_store.configuration_for_server(id)?;
|
||||||
|
let command = configuration.command();
|
||||||
|
Some(acp::McpServer {
|
||||||
|
name: id.0.to_string(),
|
||||||
|
command: command.path.clone(),
|
||||||
|
args: command.args.clone(),
|
||||||
|
env: if let Some(env) = command.env.as_ref() {
|
||||||
|
env.iter()
|
||||||
|
.map(|(name, value)| acp::EnvVariable {
|
||||||
|
name: name.clone(),
|
||||||
|
value: value.clone(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let response = conn
|
let response = conn
|
||||||
.new_session(acp::NewSessionRequest {
|
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
|
||||||
mcp_servers: vec![],
|
|
||||||
cwd,
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||||
|
@ -185,13 +207,16 @@ impl AgentConnection for AcpConnection {
|
||||||
|
|
||||||
let session_id = response.session_id;
|
let session_id = response.session_id;
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||||
let thread = cx.new(|_cx| {
|
let thread = cx.new(|cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
self.server_name.clone(),
|
self.server_name.clone(),
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
|
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
|
||||||
|
watch::Receiver::constant(self.prompt_capabilities),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -263,7 +288,9 @@ impl AgentConnection for AcpConnection {
|
||||||
|
|
||||||
match serde_json::from_value(data.clone()) {
|
match serde_json::from_value(data.clone()) {
|
||||||
Ok(ErrorDetails { details }) => {
|
Ok(ErrorDetails { details }) => {
|
||||||
if suppress_abort_err && details.contains("This operation was aborted")
|
if suppress_abort_err
|
||||||
|
&& (details.contains("This operation was aborted")
|
||||||
|
|| details.contains("The user aborted a request"))
|
||||||
{
|
{
|
||||||
Ok(acp::PromptResponse {
|
Ok(acp::PromptResponse {
|
||||||
stop_reason: acp::StopReason::Cancelled,
|
stop_reason: acp::StopReason::Cancelled,
|
||||||
|
@ -279,10 +306,6 @@ impl AgentConnection for AcpConnection {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
self.prompt_capabilities
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||||
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
||||||
session.suppress_abort_err = true;
|
session.suppress_abort_err = true;
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -36,6 +36,7 @@ pub trait AgentServer: Send {
|
||||||
fn name(&self) -> SharedString;
|
fn name(&self) -> SharedString;
|
||||||
fn empty_state_headline(&self) -> SharedString;
|
fn empty_state_headline(&self) -> SharedString;
|
||||||
fn empty_state_message(&self) -> SharedString;
|
fn empty_state_message(&self) -> SharedString;
|
||||||
|
fn telemetry_id(&self) -> &'static str;
|
||||||
|
|
||||||
fn connect(
|
fn connect(
|
||||||
&self,
|
&self,
|
||||||
|
@ -97,7 +98,7 @@ pub struct AgentServerCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentServerCommand {
|
impl AgentServerCommand {
|
||||||
pub(crate) async fn resolve(
|
pub async fn resolve(
|
||||||
path_bin_name: &'static str,
|
path_bin_name: &'static str,
|
||||||
extra_args: &[&'static str],
|
extra_args: &[&'static str],
|
||||||
fallback_path: Option<&Path>,
|
fallback_path: Option<&Path>,
|
||||||
|
|
|
@ -43,6 +43,10 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
|
||||||
pub struct ClaudeCode;
|
pub struct ClaudeCode;
|
||||||
|
|
||||||
impl AgentServer for ClaudeCode {
|
impl AgentServer for ClaudeCode {
|
||||||
|
fn telemetry_id(&self) -> &'static str {
|
||||||
|
"claude-code"
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Claude Code".into()
|
"Claude Code".into()
|
||||||
}
|
}
|
||||||
|
@ -249,13 +253,19 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||||
});
|
});
|
||||||
|
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||||
let thread = cx.new(|_cx| {
|
let thread = cx.new(|cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
"Claude Code",
|
"Claude Code",
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
|
watch::Receiver::constant(acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: false,
|
||||||
|
embedded_context: true,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -319,14 +329,6 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||||
cx.foreground_executor().spawn(async move { end_rx.await? })
|
cx.foreground_executor().spawn(async move { end_rx.await? })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: false,
|
|
||||||
embedded_context: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
|
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
|
||||||
let sessions = self.sessions.borrow();
|
let sessions = self.sessions.borrow();
|
||||||
let Some(session) = sessions.get(session_id) else {
|
let Some(session) = sessions.get(session_id) else {
|
||||||
|
|
|
@ -22,6 +22,10 @@ impl CustomAgentServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::AgentServer for CustomAgentServer {
|
impl crate::AgentServer for CustomAgentServer {
|
||||||
|
fn telemetry_id(&self) -> &'static str {
|
||||||
|
"custom"
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
self.name.clone()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,10 @@ pub struct Gemini;
|
||||||
const ACP_ARG: &str = "--experimental-acp";
|
const ACP_ARG: &str = "--experimental-acp";
|
||||||
|
|
||||||
impl AgentServer for Gemini {
|
impl AgentServer for Gemini {
|
||||||
|
fn telemetry_id(&self) -> &'static str {
|
||||||
|
"gemini-cli"
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Gemini CLI".into()
|
"Gemini CLI".into()
|
||||||
}
|
}
|
||||||
|
@ -53,7 +57,7 @@ impl AgentServer for Gemini {
|
||||||
return Err(LoadError::NotInstalled {
|
return Err(LoadError::NotInstalled {
|
||||||
error_message: "Failed to find Gemini CLI binary".into(),
|
error_message: "Failed to find Gemini CLI binary".into(),
|
||||||
install_message: "Install Gemini CLI".into(),
|
install_message: "Install Gemini CLI".into(),
|
||||||
install_command: "npm install -g @google/gemini-cli@preview".into()
|
install_command: Self::install_command().into(),
|
||||||
}.into());
|
}.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,7 +92,7 @@ impl AgentServer for Gemini {
|
||||||
current_version
|
current_version
|
||||||
).into(),
|
).into(),
|
||||||
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
||||||
upgrade_command: "npm install -g @google/gemini-cli@preview".into(),
|
upgrade_command: Self::upgrade_command().into(),
|
||||||
}.into())
|
}.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,6 +105,20 @@ impl AgentServer for Gemini {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Gemini {
|
||||||
|
pub fn binary_name() -> &'static str {
|
||||||
|
"gemini"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_command() -> &'static str {
|
||||||
|
"npm install -g @google/gemini-cli@preview"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upgrade_command() -> &'static str {
|
||||||
|
"npm install -g @google/gemini-cli@preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -373,7 +373,7 @@ impl MessageEditor {
|
||||||
|
|
||||||
if Img::extensions().contains(&extension) && !extension.contains("svg") {
|
if Img::extensions().contains(&extension) && !extension.contains("svg") {
|
||||||
if !self.prompt_capabilities.get().image {
|
if !self.prompt_capabilities.get().image {
|
||||||
return Task::ready(Err(anyhow!("This agent does not support images yet")));
|
return Task::ready(Err(anyhow!("This model does not support images yet")));
|
||||||
}
|
}
|
||||||
let task = self
|
let task = self
|
||||||
.project
|
.project
|
||||||
|
|
|
@ -462,7 +462,7 @@ impl AcpThreadHistory {
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}))
|
}))
|
||||||
.end_slot::<IconButton>(if hovered || selected {
|
.end_slot::<IconButton>(if hovered {
|
||||||
Some(
|
Some(
|
||||||
IconButton::new("delete", IconName::Trash)
|
IconButton::new("delete", IconName::Trash)
|
||||||
.shape(IconButtonShape::Square)
|
.shape(IconButtonShape::Square)
|
||||||
|
|
|
@ -35,6 +35,7 @@ use prompt_store::{PromptId, PromptStore};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use settings::{Settings as _, SettingsStore};
|
use settings::{Settings as _, SettingsStore};
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use std::{collections::BTreeMap, rc::Rc, time::Duration};
|
use std::{collections::BTreeMap, rc::Rc, time::Duration};
|
||||||
|
@ -274,6 +275,7 @@ pub struct AcpThreadView {
|
||||||
edits_expanded: bool,
|
edits_expanded: bool,
|
||||||
plan_expanded: bool,
|
plan_expanded: bool,
|
||||||
editor_expanded: bool,
|
editor_expanded: bool,
|
||||||
|
should_be_following: bool,
|
||||||
editing_message: Option<usize>,
|
editing_message: Option<usize>,
|
||||||
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
||||||
is_loading_contents: bool,
|
is_loading_contents: bool,
|
||||||
|
@ -385,6 +387,7 @@ impl AcpThreadView {
|
||||||
edits_expanded: false,
|
edits_expanded: false,
|
||||||
plan_expanded: false,
|
plan_expanded: false,
|
||||||
editor_expanded: false,
|
editor_expanded: false,
|
||||||
|
should_be_following: false,
|
||||||
history_store,
|
history_store,
|
||||||
hovered_recent_history_item: None,
|
hovered_recent_history_item: None,
|
||||||
prompt_capabilities,
|
prompt_capabilities,
|
||||||
|
@ -472,7 +475,7 @@ impl AcpThreadView {
|
||||||
let action_log = thread.read(cx).action_log().clone();
|
let action_log = thread.read(cx).action_log().clone();
|
||||||
|
|
||||||
this.prompt_capabilities
|
this.prompt_capabilities
|
||||||
.set(connection.prompt_capabilities());
|
.set(thread.read(cx).prompt_capabilities());
|
||||||
|
|
||||||
let count = thread.read(cx).entries().len();
|
let count = thread.read(cx).entries().len();
|
||||||
this.list_state.splice(0..0, count);
|
this.list_state.splice(0..0, count);
|
||||||
|
@ -890,6 +893,8 @@ impl AcpThreadView {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let agent_telemetry_id = self.agent.telemetry_id();
|
||||||
|
|
||||||
self.thread_error.take();
|
self.thread_error.take();
|
||||||
self.editing_message.take();
|
self.editing_message.take();
|
||||||
self.thread_feedback.clear();
|
self.thread_feedback.clear();
|
||||||
|
@ -897,6 +902,13 @@ impl AcpThreadView {
|
||||||
let Some(thread) = self.thread().cloned() else {
|
let Some(thread) = self.thread().cloned() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if self.should_be_following {
|
||||||
|
self.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
self.is_loading_contents = true;
|
self.is_loading_contents = true;
|
||||||
let guard = cx.new(|_| ());
|
let guard = cx.new(|_| ());
|
||||||
|
@ -927,6 +939,9 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
|
telemetry::event!("Agent Message Sent", agent = agent_telemetry_id);
|
||||||
|
|
||||||
thread.send(contents, cx)
|
thread.send(contents, cx)
|
||||||
})?;
|
})?;
|
||||||
send.await
|
send.await
|
||||||
|
@ -938,6 +953,16 @@ impl AcpThreadView {
|
||||||
this.handle_thread_error(err, cx);
|
this.handle_thread_error(err, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
} else {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.should_be_following = this
|
||||||
|
.workspace
|
||||||
|
.update(cx, |workspace, _| {
|
||||||
|
workspace.is_being_followed(CollaboratorId::Agent)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -1144,6 +1169,10 @@ impl AcpThreadView {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AcpThreadEvent::PromptCapabilitiesUpdated => {
|
||||||
|
self.prompt_capabilities
|
||||||
|
.set(thread.read(cx).prompt_capabilities());
|
||||||
|
}
|
||||||
AcpThreadEvent::TokenUsageUpdated => {}
|
AcpThreadEvent::TokenUsageUpdated => {}
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -1223,30 +1252,44 @@ impl AcpThreadView {
|
||||||
pending_auth_method.replace(method.clone());
|
pending_auth_method.replace(method.clone());
|
||||||
let authenticate = connection.authenticate(method, cx);
|
let authenticate = connection.authenticate(method, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
self.auth_task = Some(cx.spawn_in(window, {
|
self.auth_task =
|
||||||
let project = self.project.clone();
|
Some(cx.spawn_in(window, {
|
||||||
let agent = self.agent.clone();
|
let project = self.project.clone();
|
||||||
async move |this, cx| {
|
let agent = self.agent.clone();
|
||||||
let result = authenticate.await;
|
async move |this, cx| {
|
||||||
|
let result = authenticate.await;
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
match &result {
|
||||||
if let Err(err) = result {
|
Ok(_) => telemetry::event!(
|
||||||
this.handle_thread_error(err, cx);
|
"Authenticate Agent Succeeded",
|
||||||
} else {
|
agent = agent.telemetry_id()
|
||||||
this.thread_state = Self::initial_state(
|
),
|
||||||
agent,
|
Err(_) => {
|
||||||
None,
|
telemetry::event!(
|
||||||
this.workspace.clone(),
|
"Authenticate Agent Failed",
|
||||||
project.clone(),
|
agent = agent.telemetry_id(),
|
||||||
window,
|
)
|
||||||
cx,
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
this.auth_task.take()
|
|
||||||
})
|
this.update_in(cx, |this, window, cx| {
|
||||||
.ok();
|
if let Err(err) = result {
|
||||||
}
|
this.handle_thread_error(err, cx);
|
||||||
}));
|
} else {
|
||||||
|
this.thread_state = Self::initial_state(
|
||||||
|
agent,
|
||||||
|
None,
|
||||||
|
this.workspace.clone(),
|
||||||
|
project.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.auth_task.take()
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authorize_tool_call(
|
fn authorize_tool_call(
|
||||||
|
@ -1254,6 +1297,7 @@ impl AcpThreadView {
|
||||||
tool_call_id: acp::ToolCallId,
|
tool_call_id: acp::ToolCallId,
|
||||||
option_id: acp::PermissionOptionId,
|
option_id: acp::PermissionOptionId,
|
||||||
option_kind: acp::PermissionOptionKind,
|
option_kind: acp::PermissionOptionKind,
|
||||||
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let Some(thread) = self.thread() else {
|
let Some(thread) = self.thread() else {
|
||||||
|
@ -1262,6 +1306,13 @@ impl AcpThreadView {
|
||||||
thread.update(cx, |thread, cx| {
|
thread.update(cx, |thread, cx| {
|
||||||
thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
|
thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
|
||||||
});
|
});
|
||||||
|
if self.should_be_following {
|
||||||
|
self.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1305,14 +1356,23 @@ impl AcpThreadView {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let has_checkpoint_button = message
|
||||||
|
.checkpoint
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|checkpoint| checkpoint.show);
|
||||||
|
|
||||||
let agent_name = self.agent.name();
|
let agent_name = self.agent.name();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id(("user_message", entry_ix))
|
.id(("user_message", entry_ix))
|
||||||
.map(|this| if rules_item.is_some() {
|
.map(|this| {
|
||||||
this.pt_3()
|
if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() {
|
||||||
} else {
|
this.pt_4()
|
||||||
this.pt_2()
|
} else if rules_item.is_some() {
|
||||||
|
this.pt_3()
|
||||||
|
} else {
|
||||||
|
this.pt_2()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.pb_4()
|
.pb_4()
|
||||||
.px_2()
|
.px_2()
|
||||||
|
@ -1492,12 +1552,11 @@ impl AcpThreadView {
|
||||||
return primary;
|
return primary;
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
let primary = if entry_ix == total_entries - 1 {
|
||||||
let primary = if entry_ix == total_entries - 1 && !is_generating {
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(primary)
|
.child(primary)
|
||||||
.child(self.render_thread_controls(cx))
|
.child(self.render_thread_controls(&thread, cx))
|
||||||
.when_some(
|
.when_some(
|
||||||
self.thread_feedback.comments_editor.clone(),
|
self.thread_feedback.comments_editor.clone(),
|
||||||
|this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
|
|this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
|
||||||
|
@ -1639,15 +1698,16 @@ impl AcpThreadView {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_tool_call_icon(
|
fn render_tool_call(
|
||||||
&self,
|
&self,
|
||||||
group_name: SharedString,
|
|
||||||
entry_ix: usize,
|
entry_ix: usize,
|
||||||
is_collapsible: bool,
|
|
||||||
is_open: bool,
|
|
||||||
tool_call: &ToolCall,
|
tool_call: &ToolCall,
|
||||||
|
window: &Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> Div {
|
) -> Div {
|
||||||
|
let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
|
||||||
|
let card_header_id = SharedString::from("inner-tool-call-header");
|
||||||
|
|
||||||
let tool_icon =
|
let tool_icon =
|
||||||
if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 {
|
if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 {
|
||||||
FileIcons::get_icon(&tool_call.locations[0].path, cx)
|
FileIcons::get_icon(&tool_call.locations[0].path, cx)
|
||||||
|
@ -1655,7 +1715,7 @@ impl AcpThreadView {
|
||||||
.unwrap_or(Icon::new(IconName::ToolPencil))
|
.unwrap_or(Icon::new(IconName::ToolPencil))
|
||||||
} else {
|
} else {
|
||||||
Icon::new(match tool_call.kind {
|
Icon::new(match tool_call.kind {
|
||||||
acp::ToolKind::Read => IconName::ToolRead,
|
acp::ToolKind::Read => IconName::ToolSearch,
|
||||||
acp::ToolKind::Edit => IconName::ToolPencil,
|
acp::ToolKind::Edit => IconName::ToolPencil,
|
||||||
acp::ToolKind::Delete => IconName::ToolDeleteFile,
|
acp::ToolKind::Delete => IconName::ToolDeleteFile,
|
||||||
acp::ToolKind::Move => IconName::ArrowRightLeft,
|
acp::ToolKind::Move => IconName::ArrowRightLeft,
|
||||||
|
@ -1669,59 +1729,6 @@ impl AcpThreadView {
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
.color(Color::Muted);
|
.color(Color::Muted);
|
||||||
|
|
||||||
let base_container = h_flex().flex_shrink_0().size_4().justify_center();
|
|
||||||
|
|
||||||
if is_collapsible {
|
|
||||||
base_container
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.group_hover(&group_name, |s| s.invisible().w_0())
|
|
||||||
.child(tool_icon),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.absolute()
|
|
||||||
.inset_0()
|
|
||||||
.invisible()
|
|
||||||
.justify_center()
|
|
||||||
.group_hover(&group_name, |s| s.visible())
|
|
||||||
.child(
|
|
||||||
Disclosure::new(("expand", entry_ix), is_open)
|
|
||||||
.opened_icon(IconName::ChevronUp)
|
|
||||||
.closed_icon(IconName::ChevronRight)
|
|
||||||
.on_click(cx.listener({
|
|
||||||
let id = tool_call.id.clone();
|
|
||||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
|
||||||
if is_open {
|
|
||||||
this.expanded_tool_calls.remove(&id);
|
|
||||||
} else {
|
|
||||||
this.expanded_tool_calls.insert(id.clone());
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
base_container.child(tool_icon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_tool_call(
|
|
||||||
&self,
|
|
||||||
entry_ix: usize,
|
|
||||||
tool_call: &ToolCall,
|
|
||||||
window: &Window,
|
|
||||||
cx: &Context<Self>,
|
|
||||||
) -> Div {
|
|
||||||
let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
|
|
||||||
let card_header_id = SharedString::from("inner-tool-call-header");
|
|
||||||
|
|
||||||
let in_progress = match &tool_call.status {
|
|
||||||
ToolCallStatus::InProgress => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let failed_or_canceled = match &tool_call.status {
|
let failed_or_canceled = match &tool_call.status {
|
||||||
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
|
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
|
@ -1821,6 +1828,7 @@ impl AcpThreadView {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(header_id)
|
.id(header_id)
|
||||||
|
.group(&card_header_id)
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.max_w_full()
|
.max_w_full()
|
||||||
|
@ -1838,19 +1846,11 @@ impl AcpThreadView {
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.group(&card_header_id)
|
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.h(window.line_height() - px(2.))
|
.h(window.line_height() - px(2.))
|
||||||
.text_size(self.tool_name_font_size())
|
.text_size(self.tool_name_font_size())
|
||||||
.child(self.render_tool_call_icon(
|
.child(tool_icon)
|
||||||
card_header_id,
|
|
||||||
entry_ix,
|
|
||||||
is_collapsible,
|
|
||||||
is_open,
|
|
||||||
tool_call,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
.child(if tool_call.locations.len() == 1 {
|
.child(if tool_call.locations.len() == 1 {
|
||||||
let name = tool_call.locations[0]
|
let name = tool_call.locations[0]
|
||||||
.path
|
.path
|
||||||
|
@ -1878,13 +1878,13 @@ impl AcpThreadView {
|
||||||
})
|
})
|
||||||
.child(name)
|
.child(name)
|
||||||
.tooltip(Tooltip::text("Jump to File"))
|
.tooltip(Tooltip::text("Jump to File"))
|
||||||
|
.cursor(gpui::CursorStyle::PointingHand)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.open_tool_call_location(entry_ix, 0, window, cx);
|
this.open_tool_call_location(entry_ix, 0, window, cx);
|
||||||
}))
|
}))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("non-card-label-container")
|
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.max_w_full()
|
.max_w_full()
|
||||||
|
@ -1895,47 +1895,39 @@ impl AcpThreadView {
|
||||||
default_markdown_style(false, true, window, cx),
|
default_markdown_style(false, true, window, cx),
|
||||||
)))
|
)))
|
||||||
.child(gradient_overlay(gradient_color))
|
.child(gradient_overlay(gradient_color))
|
||||||
.on_click(cx.listener({
|
|
||||||
let id = tool_call.id.clone();
|
|
||||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
|
||||||
if is_open {
|
|
||||||
this.expanded_tool_calls.remove(&id);
|
|
||||||
} else {
|
|
||||||
this.expanded_tool_calls.insert(id.clone());
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.into_any()
|
.into_any()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.when(in_progress && use_card_layout && !is_open, |this| {
|
.child(
|
||||||
this.child(
|
h_flex()
|
||||||
div().absolute().right_2().child(
|
.gap_px()
|
||||||
Icon::new(IconName::ArrowCircle)
|
.when(is_collapsible, |this| {
|
||||||
.color(Color::Muted)
|
this.child(
|
||||||
.size(IconSize::Small)
|
Disclosure::new(("expand", entry_ix), is_open)
|
||||||
.with_animation(
|
.opened_icon(IconName::ChevronUp)
|
||||||
"running",
|
.closed_icon(IconName::ChevronDown)
|
||||||
Animation::new(Duration::from_secs(3)).repeat(),
|
.visible_on_hover(&card_header_id)
|
||||||
|icon, delta| {
|
.on_click(cx.listener({
|
||||||
icon.transform(Transformation::rotate(percentage(
|
let id = tool_call.id.clone();
|
||||||
delta,
|
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||||
)))
|
if is_open {
|
||||||
},
|
this.expanded_tool_calls.remove(&id);
|
||||||
),
|
} else {
|
||||||
),
|
this.expanded_tool_calls.insert(id.clone());
|
||||||
)
|
}
|
||||||
})
|
cx.notify();
|
||||||
.when(failed_or_canceled, |this| {
|
}
|
||||||
this.child(
|
})),
|
||||||
div().absolute().right_2().child(
|
)
|
||||||
Icon::new(IconName::Close)
|
})
|
||||||
.color(Color::Error)
|
.when(failed_or_canceled, |this| {
|
||||||
.size(IconSize::Small),
|
this.child(
|
||||||
),
|
Icon::new(IconName::Close)
|
||||||
)
|
.color(Color::Error)
|
||||||
}),
|
.size(IconSize::Small),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.children(tool_output_display)
|
.children(tool_output_display)
|
||||||
}
|
}
|
||||||
|
@ -2005,9 +1997,27 @@ impl AcpThreadView {
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let uri: SharedString = resource_link.uri.clone().into();
|
let uri: SharedString = resource_link.uri.clone().into();
|
||||||
|
let is_file = resource_link.uri.strip_prefix("file://");
|
||||||
|
|
||||||
let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") {
|
let label: SharedString = if let Some(abs_path) = is_file {
|
||||||
path.to_string().into()
|
if let Some(project_path) = self
|
||||||
|
.project
|
||||||
|
.read(cx)
|
||||||
|
.project_path_for_absolute_path(&Path::new(abs_path), cx)
|
||||||
|
&& let Some(worktree) = self
|
||||||
|
.project
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(project_path.worktree_id, cx)
|
||||||
|
{
|
||||||
|
worktree
|
||||||
|
.read(cx)
|
||||||
|
.full_path(&project_path.path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
abs_path.to_string().into()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
uri.clone()
|
uri.clone()
|
||||||
};
|
};
|
||||||
|
@ -2024,10 +2034,12 @@ impl AcpThreadView {
|
||||||
Button::new(button_id, label)
|
Button::new(button_id, label)
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.icon(IconName::ArrowUpRight)
|
|
||||||
.icon_size(IconSize::XSmall)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
|
.when(is_file.is_none(), |this| {
|
||||||
|
this.icon(IconName::ArrowUpRight)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
})
|
||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
move |_, _, window, cx: &mut Context<Self>| {
|
move |_, _, window, cx: &mut Context<Self>| {
|
||||||
|
@ -2086,11 +2098,12 @@ impl AcpThreadView {
|
||||||
let tool_call_id = tool_call_id.clone();
|
let tool_call_id = tool_call_id.clone();
|
||||||
let option_id = option.id.clone();
|
let option_id = option.id.clone();
|
||||||
let option_kind = option.kind;
|
let option_kind = option.kind;
|
||||||
move |this, _, _, cx| {
|
move |this, _, window, cx| {
|
||||||
this.authorize_tool_call(
|
this.authorize_tool_call(
|
||||||
tool_call_id.clone(),
|
tool_call_id.clone(),
|
||||||
option_id.clone(),
|
option_id.clone(),
|
||||||
option_kind,
|
option_kind,
|
||||||
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2735,6 +2748,12 @@ impl AcpThreadView {
|
||||||
.on_click({
|
.on_click({
|
||||||
let method_id = method.id.clone();
|
let method_id = method.id.clone();
|
||||||
cx.listener(move |this, _, window, cx| {
|
cx.listener(move |this, _, window, cx| {
|
||||||
|
telemetry::event!(
|
||||||
|
"Authenticate Agent Started",
|
||||||
|
agent = this.agent.telemetry_id(),
|
||||||
|
method = method_id
|
||||||
|
);
|
||||||
|
|
||||||
this.authenticate(method_id.clone(), window, cx)
|
this.authenticate(method_id.clone(), window, cx)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -2763,6 +2782,8 @@ impl AcpThreadView {
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id());
|
||||||
|
|
||||||
let task = this
|
let task = this
|
||||||
.workspace
|
.workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
|
@ -2770,7 +2791,7 @@ impl AcpThreadView {
|
||||||
let cwd = project.first_project_directory(cx);
|
let cwd = project.first_project_directory(cx);
|
||||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||||
let spawn_in_terminal = task::SpawnInTerminal {
|
let spawn_in_terminal = task::SpawnInTerminal {
|
||||||
id: task::TaskId("install".to_string()),
|
id: task::TaskId(install_command.clone()),
|
||||||
full_label: install_command.clone(),
|
full_label: install_command.clone(),
|
||||||
label: install_command.clone(),
|
label: install_command.clone(),
|
||||||
command: Some(install_command.clone()),
|
command: Some(install_command.clone()),
|
||||||
|
@ -2820,6 +2841,8 @@ impl AcpThreadView {
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id());
|
||||||
|
|
||||||
let task = this
|
let task = this
|
||||||
.workspace
|
.workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
|
@ -2827,7 +2850,7 @@ impl AcpThreadView {
|
||||||
let cwd = project.first_project_directory(cx);
|
let cwd = project.first_project_directory(cx);
|
||||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||||
let spawn_in_terminal = task::SpawnInTerminal {
|
let spawn_in_terminal = task::SpawnInTerminal {
|
||||||
id: task::TaskId("upgrade".to_string()),
|
id: task::TaskId(upgrade_command.to_string()),
|
||||||
full_label: upgrade_command.clone(),
|
full_label: upgrade_command.clone(),
|
||||||
label: upgrade_command.clone(),
|
label: upgrade_command.clone(),
|
||||||
command: Some(upgrade_command.clone()),
|
command: Some(upgrade_command.clone()),
|
||||||
|
@ -3643,13 +3666,53 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_following(&self, cx: &App) -> bool {
|
||||||
|
match self.thread().map(|thread| thread.read(cx).status()) {
|
||||||
|
Some(ThreadStatus::Generating) => self
|
||||||
|
.workspace
|
||||||
|
.read_with(cx, |workspace, _| {
|
||||||
|
workspace.is_being_followed(CollaboratorId::Agent)
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
|
_ => self.should_be_following,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_following(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let following = self.is_following(cx);
|
||||||
|
|
||||||
|
self.should_be_following = !following;
|
||||||
|
if self.thread().map(|thread| thread.read(cx).status()) == Some(ThreadStatus::Generating) {
|
||||||
|
self.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
if following {
|
||||||
|
workspace.unfollow(CollaboratorId::Agent, window, cx);
|
||||||
|
} else {
|
||||||
|
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetry::event!("Follow Agent Selected", following = !following);
|
||||||
|
}
|
||||||
|
|
||||||
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let following = self
|
let following = self.is_following(cx);
|
||||||
.workspace
|
|
||||||
.read_with(cx, |workspace, _| {
|
let tooltip_label = if following {
|
||||||
workspace.is_being_followed(CollaboratorId::Agent)
|
if self.agent.name() == "Zed Agent" {
|
||||||
})
|
format!("Stop Following the {}", self.agent.name())
|
||||||
.unwrap_or(false);
|
} else {
|
||||||
|
format!("Stop Following {}", self.agent.name())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.agent.name() == "Zed Agent" {
|
||||||
|
format!("Follow the {}", self.agent.name())
|
||||||
|
} else {
|
||||||
|
format!("Follow {}", self.agent.name())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
IconButton::new("follow-agent", IconName::Crosshair)
|
IconButton::new("follow-agent", IconName::Crosshair)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
|
@ -3658,10 +3721,10 @@ impl AcpThreadView {
|
||||||
.selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
|
.selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
|
||||||
.tooltip(move |window, cx| {
|
.tooltip(move |window, cx| {
|
||||||
if following {
|
if following {
|
||||||
Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
|
Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx)
|
||||||
} else {
|
} else {
|
||||||
Tooltip::with_meta(
|
Tooltip::with_meta(
|
||||||
"Follow Agent",
|
tooltip_label.clone(),
|
||||||
Some(&Follow),
|
Some(&Follow),
|
||||||
"Track the agent's location as it reads and edits files.",
|
"Track the agent's location as it reads and edits files.",
|
||||||
window,
|
window,
|
||||||
|
@ -3670,15 +3733,7 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.workspace
|
this.toggle_following(window, cx);
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
if following {
|
|
||||||
workspace.unfollow(CollaboratorId::Agent, window, cx);
|
|
||||||
} else {
|
|
||||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4090,7 +4145,20 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_thread_controls(&self, cx: &Context<Self>) -> impl IntoElement {
|
fn render_thread_controls(
|
||||||
|
&self,
|
||||||
|
thread: &Entity<AcpThread>,
|
||||||
|
cx: &Context<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
||||||
|
if is_generating {
|
||||||
|
return h_flex().id("thread-controls-container").ml_1().child(
|
||||||
|
div()
|
||||||
|
.py_2()
|
||||||
|
.px(rems_from_px(22.))
|
||||||
|
.child(SpinnerLabel::new().size(LabelSize::Small)),
|
||||||
|
);
|
||||||
|
}
|
||||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.shape(ui::IconButtonShape::Square)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
|
@ -4696,6 +4764,24 @@ impl AcpThreadView {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let agent = self.agent.clone();
|
||||||
|
let ThreadState::Ready { thread, .. } = &self.thread_state else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let connection = thread.read(cx).connection().clone();
|
||||||
|
let err = AuthRequired {
|
||||||
|
description: None,
|
||||||
|
provider_id: None,
|
||||||
|
};
|
||||||
|
self.clear_thread_error(cx);
|
||||||
|
let this = cx.weak_entity();
|
||||||
|
window.defer(cx, |window, cx| {
|
||||||
|
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn upgrade_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
Button::new("upgrade", "Upgrade")
|
Button::new("upgrade", "Upgrade")
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
|
@ -4796,45 +4882,30 @@ impl Render for AcpThreadView {
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.child(self.render_load_error(e, cx)),
|
.child(self.render_load_error(e, cx)),
|
||||||
ThreadState::Ready { thread, .. } => {
|
ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
|
||||||
let thread_clone = thread.clone();
|
if has_messages {
|
||||||
|
this.child(
|
||||||
v_flex().flex_1().map(|this| {
|
list(
|
||||||
if has_messages {
|
self.list_state.clone(),
|
||||||
this.child(
|
cx.processor(|this, index: usize, window, cx| {
|
||||||
list(
|
let Some((entry, len)) = this.thread().and_then(|thread| {
|
||||||
self.list_state.clone(),
|
let entries = &thread.read(cx).entries();
|
||||||
cx.processor(|this, index: usize, window, cx| {
|
Some((entries.get(index)?, entries.len()))
|
||||||
let Some((entry, len)) = this.thread().and_then(|thread| {
|
}) else {
|
||||||
let entries = &thread.read(cx).entries();
|
return Empty.into_any();
|
||||||
Some((entries.get(index)?, entries.len()))
|
};
|
||||||
}) else {
|
this.render_entry(index, len, entry, window, cx)
|
||||||
return Empty.into_any();
|
}),
|
||||||
};
|
|
||||||
this.render_entry(index, len, entry, window, cx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
|
|
||||||
.flex_grow()
|
|
||||||
.into_any(),
|
|
||||||
)
|
)
|
||||||
.child(self.render_vertical_scrollbar(cx))
|
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
|
||||||
.children(
|
.flex_grow()
|
||||||
match thread_clone.read(cx).status() {
|
.into_any(),
|
||||||
ThreadStatus::Idle
|
)
|
||||||
| ThreadStatus::WaitingForToolConfirmation => None,
|
.child(self.render_vertical_scrollbar(cx))
|
||||||
ThreadStatus::Generating => div()
|
} else {
|
||||||
.py_2()
|
this.child(self.render_recent_history(window, cx))
|
||||||
.px(rems_from_px(22.))
|
}
|
||||||
.child(SpinnerLabel::new().size(LabelSize::Small))
|
}),
|
||||||
.into(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.child(self.render_recent_history(window, cx))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
// The activity bar is intentionally rendered outside of the ThreadState::Ready match
|
// The activity bar is intentionally rendered outside of the ThreadState::Ready match
|
||||||
// above so that the scrollbar doesn't render behind it. The current setup allows
|
// above so that the scrollbar doesn't render behind it. The current setup allows
|
||||||
|
@ -5251,6 +5322,10 @@ pub(crate) mod tests {
|
||||||
where
|
where
|
||||||
C: 'static + AgentConnection + Send + Clone,
|
C: 'static + AgentConnection + Send + Clone,
|
||||||
{
|
{
|
||||||
|
fn telemetry_id(&self) -> &'static str {
|
||||||
|
"test"
|
||||||
|
}
|
||||||
|
|
||||||
fn logo(&self) -> ui::IconName {
|
fn logo(&self) -> ui::IconName {
|
||||||
ui::IconName::Ai
|
ui::IconName::Ai
|
||||||
}
|
}
|
||||||
|
@ -5299,6 +5374,12 @@ pub(crate) mod tests {
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
SessionId("test".into()),
|
SessionId("test".into()),
|
||||||
|
watch::Receiver::constant(acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: true,
|
||||||
|
embedded_context: true,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
@ -5307,14 +5388,6 @@ pub(crate) mod tests {
|
||||||
&[]
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: true,
|
|
||||||
embedded_context: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn authenticate(
|
fn authenticate(
|
||||||
&self,
|
&self,
|
||||||
_method_id: acp::AuthMethodId,
|
_method_id: acp::AuthMethodId,
|
||||||
|
|
|
@ -5,6 +5,7 @@ mod tool_picker;
|
||||||
|
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||||
use cloud_llm_client::Plan;
|
use cloud_llm_client::Plan;
|
||||||
|
@ -15,7 +16,7 @@ use extension_host::ExtensionStore;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
|
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
|
||||||
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||||
};
|
};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
|
@ -23,10 +24,11 @@ use language_model::{
|
||||||
};
|
};
|
||||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||||
use project::{
|
use project::{
|
||||||
|
Project,
|
||||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||||
project_settings::{ContextServerSettings, ProjectSettings},
|
project_settings::{ContextServerSettings, ProjectSettings},
|
||||||
};
|
};
|
||||||
use settings::{Settings, update_settings_file};
|
use settings::{Settings, SettingsStore, update_settings_file};
|
||||||
use ui::{
|
use ui::{
|
||||||
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
||||||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||||
|
@ -39,7 +41,7 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||||
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer,
|
AddContextServer, ExternalAgent, NewExternalAgentThread,
|
||||||
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -47,6 +49,7 @@ pub struct AgentConfiguration {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
project: WeakEntity<Project>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||||
context_server_store: Entity<ContextServerStore>,
|
context_server_store: Entity<ContextServerStore>,
|
||||||
|
@ -56,6 +59,8 @@ pub struct AgentConfiguration {
|
||||||
_registry_subscription: Subscription,
|
_registry_subscription: Subscription,
|
||||||
scroll_handle: ScrollHandle,
|
scroll_handle: ScrollHandle,
|
||||||
scrollbar_state: ScrollbarState,
|
scrollbar_state: ScrollbarState,
|
||||||
|
gemini_is_installed: bool,
|
||||||
|
_check_for_gemini: Task<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentConfiguration {
|
impl AgentConfiguration {
|
||||||
|
@ -65,6 +70,7 @@ impl AgentConfiguration {
|
||||||
tools: Entity<ToolWorkingSet>,
|
tools: Entity<ToolWorkingSet>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
project: WeakEntity<Project>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -89,6 +95,11 @@ impl AgentConfiguration {
|
||||||
|
|
||||||
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
|
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
|
||||||
.detach();
|
.detach();
|
||||||
|
cx.observe_global_in::<SettingsStore>(window, |this, _, cx| {
|
||||||
|
this.check_for_gemini(cx);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
let scroll_handle = ScrollHandle::new();
|
let scroll_handle = ScrollHandle::new();
|
||||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||||
|
@ -97,6 +108,7 @@ impl AgentConfiguration {
|
||||||
fs,
|
fs,
|
||||||
language_registry,
|
language_registry,
|
||||||
workspace,
|
workspace,
|
||||||
|
project,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
configuration_views_by_provider: HashMap::default(),
|
configuration_views_by_provider: HashMap::default(),
|
||||||
context_server_store,
|
context_server_store,
|
||||||
|
@ -106,8 +118,11 @@ impl AgentConfiguration {
|
||||||
_registry_subscription: registry_subscription,
|
_registry_subscription: registry_subscription,
|
||||||
scroll_handle,
|
scroll_handle,
|
||||||
scrollbar_state,
|
scrollbar_state,
|
||||||
|
gemini_is_installed: false,
|
||||||
|
_check_for_gemini: Task::ready(()),
|
||||||
};
|
};
|
||||||
this.build_provider_configuration_views(window, cx);
|
this.build_provider_configuration_views(window, cx);
|
||||||
|
this.check_for_gemini(cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +152,34 @@ impl AgentConfiguration {
|
||||||
self.configuration_views_by_provider
|
self.configuration_views_by_provider
|
||||||
.insert(provider.id(), configuration_view);
|
.insert(provider.id(), configuration_view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_for_gemini(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let project = self.project.clone();
|
||||||
|
let settings = AllAgentServersSettings::get_global(cx).clone();
|
||||||
|
self._check_for_gemini = cx.spawn({
|
||||||
|
async move |this, cx| {
|
||||||
|
let Some(project) = project.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let gemini_is_installed = AgentServerCommand::resolve(
|
||||||
|
Gemini::binary_name(),
|
||||||
|
&[],
|
||||||
|
// TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here
|
||||||
|
None,
|
||||||
|
settings.gemini,
|
||||||
|
&project,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_some();
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.gemini_is_installed = gemini_is_installed;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for AgentConfiguration {
|
impl Focusable for AgentConfiguration {
|
||||||
|
@ -211,7 +254,6 @@ impl AgentConfiguration {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(provider_id_string.clone())
|
.id(provider_id_string.clone())
|
||||||
.cursor_pointer()
|
|
||||||
.px_2()
|
.px_2()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
@ -231,10 +273,7 @@ impl AgentConfiguration {
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(Label::new(provider_name.clone()))
|
||||||
Label::new(provider_name.clone())
|
|
||||||
.size(LabelSize::Large),
|
|
||||||
)
|
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if is_zed_provider && is_signed_in {
|
if is_zed_provider && is_signed_in {
|
||||||
this.child(
|
this.child(
|
||||||
|
@ -279,7 +318,7 @@ impl AgentConfiguration {
|
||||||
"Start New Thread",
|
"Start New Thread",
|
||||||
)
|
)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Thread)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
|
@ -378,7 +417,7 @@ impl AgentConfiguration {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Label::new("Add at least one provider to use AI-powered features.")
|
Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -519,6 +558,14 @@ impl AgentConfiguration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
|
||||||
|
cx.theme().colors().background.opacity(0.25)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
|
||||||
|
cx.theme().colors().border.opacity(0.6)
|
||||||
|
}
|
||||||
|
|
||||||
fn render_context_servers_section(
|
fn render_context_servers_section(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
@ -536,7 +583,12 @@ impl AgentConfiguration {
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
||||||
.child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)),
|
.child(
|
||||||
|
Label::new(
|
||||||
|
"All context servers connected through the Model Context Protocol.",
|
||||||
|
)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.children(
|
.children(
|
||||||
context_server_ids.into_iter().map(|context_server_id| {
|
context_server_ids.into_iter().map(|context_server_id| {
|
||||||
|
@ -546,7 +598,7 @@ impl AgentConfiguration {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.gap_2()
|
.gap_1p5()
|
||||||
.child(
|
.child(
|
||||||
h_flex().w_full().child(
|
h_flex().w_full().child(
|
||||||
Button::new("add-context-server", "Add Custom Server")
|
Button::new("add-context-server", "Add Custom Server")
|
||||||
|
@ -637,8 +689,6 @@ impl AgentConfiguration {
|
||||||
.map_or([].as_slice(), |tools| tools.as_slice());
|
.map_or([].as_slice(), |tools| tools.as_slice());
|
||||||
let tool_count = tools.len();
|
let tool_count = tools.len();
|
||||||
|
|
||||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
|
||||||
|
|
||||||
let (source_icon, source_tooltip) = if is_from_extension {
|
let (source_icon, source_tooltip) = if is_from_extension {
|
||||||
(
|
(
|
||||||
IconName::ZedMcpExtension,
|
IconName::ZedMcpExtension,
|
||||||
|
@ -781,8 +831,8 @@ impl AgentConfiguration {
|
||||||
.id(item_id.clone())
|
.id(item_id.clone())
|
||||||
.border_1()
|
.border_1()
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
.border_color(border_color)
|
.border_color(self.card_item_border_color(cx))
|
||||||
.bg(cx.theme().colors().background.opacity(0.2))
|
.bg(self.card_item_bg_color(cx))
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -790,7 +840,11 @@ impl AgentConfiguration {
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.when(
|
.when(
|
||||||
error.is_some() || are_tools_expanded && tool_count >= 1,
|
error.is_some() || are_tools_expanded && tool_count >= 1,
|
||||||
|element| element.border_b_1().border_color(border_color),
|
|element| {
|
||||||
|
element
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(self.card_item_border_color(cx))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -972,6 +1026,166 @@ impl AgentConfiguration {
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let settings = AllAgentServersSettings::get_global(cx).clone();
|
||||||
|
let user_defined_agents = settings
|
||||||
|
.custom
|
||||||
|
.iter()
|
||||||
|
.map(|(name, settings)| {
|
||||||
|
self.render_agent_server(
|
||||||
|
IconName::Ai,
|
||||||
|
name.clone(),
|
||||||
|
ExternalAgent::Custom {
|
||||||
|
name: name.clone(),
|
||||||
|
settings: settings.clone(),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.p(DynamicSpacing::Base16.rems(cx))
|
||||||
|
.pr(DynamicSpacing::Base20.rems(cx))
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(Headline::new("External Agents"))
|
||||||
|
.child(
|
||||||
|
Label::new(
|
||||||
|
"Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
|
||||||
|
)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(self.render_agent_server(
|
||||||
|
IconName::AiGemini,
|
||||||
|
"Gemini CLI",
|
||||||
|
ExternalAgent::Gemini,
|
||||||
|
(!self.gemini_is_installed).then_some(Gemini::install_command().into()),
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
// TODO add CC
|
||||||
|
.children(user_defined_agents),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_agent_server(
|
||||||
|
&self,
|
||||||
|
icon: IconName,
|
||||||
|
name: impl Into<SharedString>,
|
||||||
|
agent: ExternalAgent,
|
||||||
|
install_command: Option<SharedString>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let name = name.into();
|
||||||
|
h_flex()
|
||||||
|
.p_1()
|
||||||
|
.pl_2()
|
||||||
|
.gap_1p5()
|
||||||
|
.justify_between()
|
||||||
|
.border_1()
|
||||||
|
.rounded_md()
|
||||||
|
.border_color(self.card_item_border_color(cx))
|
||||||
|
.bg(self.card_item_bg_color(cx))
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||||
|
.child(Label::new(name.clone())),
|
||||||
|
)
|
||||||
|
.map(|this| {
|
||||||
|
if let Some(install_command) = install_command {
|
||||||
|
this.child(
|
||||||
|
Button::new(
|
||||||
|
SharedString::from(format!("install_external_agent-{name}")),
|
||||||
|
"Install Agent",
|
||||||
|
)
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.icon_position(IconPosition::Start)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.tooltip(Tooltip::text(install_command.clone()))
|
||||||
|
.on_click(cx.listener(
|
||||||
|
move |this, _, window, cx| {
|
||||||
|
let Some(project) = this.project.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(workspace) = this.workspace.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let cwd = project.read(cx).first_project_directory(cx);
|
||||||
|
let shell =
|
||||||
|
project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
||||||
|
let spawn_in_terminal = task::SpawnInTerminal {
|
||||||
|
id: task::TaskId(install_command.to_string()),
|
||||||
|
full_label: install_command.to_string(),
|
||||||
|
label: install_command.to_string(),
|
||||||
|
command: Some(install_command.to_string()),
|
||||||
|
args: Vec::new(),
|
||||||
|
command_label: install_command.to_string(),
|
||||||
|
cwd,
|
||||||
|
env: Default::default(),
|
||||||
|
use_new_terminal: true,
|
||||||
|
allow_concurrent_runs: true,
|
||||||
|
reveal: Default::default(),
|
||||||
|
reveal_target: Default::default(),
|
||||||
|
hide: Default::default(),
|
||||||
|
shell,
|
||||||
|
show_summary: true,
|
||||||
|
show_command: true,
|
||||||
|
show_rerun: false,
|
||||||
|
};
|
||||||
|
let task = workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
|
||||||
|
});
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
task.await;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.check_for_gemini(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.child(
|
||||||
|
h_flex().gap_1().child(
|
||||||
|
Button::new(
|
||||||
|
SharedString::from(format!("start_acp_thread-{name}")),
|
||||||
|
"Start New Thread",
|
||||||
|
)
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.icon(IconName::Thread)
|
||||||
|
.icon_position(IconPosition::Start)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
window.dispatch_action(
|
||||||
|
NewExternalAgentThread {
|
||||||
|
agent: Some(agent.clone()),
|
||||||
|
}
|
||||||
|
.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for AgentConfiguration {
|
impl Render for AgentConfiguration {
|
||||||
|
@ -991,6 +1205,7 @@ impl Render for AgentConfiguration {
|
||||||
.size_full()
|
.size_full()
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
.child(self.render_general_settings_section(cx))
|
.child(self.render_general_settings_section(cx))
|
||||||
|
.child(self.render_agent_servers_section(cx))
|
||||||
.child(self.render_context_servers_section(window, cx))
|
.child(self.render_context_servers_section(window, cx))
|
||||||
.child(self.render_provider_configuration_section(cx)),
|
.child(self.render_provider_configuration_section(cx)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1529,6 +1529,7 @@ impl AgentDiff {
|
||||||
| AcpThreadEvent::TokenUsageUpdated
|
| AcpThreadEvent::TokenUsageUpdated
|
||||||
| AcpThreadEvent::EntriesRemoved(_)
|
| AcpThreadEvent::EntriesRemoved(_)
|
||||||
| AcpThreadEvent::ToolAuthorizationRequired
|
| AcpThreadEvent::ToolAuthorizationRequired
|
||||||
|
| AcpThreadEvent::PromptCapabilitiesUpdated
|
||||||
| AcpThreadEvent::Retry(_) => {}
|
| AcpThreadEvent::Retry(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ use agent_servers::AgentServerSettings;
|
||||||
use agent2::{DbThreadMetadata, HistoryEntry};
|
use agent2::{DbThreadMetadata, HistoryEntry};
|
||||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use zed_actions::OpenBrowser;
|
||||||
|
use zed_actions::agent::ReauthenticateAgent;
|
||||||
|
|
||||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||||
use crate::agent_diff::AgentDiffThread;
|
use crate::agent_diff::AgentDiffThread;
|
||||||
|
@ -247,6 +249,7 @@ enum WhichFontSize {
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO unify this with ExternalAgent
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum AgentType {
|
pub enum AgentType {
|
||||||
#[default]
|
#[default]
|
||||||
|
@ -1031,6 +1034,8 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
telemetry::event!("Agent Thread Started", agent = "zed-text");
|
||||||
|
|
||||||
let context = self
|
let context = self
|
||||||
.context_store
|
.context_store
|
||||||
.update(cx, |context_store, cx| context_store.create(cx));
|
.update(cx, |context_store, cx| context_store.create(cx));
|
||||||
|
@ -1123,6 +1128,8 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
|
||||||
|
|
||||||
let server = ext_agent.server(fs, history);
|
let server = ext_agent.server(fs, history);
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
@ -1480,6 +1487,7 @@ impl AgentPanel {
|
||||||
tools,
|
tools,
|
||||||
self.language_registry.clone(),
|
self.language_registry.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
|
self.project.downgrade(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -2211,6 +2219,8 @@ impl AgentPanel {
|
||||||
"Enable Full Screen"
|
"Enable Full Screen"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let selected_agent = self.selected_agent.clone();
|
||||||
|
|
||||||
PopoverMenu::new("agent-options-menu")
|
PopoverMenu::new("agent-options-menu")
|
||||||
.trigger_with_tooltip(
|
.trigger_with_tooltip(
|
||||||
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
||||||
|
@ -2290,6 +2300,11 @@ impl AgentPanel {
|
||||||
.action("Settings", Box::new(OpenSettings))
|
.action("Settings", Box::new(OpenSettings))
|
||||||
.separator()
|
.separator()
|
||||||
.action(full_screen_label, Box::new(ToggleZoom));
|
.action(full_screen_label, Box::new(ToggleZoom));
|
||||||
|
|
||||||
|
if selected_agent == AgentType::Gemini {
|
||||||
|
menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
|
||||||
|
}
|
||||||
|
|
||||||
menu
|
menu
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -2324,6 +2339,8 @@ impl AgentPanel {
|
||||||
.menu({
|
.menu({
|
||||||
let menu = self.assistant_navigation_menu.clone();
|
let menu = self.assistant_navigation_menu.clone();
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
|
telemetry::event!("View Thread History Clicked");
|
||||||
|
|
||||||
if let Some(menu) = menu.as_ref() {
|
if let Some(menu) = menu.as_ref() {
|
||||||
menu.update(cx, |_, cx| {
|
menu.update(cx, |_, cx| {
|
||||||
cx.defer_in(window, |menu, window, cx| {
|
cx.defer_in(window, |menu, window, cx| {
|
||||||
|
@ -2502,6 +2519,8 @@ impl AgentPanel {
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
|
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
|
telemetry::event!("New Thread Clicked");
|
||||||
|
|
||||||
let active_thread = active_thread.clone();
|
let active_thread = active_thread.clone();
|
||||||
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||||
menu = menu
|
menu = menu
|
||||||
|
@ -2678,6 +2697,15 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
menu
|
menu
|
||||||
|
})
|
||||||
|
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
|
||||||
|
menu.separator().link(
|
||||||
|
"Add Your Own Agent",
|
||||||
|
OpenBrowser {
|
||||||
|
url: "https://agentclientprotocol.com/".into(),
|
||||||
|
}
|
||||||
|
.boxed_clone(),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
menu
|
menu
|
||||||
}))
|
}))
|
||||||
|
@ -3758,6 +3786,11 @@ impl Render for AgentPanel {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.on_action(cx.listener(Self::toggle_burn_mode))
|
.on_action(cx.listener(Self::toggle_burn_mode))
|
||||||
|
.on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
|
||||||
|
if let Some(thread_view) = this.active_thread_view() {
|
||||||
|
thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
|
||||||
|
}
|
||||||
|
}))
|
||||||
.child(self.render_toolbar(window, cx))
|
.child(self.render_toolbar(window, cx))
|
||||||
.children(self.render_onboarding(window, cx))
|
.children(self.render_onboarding(window, cx))
|
||||||
.map(|parent| match &self.active_view {
|
.map(|parent| match &self.active_view {
|
||||||
|
|
|
@ -160,6 +160,7 @@ pub struct NewNativeAgentThreadFromSummary {
|
||||||
from_session_id: agent_client_protocol::SessionId,
|
from_session_id: agent_client_protocol::SessionId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO unify this with AgentType
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
enum ExternalAgent {
|
enum ExternalAgent {
|
||||||
|
@ -174,6 +175,15 @@ enum ExternalAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExternalAgent {
|
impl ExternalAgent {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::NativeAgent => "zed",
|
||||||
|
Self::Gemini => "gemini-cli",
|
||||||
|
Self::ClaudeCode => "claude-code",
|
||||||
|
Self::Custom { .. } => "custom",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn server(
|
pub fn server(
|
||||||
&self,
|
&self,
|
||||||
fs: Arc<dyn fs::Fs>,
|
fs: Arc<dyn fs::Fs>,
|
||||||
|
|
|
@ -361,6 +361,7 @@ impl TextThreadEditor {
|
||||||
if self.sending_disabled(cx) {
|
if self.sending_disabled(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
telemetry::event!("Agent Message Sent", agent = "zed-text");
|
||||||
self.send_to_model(window, cx);
|
self.send_to_model(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}
|
||||||
|
|
||||||
#[derive(IntoElement, RegisterComponent)]
|
#[derive(IntoElement, RegisterComponent)]
|
||||||
pub struct AiUpsellCard {
|
pub struct AiUpsellCard {
|
||||||
pub sign_in_status: SignInStatus,
|
sign_in_status: SignInStatus,
|
||||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||||
pub account_too_young: bool,
|
account_too_young: bool,
|
||||||
pub user_plan: Option<Plan>,
|
user_plan: Option<Plan>,
|
||||||
pub tab_index: Option<isize>,
|
tab_index: Option<isize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AiUpsellCard {
|
impl AiUpsellCard {
|
||||||
|
@ -43,6 +43,11 @@ impl AiUpsellCard {
|
||||||
tab_index: None,
|
tab_index: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
|
||||||
|
self.tab_index = tab_index;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for AiUpsellCard {
|
impl RenderOnce for AiUpsellCard {
|
||||||
|
|
|
@ -118,7 +118,7 @@ impl Tool for FetchTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_perform_edits(&self) -> bool {
|
fn may_perform_edits(&self) -> bool {
|
||||||
|
|
|
@ -435,8 +435,8 @@ mod test {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
matches,
|
matches,
|
||||||
&[
|
&[
|
||||||
PathBuf::from("root/apple/banana/carrot"),
|
PathBuf::from(path!("root/apple/banana/carrot")),
|
||||||
PathBuf::from("root/apple/bandana/carbonara")
|
PathBuf::from(path!("root/apple/bandana/carbonara"))
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -447,8 +447,8 @@ mod test {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
matches,
|
matches,
|
||||||
&[
|
&[
|
||||||
PathBuf::from("root/apple/banana/carrot"),
|
PathBuf::from(path!("root/apple/banana/carrot")),
|
||||||
PathBuf::from("root/apple/bandana/carbonara")
|
PathBuf::from(path!("root/apple/bandana/carbonara"))
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ impl Tool for ReadFileTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon(&self) -> IconName {
|
fn icon(&self) -> IconName {
|
||||||
IconName::ToolRead
|
IconName::ToolSearch
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use db::{
|
use db::{
|
||||||
define_connection, query,
|
query,
|
||||||
sqlez::{bindable::Column, statement::Statement},
|
sqlez::{
|
||||||
|
bindable::Column, domain::Domain, statement::Statement,
|
||||||
|
thread_safe_connection::ThreadSafeConnection,
|
||||||
|
},
|
||||||
sqlez_macros::sql,
|
sqlez_macros::sql,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -50,8 +53,11 @@ impl Column for SerializedCommandInvocation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
|
pub struct CommandPaletteDB(ThreadSafeConnection);
|
||||||
&[sql!(
|
|
||||||
|
impl Domain for CommandPaletteDB {
|
||||||
|
const NAME: &str = stringify!(CommandPaletteDB);
|
||||||
|
const MIGRATIONS: &[&str] = &[sql!(
|
||||||
CREATE TABLE IF NOT EXISTS command_invocations(
|
CREATE TABLE IF NOT EXISTS command_invocations(
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
command_name TEXT NOT NULL,
|
command_name TEXT NOT NULL,
|
||||||
|
@ -59,7 +65,9 @@ define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()>
|
||||||
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
|
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)];
|
)];
|
||||||
);
|
}
|
||||||
|
|
||||||
|
db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
|
||||||
|
|
||||||
impl CommandPaletteDB {
|
impl CommandPaletteDB {
|
||||||
pub async fn write_command_invocation(
|
pub async fn write_command_invocation(
|
||||||
|
|
|
@ -110,11 +110,14 @@ pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implements a basic DB wrapper for a given domain
|
/// Implements a basic DB wrapper for a given domain
|
||||||
|
///
|
||||||
|
/// Arguments:
|
||||||
|
/// - static variable name for connection
|
||||||
|
/// - type of connection wrapper
|
||||||
|
/// - dependencies, whose migrations should be run prior to this domain's migrations
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! define_connection {
|
macro_rules! static_connection {
|
||||||
(pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => {
|
($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => {
|
||||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
|
||||||
|
|
||||||
impl ::std::ops::Deref for $t {
|
impl ::std::ops::Deref for $t {
|
||||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||||
|
|
||||||
|
@ -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 {
|
impl $t {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub async fn open_test_db(name: &'static str) -> Self {
|
pub async fn open_test_db(name: &'static str) -> Self {
|
||||||
|
@ -142,7 +135,8 @@ macro_rules! define_connection {
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||||
$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")))]
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
@ -153,46 +147,10 @@ macro_rules! define_connection {
|
||||||
} else {
|
} else {
|
||||||
$crate::RELEASE_CHANNEL.dev_name()
|
$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)
|
pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
|
||||||
|
@ -219,17 +177,12 @@ mod tests {
|
||||||
enum BadDB {}
|
enum BadDB {}
|
||||||
|
|
||||||
impl Domain for BadDB {
|
impl Domain for BadDB {
|
||||||
fn name() -> &'static str {
|
const NAME: &str = "db_tests";
|
||||||
"db_tests"
|
const MIGRATIONS: &[&str] = &[
|
||||||
}
|
sql!(CREATE TABLE test(value);),
|
||||||
|
// failure because test already exists
|
||||||
fn migrations() -> &'static [&'static str] {
|
sql!(CREATE TABLE test(value);),
|
||||||
&[
|
];
|
||||||
sql!(CREATE TABLE test(value);),
|
|
||||||
// failure because test already exists
|
|
||||||
sql!(CREATE TABLE test(value);),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let tempdir = tempfile::Builder::new()
|
let tempdir = tempfile::Builder::new()
|
||||||
|
@ -251,25 +204,15 @@ mod tests {
|
||||||
enum CorruptedDB {}
|
enum CorruptedDB {}
|
||||||
|
|
||||||
impl Domain for CorruptedDB {
|
impl Domain for CorruptedDB {
|
||||||
fn name() -> &'static str {
|
const NAME: &str = "db_tests";
|
||||||
"db_tests"
|
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
||||||
}
|
|
||||||
|
|
||||||
fn migrations() -> &'static [&'static str] {
|
|
||||||
&[sql!(CREATE TABLE test(value);)]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GoodDB {}
|
enum GoodDB {}
|
||||||
|
|
||||||
impl Domain for GoodDB {
|
impl Domain for GoodDB {
|
||||||
fn name() -> &'static str {
|
const NAME: &str = "db_tests"; //Notice same name
|
||||||
"db_tests" //Notice same name
|
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)];
|
||||||
}
|
|
||||||
|
|
||||||
fn migrations() -> &'static [&'static str] {
|
|
||||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let tempdir = tempfile::Builder::new()
|
let tempdir = tempfile::Builder::new()
|
||||||
|
@ -305,25 +248,16 @@ mod tests {
|
||||||
enum CorruptedDB {}
|
enum CorruptedDB {}
|
||||||
|
|
||||||
impl Domain for CorruptedDB {
|
impl Domain for CorruptedDB {
|
||||||
fn name() -> &'static str {
|
const NAME: &str = "db_tests";
|
||||||
"db_tests"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn migrations() -> &'static [&'static str] {
|
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
||||||
&[sql!(CREATE TABLE test(value);)]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GoodDB {}
|
enum GoodDB {}
|
||||||
|
|
||||||
impl Domain for GoodDB {
|
impl Domain for GoodDB {
|
||||||
fn name() -> &'static str {
|
const NAME: &str = "db_tests"; //Notice same name
|
||||||
"db_tests" //Notice same name
|
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration
|
||||||
}
|
|
||||||
|
|
||||||
fn migrations() -> &'static [&'static str] {
|
|
||||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let tempdir = tempfile::Builder::new()
|
let tempdir = tempfile::Builder::new()
|
||||||
|
|
|
@ -2,16 +2,26 @@ use gpui::App;
|
||||||
use sqlez_macros::sql;
|
use sqlez_macros::sql;
|
||||||
use util::ResultExt as _;
|
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<()> =
|
pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||||
&[sql!(
|
|
||||||
|
impl Domain for KeyValueStore {
|
||||||
|
const NAME: &str = stringify!(KeyValueStore);
|
||||||
|
|
||||||
|
const MIGRATIONS: &[&str] = &[sql!(
|
||||||
CREATE TABLE IF NOT EXISTS kv_store(
|
CREATE TABLE IF NOT EXISTS kv_store(
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT NOT NULL
|
value TEXT NOT NULL
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)];
|
)];
|
||||||
);
|
}
|
||||||
|
|
||||||
|
crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
|
||||||
|
|
||||||
pub trait Dismissable {
|
pub trait Dismissable {
|
||||||
const KEY: &'static str;
|
const KEY: &'static str;
|
||||||
|
@ -91,15 +101,19 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> =
|
pub struct GlobalKeyValueStore(ThreadSafeConnection);
|
||||||
&[sql!(
|
|
||||||
|
impl Domain for GlobalKeyValueStore {
|
||||||
|
const NAME: &str = stringify!(GlobalKeyValueStore);
|
||||||
|
const MIGRATIONS: &[&str] = &[sql!(
|
||||||
CREATE TABLE IF NOT EXISTS kv_store(
|
CREATE TABLE IF NOT EXISTS kv_store(
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT NOT NULL
|
value TEXT NOT NULL
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)];
|
)];
|
||||||
global
|
}
|
||||||
);
|
|
||||||
|
crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global);
|
||||||
|
|
||||||
impl GlobalKeyValueStore {
|
impl GlobalKeyValueStore {
|
||||||
query! {
|
query! {
|
||||||
|
|
|
@ -1404,7 +1404,7 @@ impl ProjectItem for Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn for_broken_project_item(
|
fn for_broken_project_item(
|
||||||
abs_path: PathBuf,
|
abs_path: &Path,
|
||||||
is_local: bool,
|
is_local: bool,
|
||||||
e: &anyhow::Error,
|
e: &anyhow::Error,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
|
use db::{
|
||||||
use db::sqlez::statement::Statement;
|
query,
|
||||||
|
sqlez::{
|
||||||
|
bindable::{Bind, Column, StaticColumnCount},
|
||||||
|
domain::Domain,
|
||||||
|
statement::Statement,
|
||||||
|
},
|
||||||
|
sqlez_macros::sql,
|
||||||
|
};
|
||||||
use fs::MTime;
|
use fs::MTime;
|
||||||
use itertools::Itertools as _;
|
use itertools::Itertools as _;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use db::sqlez_macros::sql;
|
|
||||||
use db::{define_connection, query};
|
|
||||||
|
|
||||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Default)]
|
#[derive(Clone, Debug, PartialEq, Default)]
|
||||||
|
@ -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:
|
// Current schema shape using pseudo-rust syntax:
|
||||||
// editors(
|
// editors(
|
||||||
// item_id: usize,
|
// item_id: usize,
|
||||||
|
@ -113,7 +121,8 @@ define_connection!(
|
||||||
// start: usize,
|
// start: usize,
|
||||||
// end: usize,
|
// end: usize,
|
||||||
// )
|
// )
|
||||||
pub static ref DB: EditorDb<WorkspaceDb> = &[
|
|
||||||
|
const MIGRATIONS: &[&str] = &[
|
||||||
sql! (
|
sql! (
|
||||||
CREATE TABLE editors(
|
CREATE TABLE editors(
|
||||||
item_id INTEGER NOT NULL,
|
item_id INTEGER NOT NULL,
|
||||||
|
@ -189,7 +198,9 @@ define_connection!(
|
||||||
) STRICT;
|
) STRICT;
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
);
|
}
|
||||||
|
|
||||||
|
db::static_connection!(DB, EditorDb, [WorkspaceDb]);
|
||||||
|
|
||||||
// https://www.sqlite.org/limits.html
|
// https://www.sqlite.org/limits.html
|
||||||
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
|
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
|
||||||
|
|
|
@ -9,10 +9,8 @@ use parking::Parker;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use windows::{
|
use windows::{
|
||||||
Foundation::TimeSpan,
|
|
||||||
System::Threading::{
|
System::Threading::{
|
||||||
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions,
|
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority,
|
||||||
WorkItemPriority,
|
|
||||||
},
|
},
|
||||||
Win32::{
|
Win32::{
|
||||||
Foundation::{LPARAM, WPARAM},
|
Foundation::{LPARAM, WPARAM},
|
||||||
|
@ -56,12 +54,7 @@ impl WindowsDispatcher {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
ThreadPool::RunWithPriorityAndOptionsAsync(
|
ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err();
|
||||||
&handler,
|
|
||||||
WorkItemPriority::High,
|
|
||||||
WorkItemOptions::TimeSliced,
|
|
||||||
)
|
|
||||||
.log_err();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
|
fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
|
||||||
|
@ -72,12 +65,7 @@ impl WindowsDispatcher {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
let delay = TimeSpan {
|
ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err();
|
||||||
// A time period expressed in 100-nanosecond units.
|
|
||||||
// 10,000,000 ticks per second
|
|
||||||
Duration: (duration.as_nanos() / 100) as i64,
|
|
||||||
};
|
|
||||||
ThreadPoolTimer::CreateTimer(&handler, delay).log_err();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -401,12 +401,19 @@ pub fn init(cx: &mut App) {
|
||||||
mod persistence {
|
mod persistence {
|
||||||
use std::path::PathBuf;
|
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};
|
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||||
|
|
||||||
define_connection! {
|
pub struct ImageViewerDb(ThreadSafeConnection);
|
||||||
pub static ref IMAGE_VIEWER: ImageViewerDb<WorkspaceDb> =
|
|
||||||
&[sql!(
|
impl Domain for ImageViewerDb {
|
||||||
|
const NAME: &str = stringify!(ImageViewerDb);
|
||||||
|
|
||||||
|
const MIGRATIONS: &[&str] = &[sql!(
|
||||||
CREATE TABLE image_viewers (
|
CREATE TABLE image_viewers (
|
||||||
workspace_id INTEGER,
|
workspace_id INTEGER,
|
||||||
item_id INTEGER UNIQUE,
|
item_id INTEGER UNIQUE,
|
||||||
|
@ -417,9 +424,11 @@ mod persistence {
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)];
|
)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]);
|
||||||
|
|
||||||
impl ImageViewerDb {
|
impl ImageViewerDb {
|
||||||
query! {
|
query! {
|
||||||
pub async fn save_image_path(
|
pub async fn save_image_path(
|
||||||
|
|
|
@ -1569,11 +1569,21 @@ impl Buffer {
|
||||||
self.send_operation(op, true, cx);
|
self.send_operation(op, true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> {
|
pub fn buffer_diagnostics(
|
||||||
let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else {
|
&self,
|
||||||
return None;
|
for_server: Option<LanguageServerId>,
|
||||||
};
|
) -> Vec<&DiagnosticEntry<Anchor>> {
|
||||||
Some(&self.diagnostics[idx].1)
|
match for_server {
|
||||||
|
Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) {
|
||||||
|
Ok(idx) => self.diagnostics[idx].1.iter().collect(),
|
||||||
|
Err(_) => Vec::new(),
|
||||||
|
},
|
||||||
|
None => self
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(_, diagnostic_set)| diagnostic_set.iter())
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_autoindent(&mut self, cx: &mut Context<Self>) {
|
fn request_autoindent(&mut self, cx: &mut Context<Self>) {
|
||||||
|
|
|
@ -1743,6 +1743,5 @@ pub enum Event {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<Event> for LogStore {}
|
impl EventEmitter<Event> for LogStore {}
|
||||||
impl EventEmitter<Event> for LspLogView {}
|
|
||||||
impl EventEmitter<EditorEvent> for LspLogView {}
|
impl EventEmitter<EditorEvent> for LspLogView {}
|
||||||
impl EventEmitter<SearchEvent> for LspLogView {}
|
impl EventEmitter<SearchEvent> for LspLogView {}
|
||||||
|
|
|
@ -11,6 +11,21 @@
|
||||||
(#set! injection.language "css"))
|
(#set! injection.language "css"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
(call_expression
|
||||||
|
function: (member_expression
|
||||||
|
object: (identifier) @_obj (#eq? @_obj "styled")
|
||||||
|
property: (property_identifier))
|
||||||
|
arguments: (template_string (string_fragment) @injection.content
|
||||||
|
(#set! injection.language "css"))
|
||||||
|
)
|
||||||
|
|
||||||
|
(call_expression
|
||||||
|
function: (call_expression
|
||||||
|
function: (identifier) @_name (#eq? @_name "styled"))
|
||||||
|
arguments: (template_string (string_fragment) @injection.content
|
||||||
|
(#set! injection.language "css"))
|
||||||
|
)
|
||||||
|
|
||||||
(call_expression
|
(call_expression
|
||||||
function: (identifier) @_name (#eq? @_name "html")
|
function: (identifier) @_name (#eq? @_name "html")
|
||||||
arguments: (template_string) @injection.content
|
arguments: (template_string) @injection.content
|
||||||
|
|
|
@ -6,9 +6,6 @@
|
||||||
(self) @variable.special
|
(self) @variable.special
|
||||||
(field_identifier) @property
|
(field_identifier) @property
|
||||||
|
|
||||||
(shorthand_field_initializer
|
|
||||||
(identifier) @property)
|
|
||||||
|
|
||||||
(trait_item name: (type_identifier) @type.interface)
|
(trait_item name: (type_identifier) @type.interface)
|
||||||
(impl_item trait: (type_identifier) @type.interface)
|
(impl_item trait: (type_identifier) @type.interface)
|
||||||
(abstract_type trait: (type_identifier) @type.interface)
|
(abstract_type trait: (type_identifier) @type.interface)
|
||||||
|
@ -41,20 +38,11 @@
|
||||||
(identifier) @function.special
|
(identifier) @function.special
|
||||||
(scoped_identifier
|
(scoped_identifier
|
||||||
name: (identifier) @function.special)
|
name: (identifier) @function.special)
|
||||||
]
|
])
|
||||||
"!" @function.special)
|
|
||||||
|
|
||||||
(macro_definition
|
(macro_definition
|
||||||
name: (identifier) @function.special.definition)
|
name: (identifier) @function.special.definition)
|
||||||
|
|
||||||
(mod_item
|
|
||||||
name: (identifier) @module)
|
|
||||||
|
|
||||||
(visibility_modifier [
|
|
||||||
(crate) @keyword
|
|
||||||
(super) @keyword
|
|
||||||
])
|
|
||||||
|
|
||||||
; Identifier conventions
|
; Identifier conventions
|
||||||
|
|
||||||
; Assume uppercase names are types/enum-constructors
|
; Assume uppercase names are types/enum-constructors
|
||||||
|
@ -127,7 +115,9 @@
|
||||||
"where"
|
"where"
|
||||||
"while"
|
"while"
|
||||||
"yield"
|
"yield"
|
||||||
|
(crate)
|
||||||
(mutable_specifier)
|
(mutable_specifier)
|
||||||
|
(super)
|
||||||
] @keyword
|
] @keyword
|
||||||
|
|
||||||
[
|
[
|
||||||
|
@ -199,7 +189,6 @@
|
||||||
operator: "/" @operator
|
operator: "/" @operator
|
||||||
|
|
||||||
(lifetime) @lifetime
|
(lifetime) @lifetime
|
||||||
(lifetime (identifier) @lifetime)
|
|
||||||
|
|
||||||
(parameter (identifier) @variable.parameter)
|
(parameter (identifier) @variable.parameter)
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,21 @@
|
||||||
(#set! injection.language "css"))
|
(#set! injection.language "css"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
(call_expression
|
||||||
|
function: (member_expression
|
||||||
|
object: (identifier) @_obj (#eq? @_obj "styled")
|
||||||
|
property: (property_identifier))
|
||||||
|
arguments: (template_string (string_fragment) @injection.content
|
||||||
|
(#set! injection.language "css"))
|
||||||
|
)
|
||||||
|
|
||||||
|
(call_expression
|
||||||
|
function: (call_expression
|
||||||
|
function: (identifier) @_name (#eq? @_name "styled"))
|
||||||
|
arguments: (template_string (string_fragment) @injection.content
|
||||||
|
(#set! injection.language "css"))
|
||||||
|
)
|
||||||
|
|
||||||
(call_expression
|
(call_expression
|
||||||
function: (identifier) @_name (#eq? @_name "html")
|
function: (identifier) @_name (#eq? @_name "html")
|
||||||
arguments: (template_string (string_fragment) @injection.content
|
arguments: (template_string (string_fragment) @injection.content
|
||||||
|
|
|
@ -15,6 +15,21 @@
|
||||||
(#set! injection.language "css"))
|
(#set! injection.language "css"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
(call_expression
|
||||||
|
function: (member_expression
|
||||||
|
object: (identifier) @_obj (#eq? @_obj "styled")
|
||||||
|
property: (property_identifier))
|
||||||
|
arguments: (template_string (string_fragment) @injection.content
|
||||||
|
(#set! injection.language "css"))
|
||||||
|
)
|
||||||
|
|
||||||
|
(call_expression
|
||||||
|
function: (call_expression
|
||||||
|
function: (identifier) @_name (#eq? @_name "styled"))
|
||||||
|
arguments: (template_string (string_fragment) @injection.content
|
||||||
|
(#set! injection.language "css"))
|
||||||
|
)
|
||||||
|
|
||||||
(call_expression
|
(call_expression
|
||||||
function: (identifier) @_name (#eq? @_name "html")
|
function: (identifier) @_name (#eq? @_name "html")
|
||||||
arguments: (template_string) @injection.content
|
arguments: (template_string) @injection.content
|
||||||
|
|
|
@ -283,17 +283,13 @@ pub(crate) fn render_ai_setup_page(
|
||||||
v_flex()
|
v_flex()
|
||||||
.mt_2()
|
.mt_2()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.child({
|
.child(
|
||||||
let mut ai_upsell_card =
|
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx)
|
||||||
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx);
|
.tab_index(Some({
|
||||||
|
tab_index += 1;
|
||||||
ai_upsell_card.tab_index = Some({
|
tab_index - 1
|
||||||
tab_index += 1;
|
})),
|
||||||
tab_index - 1
|
)
|
||||||
});
|
|
||||||
|
|
||||||
ai_upsell_card
|
|
||||||
})
|
|
||||||
.child(render_llm_provider_section(
|
.child(render_llm_provider_section(
|
||||||
&mut tab_index,
|
&mut tab_index,
|
||||||
workspace,
|
workspace,
|
||||||
|
|
|
@ -850,13 +850,19 @@ impl workspace::SerializableItem for Onboarding {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod persistence {
|
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;
|
use workspace::WorkspaceDb;
|
||||||
|
|
||||||
define_connection! {
|
pub struct OnboardingPagesDb(ThreadSafeConnection);
|
||||||
pub static ref ONBOARDING_PAGES: OnboardingPagesDb<WorkspaceDb> =
|
|
||||||
&[
|
impl Domain for OnboardingPagesDb {
|
||||||
sql!(
|
const NAME: &str = stringify!(OnboardingPagesDb);
|
||||||
|
|
||||||
|
const MIGRATIONS: &[&str] = &[sql!(
|
||||||
CREATE TABLE onboarding_pages (
|
CREATE TABLE onboarding_pages (
|
||||||
workspace_id INTEGER,
|
workspace_id INTEGER,
|
||||||
item_id INTEGER UNIQUE,
|
item_id INTEGER UNIQUE,
|
||||||
|
@ -866,10 +872,11 @@ mod persistence {
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
),
|
)];
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
|
||||||
|
|
||||||
impl OnboardingPagesDb {
|
impl OnboardingPagesDb {
|
||||||
query! {
|
query! {
|
||||||
pub async fn save_onboarding_page(
|
pub async fn save_onboarding_page(
|
||||||
|
|
|
@ -414,13 +414,19 @@ impl workspace::SerializableItem for WelcomePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod persistence {
|
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;
|
use workspace::WorkspaceDb;
|
||||||
|
|
||||||
define_connection! {
|
pub struct WelcomePagesDb(ThreadSafeConnection);
|
||||||
pub static ref WELCOME_PAGES: WelcomePagesDb<WorkspaceDb> =
|
|
||||||
&[
|
impl Domain for WelcomePagesDb {
|
||||||
sql!(
|
const NAME: &str = stringify!(WelcomePagesDb);
|
||||||
|
|
||||||
|
const MIGRATIONS: &[&str] = (&[sql!(
|
||||||
CREATE TABLE welcome_pages (
|
CREATE TABLE welcome_pages (
|
||||||
workspace_id INTEGER,
|
workspace_id INTEGER,
|
||||||
item_id INTEGER UNIQUE,
|
item_id INTEGER UNIQUE,
|
||||||
|
@ -430,10 +436,11 @@ mod persistence {
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
),
|
)]);
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]);
|
||||||
|
|
||||||
impl WelcomePagesDb {
|
impl WelcomePagesDb {
|
||||||
query! {
|
query! {
|
||||||
pub async fn save_welcome_page(
|
pub async fn save_welcome_page(
|
||||||
|
|
|
@ -446,7 +446,6 @@ pub enum ResponseStreamResult {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct ResponseStreamEvent {
|
pub struct ResponseStreamEvent {
|
||||||
pub model: String,
|
|
||||||
pub choices: Vec<ChoiceDelta>,
|
pub choices: Vec<ChoiceDelta>,
|
||||||
pub usage: Option<Usage>,
|
pub usage: Option<Usage>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -7588,19 +7588,16 @@ impl LspStore {
|
||||||
let snapshot = buffer_handle.read(cx).snapshot();
|
let snapshot = buffer_handle.read(cx).snapshot();
|
||||||
let buffer = buffer_handle.read(cx);
|
let buffer = buffer_handle.read(cx);
|
||||||
let reused_diagnostics = buffer
|
let reused_diagnostics = buffer
|
||||||
.get_diagnostics(server_id)
|
.buffer_diagnostics(Some(server_id))
|
||||||
.into_iter()
|
.iter()
|
||||||
.flat_map(|diag| {
|
.filter(|v| merge(buffer, &v.diagnostic, cx))
|
||||||
diag.iter()
|
.map(|v| {
|
||||||
.filter(|v| merge(buffer, &v.diagnostic, cx))
|
let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
|
||||||
.map(|v| {
|
let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
|
||||||
let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
|
DiagnosticEntry {
|
||||||
let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
|
range: start..end,
|
||||||
DiagnosticEntry {
|
diagnostic: v.diagnostic.clone(),
|
||||||
range: start..end,
|
}
|
||||||
diagnostic: v.diagnostic.clone(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
@ -11706,12 +11703,11 @@ impl LspStore {
|
||||||
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
|
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
|
||||||
}
|
}
|
||||||
"workspace/symbol" => {
|
"workspace/symbol" => {
|
||||||
if let Some(options) = parse_register_capabilities(reg)? {
|
let options = parse_register_capabilities(reg)?;
|
||||||
server.update_capabilities(|capabilities| {
|
server.update_capabilities(|capabilities| {
|
||||||
capabilities.workspace_symbol_provider = Some(options);
|
capabilities.workspace_symbol_provider = Some(options);
|
||||||
});
|
});
|
||||||
notify_server_capabilities_updated(&server, cx);
|
notify_server_capabilities_updated(&server, cx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"workspace/fileOperations" => {
|
"workspace/fileOperations" => {
|
||||||
if let Some(options) = reg.register_options {
|
if let Some(options) = reg.register_options {
|
||||||
|
@ -11735,12 +11731,11 @@ impl LspStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"textDocument/rangeFormatting" => {
|
"textDocument/rangeFormatting" => {
|
||||||
if let Some(options) = parse_register_capabilities(reg)? {
|
let options = parse_register_capabilities(reg)?;
|
||||||
server.update_capabilities(|capabilities| {
|
server.update_capabilities(|capabilities| {
|
||||||
capabilities.document_range_formatting_provider = Some(options);
|
capabilities.document_range_formatting_provider = Some(options);
|
||||||
});
|
});
|
||||||
notify_server_capabilities_updated(&server, cx);
|
notify_server_capabilities_updated(&server, cx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"textDocument/onTypeFormatting" => {
|
"textDocument/onTypeFormatting" => {
|
||||||
if let Some(options) = reg
|
if let Some(options) = reg
|
||||||
|
@ -11755,36 +11750,32 @@ impl LspStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"textDocument/formatting" => {
|
"textDocument/formatting" => {
|
||||||
if let Some(options) = parse_register_capabilities(reg)? {
|
let options = parse_register_capabilities(reg)?;
|
||||||
server.update_capabilities(|capabilities| {
|
server.update_capabilities(|capabilities| {
|
||||||
capabilities.document_formatting_provider = Some(options);
|
capabilities.document_formatting_provider = Some(options);
|
||||||
});
|
});
|
||||||
notify_server_capabilities_updated(&server, cx);
|
notify_server_capabilities_updated(&server, cx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"textDocument/rename" => {
|
"textDocument/rename" => {
|
||||||
if let Some(options) = parse_register_capabilities(reg)? {
|
let options = parse_register_capabilities(reg)?;
|
||||||
server.update_capabilities(|capabilities| {
|
server.update_capabilities(|capabilities| {
|
||||||
capabilities.rename_provider = Some(options);
|
capabilities.rename_provider = Some(options);
|
||||||
});
|
});
|
||||||
notify_server_capabilities_updated(&server, cx);
|
notify_server_capabilities_updated(&server, cx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"textDocument/inlayHint" => {
|
"textDocument/inlayHint" => {
|
||||||
if let Some(options) = parse_register_capabilities(reg)? {
|
let options = parse_register_capabilities(reg)?;
|
||||||
server.update_capabilities(|capabilities| {
|
server.update_capabilities(|capabilities| {
|
||||||
capabilities.inlay_hint_provider = Some(options);
|
capabilities.inlay_hint_provider = Some(options);
|
||||||
});
|
});
|
||||||
notify_server_capabilities_updated(&server, cx);
|
notify_server_capabilities_updated(&server, cx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"textDocument/documentSymbol" => {
|
"textDocument/documentSymbol" => {
|
||||||
if let Some(options) = parse_register_capabilities(reg)? {
|
let options = parse_register_capabilities(reg)?;
|
||||||
server.update_capabilities(|capabilities| {
|
server.update_capabilities(|capabilities| {
|
||||||
capabilities.document_symbol_provider = Some(options);
|
capabilities.document_symbol_provider = Some(options);
|
||||||
});
|
});
|
||||||
notify_server_capabilities_updated(&server, cx);
|
notify_server_capabilities_updated(&server, cx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"textDocument/codeAction" => {
|
"textDocument/codeAction" => {
|
||||||
if let Some(options) = reg
|
if let Some(options) = reg
|
||||||
|
@ -11800,12 +11791,11 @@ impl LspStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"textDocument/definition" => {
|
"textDocument/definition" => {
|
||||||
if let Some(options) = parse_register_capabilities(reg)? {
|
let options = parse_register_capabilities(reg)?;
|
||||||
server.update_capabilities(|capabilities| {
|
server.update_capabilities(|capabilities| {
|
||||||
capabilities.definition_provider = Some(options);
|
capabilities.definition_provider = Some(options);
|
||||||
});
|
});
|
||||||
notify_server_capabilities_updated(&server, cx);
|
notify_server_capabilities_updated(&server, cx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"textDocument/completion" => {
|
"textDocument/completion" => {
|
||||||
if let Some(caps) = reg
|
if let Some(caps) = reg
|
||||||
|
@ -12184,10 +12174,10 @@ impl LspStore {
|
||||||
// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133
|
// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133
|
||||||
fn parse_register_capabilities<T: serde::de::DeserializeOwned>(
|
fn parse_register_capabilities<T: serde::de::DeserializeOwned>(
|
||||||
reg: lsp::Registration,
|
reg: lsp::Registration,
|
||||||
) -> anyhow::Result<Option<OneOf<bool, T>>> {
|
) -> Result<OneOf<bool, T>> {
|
||||||
Ok(match reg.register_options {
|
Ok(match reg.register_options {
|
||||||
Some(options) => Some(OneOf::Right(serde_json::from_value::<T>(options)?)),
|
Some(options) => OneOf::Right(serde_json::from_value::<T>(options)?),
|
||||||
None => Some(OneOf::Left(true)),
|
None => OneOf::Left(true),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -445,7 +445,7 @@ impl SshSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn platform(&self) -> Result<SshPlatform> {
|
async fn platform(&self) -> Result<SshPlatform> {
|
||||||
let uname = self.run_command("sh", &["-c", "uname -sm"]).await?;
|
let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?;
|
||||||
let Some((os, arch)) = uname.split_once(" ") else {
|
let Some((os, arch)) = uname.split_once(" ") else {
|
||||||
anyhow::bail!("unknown uname: {uname:?}")
|
anyhow::bail!("unknown uname: {uname:?}")
|
||||||
};
|
};
|
||||||
|
@ -476,7 +476,7 @@ impl SshSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shell(&self) -> String {
|
async fn shell(&self) -> String {
|
||||||
match self.run_command("sh", &["-c", "echo $SHELL"]).await {
|
match self.run_command("sh", &["-lc", "echo $SHELL"]).await {
|
||||||
Ok(shell) => shell.trim().to_owned(),
|
Ok(shell) => shell.trim().to_owned(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to get shell: {e}");
|
log::error!("Failed to get shell: {e}");
|
||||||
|
@ -1533,7 +1533,7 @@ impl RemoteConnection for SshRemoteConnection {
|
||||||
|
|
||||||
let ssh_proxy_process = match self
|
let ssh_proxy_process = match self
|
||||||
.socket
|
.socket
|
||||||
.ssh_command("sh", &["-c", &start_proxy_command])
|
.ssh_command("sh", &["-lc", &start_proxy_command])
|
||||||
// IMPORTANT: we kill this process when we drop the task that uses it.
|
// IMPORTANT: we kill this process when we drop the task that uses it.
|
||||||
.kill_on_drop(true)
|
.kill_on_drop(true)
|
||||||
.spawn()
|
.spawn()
|
||||||
|
@ -1910,7 +1910,7 @@ impl SshRemoteConnection {
|
||||||
.run_command(
|
.run_command(
|
||||||
"sh",
|
"sh",
|
||||||
&[
|
&[
|
||||||
"-c",
|
"-lc",
|
||||||
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
|
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -1988,7 +1988,7 @@ impl SshRemoteConnection {
|
||||||
.run_command(
|
.run_command(
|
||||||
"sh",
|
"sh",
|
||||||
&[
|
&[
|
||||||
"-c",
|
"-lc",
|
||||||
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
|
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -2036,7 +2036,7 @@ impl SshRemoteConnection {
|
||||||
dst_path = &dst_path.to_string()
|
dst_path = &dst_path.to_string()
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
self.socket.run_command("sh", &["-c", &script]).await?;
|
self.socket.run_command("sh", &["-lc", &script]).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,7 @@ telemetry_events.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
watch.workspace = true
|
watch.workspace = true
|
||||||
worktree.workspace = true
|
worktree.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
crashes.workspace = true
|
crashes.workspace = true
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
|
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::Parser;
|
||||||
|
use remote_server::Commands;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
@ -21,105 +22,34 @@ struct Cli {
|
||||||
printenv: bool,
|
printenv: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum Commands {
|
|
||||||
Run {
|
|
||||||
#[arg(long)]
|
|
||||||
log_file: PathBuf,
|
|
||||||
#[arg(long)]
|
|
||||||
pid_file: PathBuf,
|
|
||||||
#[arg(long)]
|
|
||||||
stdin_socket: PathBuf,
|
|
||||||
#[arg(long)]
|
|
||||||
stdout_socket: PathBuf,
|
|
||||||
#[arg(long)]
|
|
||||||
stderr_socket: PathBuf,
|
|
||||||
},
|
|
||||||
Proxy {
|
|
||||||
#[arg(long)]
|
|
||||||
reconnect: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
identifier: String,
|
|
||||||
},
|
|
||||||
Version,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn main() {
|
fn main() {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn main() {
|
fn main() -> anyhow::Result<()> {
|
||||||
use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
|
|
||||||
use remote::proxy::ProxyLaunchError;
|
|
||||||
use remote_server::unix::{execute_proxy, execute_run};
|
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
if let Some(socket_path) = &cli.askpass {
|
if let Some(socket_path) = &cli.askpass {
|
||||||
askpass::main(socket_path);
|
askpass::main(socket_path);
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(socket) = &cli.crash_handler {
|
if let Some(socket) = &cli.crash_handler {
|
||||||
crashes::crash_server(socket.as_path());
|
crashes::crash_server(socket.as_path());
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if cli.printenv {
|
if cli.printenv {
|
||||||
util::shell_env::print_env();
|
util::shell_env::print_env();
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = match cli.command {
|
if let Some(command) = cli.command {
|
||||||
Some(Commands::Run {
|
remote_server::run(command)
|
||||||
log_file,
|
} else {
|
||||||
pid_file,
|
eprintln!("usage: remote <run|proxy|version>");
|
||||||
stdin_socket,
|
|
||||||
stdout_socket,
|
|
||||||
stderr_socket,
|
|
||||||
}) => execute_run(
|
|
||||||
log_file,
|
|
||||||
pid_file,
|
|
||||||
stdin_socket,
|
|
||||||
stdout_socket,
|
|
||||||
stderr_socket,
|
|
||||||
),
|
|
||||||
Some(Commands::Proxy {
|
|
||||||
identifier,
|
|
||||||
reconnect,
|
|
||||||
}) => match execute_proxy(identifier, reconnect) {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(err) => {
|
|
||||||
if let Some(err) = err.downcast_ref::<ProxyLaunchError>() {
|
|
||||||
std::process::exit(err.to_exit_code());
|
|
||||||
}
|
|
||||||
Err(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(Commands::Version) => {
|
|
||||||
let release_channel = *RELEASE_CHANNEL;
|
|
||||||
match release_channel {
|
|
||||||
ReleaseChannel::Stable | ReleaseChannel::Preview => {
|
|
||||||
println!("{}", env!("ZED_PKG_VERSION"))
|
|
||||||
}
|
|
||||||
ReleaseChannel::Nightly | ReleaseChannel::Dev => {
|
|
||||||
println!(
|
|
||||||
"{}",
|
|
||||||
option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
eprintln!("usage: remote <run|proxy|version>");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Err(error) = result {
|
|
||||||
log::error!("exiting due to error: {}", error);
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,4 +6,78 @@ pub mod unix;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod remote_editing_tests;
|
mod remote_editing_tests;
|
||||||
|
|
||||||
|
use clap::Subcommand;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub use headless_project::{HeadlessAppState, HeadlessProject};
|
pub use headless_project::{HeadlessAppState, HeadlessProject};
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Commands {
|
||||||
|
Run {
|
||||||
|
#[arg(long)]
|
||||||
|
log_file: PathBuf,
|
||||||
|
#[arg(long)]
|
||||||
|
pid_file: PathBuf,
|
||||||
|
#[arg(long)]
|
||||||
|
stdin_socket: PathBuf,
|
||||||
|
#[arg(long)]
|
||||||
|
stdout_socket: PathBuf,
|
||||||
|
#[arg(long)]
|
||||||
|
stderr_socket: PathBuf,
|
||||||
|
},
|
||||||
|
Proxy {
|
||||||
|
#[arg(long)]
|
||||||
|
reconnect: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
identifier: String,
|
||||||
|
},
|
||||||
|
Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn run(command: Commands) -> anyhow::Result<()> {
|
||||||
|
use anyhow::Context;
|
||||||
|
use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
|
||||||
|
use unix::{ExecuteProxyError, execute_proxy, execute_run};
|
||||||
|
|
||||||
|
match command {
|
||||||
|
Commands::Run {
|
||||||
|
log_file,
|
||||||
|
pid_file,
|
||||||
|
stdin_socket,
|
||||||
|
stdout_socket,
|
||||||
|
stderr_socket,
|
||||||
|
} => execute_run(
|
||||||
|
log_file,
|
||||||
|
pid_file,
|
||||||
|
stdin_socket,
|
||||||
|
stdout_socket,
|
||||||
|
stderr_socket,
|
||||||
|
),
|
||||||
|
Commands::Proxy {
|
||||||
|
identifier,
|
||||||
|
reconnect,
|
||||||
|
} => execute_proxy(identifier, reconnect)
|
||||||
|
.inspect_err(|err| {
|
||||||
|
if let ExecuteProxyError::ServerNotRunning(err) = err {
|
||||||
|
std::process::exit(err.to_exit_code());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.context("running proxy on the remote server"),
|
||||||
|
Commands::Version => {
|
||||||
|
let release_channel = *RELEASE_CHANNEL;
|
||||||
|
match release_channel {
|
||||||
|
ReleaseChannel::Stable | ReleaseChannel::Preview => {
|
||||||
|
println!("{}", env!("ZED_PKG_VERSION"))
|
||||||
|
}
|
||||||
|
ReleaseChannel::Nightly | ReleaseChannel::Dev => {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ use smol::Async;
|
||||||
use smol::{net::unix::UnixListener, stream::StreamExt as _};
|
use smol::{net::unix::UnixListener, stream::StreamExt as _};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::ops::ControlFlow;
|
use std::ops::ControlFlow;
|
||||||
|
use std::process::ExitStatus;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::{env, thread};
|
use std::{env, thread};
|
||||||
|
@ -46,6 +47,7 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use telemetry_events::LocationData;
|
use telemetry_events::LocationData;
|
||||||
|
use thiserror::Error;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL {
|
pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL {
|
||||||
|
@ -526,7 +528,23 @@ pub fn execute_run(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Error)]
|
||||||
|
pub(crate) enum ServerPathError {
|
||||||
|
#[error("Failed to create server_dir `{path}`")]
|
||||||
|
CreateServerDir {
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
#[error("Failed to create logs_dir `{path}`")]
|
||||||
|
CreateLogsDir {
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
struct ServerPaths {
|
struct ServerPaths {
|
||||||
log_file: PathBuf,
|
log_file: PathBuf,
|
||||||
pid_file: PathBuf,
|
pid_file: PathBuf,
|
||||||
|
@ -536,10 +554,19 @@ struct ServerPaths {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerPaths {
|
impl ServerPaths {
|
||||||
fn new(identifier: &str) -> Result<Self> {
|
fn new(identifier: &str) -> Result<Self, ServerPathError> {
|
||||||
let server_dir = paths::remote_server_state_dir().join(identifier);
|
let server_dir = paths::remote_server_state_dir().join(identifier);
|
||||||
std::fs::create_dir_all(&server_dir)?;
|
std::fs::create_dir_all(&server_dir).map_err(|source| {
|
||||||
std::fs::create_dir_all(&logs_dir())?;
|
ServerPathError::CreateServerDir {
|
||||||
|
source,
|
||||||
|
path: server_dir.clone(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let log_dir = logs_dir();
|
||||||
|
std::fs::create_dir_all(log_dir).map_err(|source| ServerPathError::CreateLogsDir {
|
||||||
|
source: source,
|
||||||
|
path: log_dir.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
let pid_file = server_dir.join("server.pid");
|
let pid_file = server_dir.join("server.pid");
|
||||||
let stdin_socket = server_dir.join("stdin.sock");
|
let stdin_socket = server_dir.join("stdin.sock");
|
||||||
|
@ -557,7 +584,43 @@ impl ServerPaths {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
|
#[derive(Debug, Error)]
|
||||||
|
pub(crate) enum ExecuteProxyError {
|
||||||
|
#[error("Failed to init server paths")]
|
||||||
|
ServerPath(#[from] ServerPathError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
ServerNotRunning(#[from] ProxyLaunchError),
|
||||||
|
|
||||||
|
#[error("Failed to check PidFile '{path}'")]
|
||||||
|
CheckPidFile {
|
||||||
|
#[source]
|
||||||
|
source: CheckPidError,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("Failed to kill existing server with pid '{pid}'")]
|
||||||
|
KillRunningServer {
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
pid: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("failed to spawn server")]
|
||||||
|
SpawnServer(#[source] SpawnServerError),
|
||||||
|
|
||||||
|
#[error("stdin_task failed")]
|
||||||
|
StdinTask(#[source] anyhow::Error),
|
||||||
|
#[error("stdout_task failed")]
|
||||||
|
StdoutTask(#[source] anyhow::Error),
|
||||||
|
#[error("stderr_task failed")]
|
||||||
|
StderrTask(#[source] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn execute_proxy(
|
||||||
|
identifier: String,
|
||||||
|
is_reconnecting: bool,
|
||||||
|
) -> Result<(), ExecuteProxyError> {
|
||||||
init_logging_proxy();
|
init_logging_proxy();
|
||||||
|
|
||||||
let server_paths = ServerPaths::new(&identifier)?;
|
let server_paths = ServerPaths::new(&identifier)?;
|
||||||
|
@ -574,12 +637,19 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
|
||||||
|
|
||||||
log::info!("starting proxy process. PID: {}", std::process::id());
|
log::info!("starting proxy process. PID: {}", std::process::id());
|
||||||
|
|
||||||
let server_pid = check_pid_file(&server_paths.pid_file)?;
|
let server_pid = check_pid_file(&server_paths.pid_file).map_err(|source| {
|
||||||
|
ExecuteProxyError::CheckPidFile {
|
||||||
|
source,
|
||||||
|
path: server_paths.pid_file.clone(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
let server_running = server_pid.is_some();
|
let server_running = server_pid.is_some();
|
||||||
if is_reconnecting {
|
if is_reconnecting {
|
||||||
if !server_running {
|
if !server_running {
|
||||||
log::error!("attempted to reconnect, but no server running");
|
log::error!("attempted to reconnect, but no server running");
|
||||||
anyhow::bail!(ProxyLaunchError::ServerNotRunning);
|
return Err(ExecuteProxyError::ServerNotRunning(
|
||||||
|
ProxyLaunchError::ServerNotRunning,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Some(pid) = server_pid {
|
if let Some(pid) = server_pid {
|
||||||
|
@ -590,7 +660,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
|
||||||
kill_running_server(pid, &server_paths)?;
|
kill_running_server(pid, &server_paths)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn_server(&server_paths)?;
|
spawn_server(&server_paths).map_err(ExecuteProxyError::SpawnServer)?;
|
||||||
};
|
};
|
||||||
|
|
||||||
let stdin_task = smol::spawn(async move {
|
let stdin_task = smol::spawn(async move {
|
||||||
|
@ -630,9 +700,9 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
|
||||||
|
|
||||||
if let Err(forwarding_result) = smol::block_on(async move {
|
if let Err(forwarding_result) = smol::block_on(async move {
|
||||||
futures::select! {
|
futures::select! {
|
||||||
result = stdin_task.fuse() => result.context("stdin_task failed"),
|
result = stdin_task.fuse() => result.map_err(ExecuteProxyError::StdinTask),
|
||||||
result = stdout_task.fuse() => result.context("stdout_task failed"),
|
result = stdout_task.fuse() => result.map_err(ExecuteProxyError::StdoutTask),
|
||||||
result = stderr_task.fuse() => result.context("stderr_task failed"),
|
result = stderr_task.fuse() => result.map_err(ExecuteProxyError::StderrTask),
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
log::error!(
|
log::error!(
|
||||||
|
@ -645,12 +715,12 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> {
|
fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> {
|
||||||
log::info!("killing existing server with PID {}", pid);
|
log::info!("killing existing server with PID {}", pid);
|
||||||
std::process::Command::new("kill")
|
std::process::Command::new("kill")
|
||||||
.arg(pid.to_string())
|
.arg(pid.to_string())
|
||||||
.output()
|
.output()
|
||||||
.context("failed to kill existing server")?;
|
.map_err(|source| ExecuteProxyError::KillRunningServer { source, pid })?;
|
||||||
|
|
||||||
for file in [
|
for file in [
|
||||||
&paths.pid_file,
|
&paths.pid_file,
|
||||||
|
@ -664,18 +734,39 @@ fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_server(paths: &ServerPaths) -> Result<()> {
|
#[derive(Debug, Error)]
|
||||||
|
pub(crate) enum SpawnServerError {
|
||||||
|
#[error("failed to remove stdin socket")]
|
||||||
|
RemoveStdinSocket(#[source] std::io::Error),
|
||||||
|
|
||||||
|
#[error("failed to remove stdout socket")]
|
||||||
|
RemoveStdoutSocket(#[source] std::io::Error),
|
||||||
|
|
||||||
|
#[error("failed to remove stderr socket")]
|
||||||
|
RemoveStderrSocket(#[source] std::io::Error),
|
||||||
|
|
||||||
|
#[error("failed to get current_exe")]
|
||||||
|
CurrentExe(#[source] std::io::Error),
|
||||||
|
|
||||||
|
#[error("failed to launch server process")]
|
||||||
|
ProcessStatus(#[source] std::io::Error),
|
||||||
|
|
||||||
|
#[error("failed to launch and detach server process: {status}\n{paths}")]
|
||||||
|
LaunchStatus { status: ExitStatus, paths: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> {
|
||||||
if paths.stdin_socket.exists() {
|
if paths.stdin_socket.exists() {
|
||||||
std::fs::remove_file(&paths.stdin_socket)?;
|
std::fs::remove_file(&paths.stdin_socket).map_err(SpawnServerError::RemoveStdinSocket)?;
|
||||||
}
|
}
|
||||||
if paths.stdout_socket.exists() {
|
if paths.stdout_socket.exists() {
|
||||||
std::fs::remove_file(&paths.stdout_socket)?;
|
std::fs::remove_file(&paths.stdout_socket).map_err(SpawnServerError::RemoveStdoutSocket)?;
|
||||||
}
|
}
|
||||||
if paths.stderr_socket.exists() {
|
if paths.stderr_socket.exists() {
|
||||||
std::fs::remove_file(&paths.stderr_socket)?;
|
std::fs::remove_file(&paths.stderr_socket).map_err(SpawnServerError::RemoveStderrSocket)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let binary_name = std::env::current_exe()?;
|
let binary_name = std::env::current_exe().map_err(SpawnServerError::CurrentExe)?;
|
||||||
let mut server_process = std::process::Command::new(binary_name);
|
let mut server_process = std::process::Command::new(binary_name);
|
||||||
server_process
|
server_process
|
||||||
.arg("run")
|
.arg("run")
|
||||||
|
@ -692,11 +783,17 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
|
||||||
|
|
||||||
let status = server_process
|
let status = server_process
|
||||||
.status()
|
.status()
|
||||||
.context("failed to launch server process")?;
|
.map_err(SpawnServerError::ProcessStatus)?;
|
||||||
anyhow::ensure!(
|
|
||||||
status.success(),
|
if !status.success() {
|
||||||
"failed to launch and detach server process"
|
return Err(SpawnServerError::LaunchStatus {
|
||||||
);
|
status,
|
||||||
|
paths: format!(
|
||||||
|
"log file: {:?}, pid file: {:?}",
|
||||||
|
paths.log_file, paths.pid_file,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let mut total_time_waited = std::time::Duration::from_secs(0);
|
let mut total_time_waited = std::time::Duration::from_secs(0);
|
||||||
let wait_duration = std::time::Duration::from_millis(20);
|
let wait_duration = std::time::Duration::from_millis(20);
|
||||||
|
@ -717,7 +814,15 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_pid_file(path: &Path) -> Result<Option<u32>> {
|
#[derive(Debug, Error)]
|
||||||
|
#[error("Failed to remove PID file for missing process (pid `{pid}`")]
|
||||||
|
pub(crate) struct CheckPidError {
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
pid: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_pid_file(path: &Path) -> Result<Option<u32>, CheckPidError> {
|
||||||
let Some(pid) = std::fs::read_to_string(&path)
|
let Some(pid) = std::fs::read_to_string(&path)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|contents| contents.parse::<u32>().ok())
|
.and_then(|contents| contents.parse::<u32>().ok())
|
||||||
|
@ -742,7 +847,7 @@ fn check_pid_file(path: &Path) -> Result<Option<u32>> {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Found PID file, but process with that PID does not exist. Removing PID file."
|
"Found PID file, but process with that PID does not exist. Removing PID file."
|
||||||
);
|
);
|
||||||
std::fs::remove_file(&path).context("Failed to remove PID file")?;
|
std::fs::remove_file(&path).map_err(|source| CheckPidError { source, pid })?;
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3348,12 +3348,15 @@ impl SerializableItem for KeymapEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod persistence {
|
mod persistence {
|
||||||
use db::{define_connection, query, sqlez_macros::sql};
|
use db::{query, sqlez::domain::Domain, sqlez_macros::sql};
|
||||||
use workspace::WorkspaceDb;
|
use workspace::WorkspaceDb;
|
||||||
|
|
||||||
define_connection! {
|
pub struct KeybindingEditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||||
pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
|
|
||||||
&[sql!(
|
impl Domain for KeybindingEditorDb {
|
||||||
|
const NAME: &str = stringify!(KeybindingEditorDb);
|
||||||
|
|
||||||
|
const MIGRATIONS: &[&str] = &[sql!(
|
||||||
CREATE TABLE keybinding_editors (
|
CREATE TABLE keybinding_editors (
|
||||||
workspace_id INTEGER,
|
workspace_id INTEGER,
|
||||||
item_id INTEGER UNIQUE,
|
item_id INTEGER UNIQUE,
|
||||||
|
@ -3362,9 +3365,11 @@ mod persistence {
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)];
|
)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db::static_connection!(KEYBINDING_EDITORS, KeybindingEditorDb, [WorkspaceDb]);
|
||||||
|
|
||||||
impl KeybindingEditorDb {
|
impl KeybindingEditorDb {
|
||||||
query! {
|
query! {
|
||||||
pub async fn save_keybinding_editor(
|
pub async fn save_keybinding_editor(
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
use crate::connection::Connection;
|
use crate::connection::Connection;
|
||||||
|
|
||||||
pub trait Domain: 'static {
|
pub trait Domain: 'static {
|
||||||
fn name() -> &'static str;
|
const NAME: &str;
|
||||||
fn migrations() -> &'static [&'static str];
|
const MIGRATIONS: &[&str];
|
||||||
|
|
||||||
|
fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Migrator: 'static {
|
pub trait Migrator: 'static {
|
||||||
|
@ -17,7 +21,11 @@ impl Migrator for () {
|
||||||
|
|
||||||
impl<D: Domain> Migrator for D {
|
impl<D: Domain> Migrator for D {
|
||||||
fn migrate(connection: &Connection) -> anyhow::Result<()> {
|
fn migrate(connection: &Connection) -> anyhow::Result<()> {
|
||||||
connection.migrate(Self::name(), Self::migrations())
|
connection.migrate(
|
||||||
|
Self::NAME,
|
||||||
|
Self::MIGRATIONS,
|
||||||
|
Self::should_allow_migration_change,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,12 @@ impl Connection {
|
||||||
/// Note: Unlike everything else in SQLez, migrations are run eagerly, without first
|
/// Note: Unlike everything else in SQLez, migrations are run eagerly, without first
|
||||||
/// preparing the SQL statements. This makes it possible to do multi-statement schema
|
/// preparing the SQL statements. This makes it possible to do multi-statement schema
|
||||||
/// updates in a single string without running into prepare errors.
|
/// updates in a single string without running into prepare errors.
|
||||||
pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> {
|
pub fn migrate(
|
||||||
|
&self,
|
||||||
|
domain: &'static str,
|
||||||
|
migrations: &[&'static str],
|
||||||
|
mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool,
|
||||||
|
) -> Result<()> {
|
||||||
self.with_savepoint("migrating", || {
|
self.with_savepoint("migrating", || {
|
||||||
// Setup the migrations table unconditionally
|
// Setup the migrations table unconditionally
|
||||||
self.exec(indoc! {"
|
self.exec(indoc! {"
|
||||||
|
@ -65,9 +70,14 @@ impl Connection {
|
||||||
&sqlformat::QueryParams::None,
|
&sqlformat::QueryParams::None,
|
||||||
Default::default(),
|
Default::default(),
|
||||||
);
|
);
|
||||||
if completed_migration == migration {
|
if completed_migration == migration
|
||||||
|
|| migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE")
|
||||||
|
{
|
||||||
// Migration already run. Continue
|
// Migration already run. Continue
|
||||||
continue;
|
continue;
|
||||||
|
} else if should_allow_migration_change(index, &completed_migration, &migration)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
anyhow::bail!(formatdoc! {"
|
anyhow::bail!(formatdoc! {"
|
||||||
Migration changed for {domain} at step {index}
|
Migration changed for {domain} at step {index}
|
||||||
|
@ -108,6 +118,7 @@ mod test {
|
||||||
a TEXT,
|
a TEXT,
|
||||||
b TEXT
|
b TEXT
|
||||||
)"}],
|
)"}],
|
||||||
|
disallow_migration_change,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -136,6 +147,7 @@ mod test {
|
||||||
d TEXT
|
d TEXT
|
||||||
)"},
|
)"},
|
||||||
],
|
],
|
||||||
|
disallow_migration_change,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -214,7 +226,11 @@ mod test {
|
||||||
|
|
||||||
// Run the migration verifying that the row got dropped
|
// Run the migration verifying that the row got dropped
|
||||||
connection
|
connection
|
||||||
.migrate("test", &["DELETE FROM test_table"])
|
.migrate(
|
||||||
|
"test",
|
||||||
|
&["DELETE FROM test_table"],
|
||||||
|
disallow_migration_change,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection
|
||||||
|
@ -232,7 +248,11 @@ mod test {
|
||||||
|
|
||||||
// Run the same migration again and verify that the table was left unchanged
|
// Run the same migration again and verify that the table was left unchanged
|
||||||
connection
|
connection
|
||||||
.migrate("test", &["DELETE FROM test_table"])
|
.migrate(
|
||||||
|
"test",
|
||||||
|
&["DELETE FROM test_table"],
|
||||||
|
disallow_migration_change,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection
|
||||||
|
@ -252,27 +272,28 @@ mod test {
|
||||||
.migrate(
|
.migrate(
|
||||||
"test migration",
|
"test migration",
|
||||||
&[
|
&[
|
||||||
indoc! {"
|
"CREATE TABLE test (col INTEGER)",
|
||||||
CREATE TABLE test (
|
"INSERT INTO test (col) VALUES (1)",
|
||||||
col INTEGER
|
|
||||||
)"},
|
|
||||||
indoc! {"
|
|
||||||
INSERT INTO test (col) VALUES (1)"},
|
|
||||||
],
|
],
|
||||||
|
disallow_migration_change,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let mut migration_changed = false;
|
||||||
|
|
||||||
// Create another migration with the same domain but different steps
|
// Create another migration with the same domain but different steps
|
||||||
let second_migration_result = connection.migrate(
|
let second_migration_result = connection.migrate(
|
||||||
"test migration",
|
"test migration",
|
||||||
&[
|
&[
|
||||||
indoc! {"
|
"CREATE TABLE test (color INTEGER )",
|
||||||
CREATE TABLE test (
|
"INSERT INTO test (color) VALUES (1)",
|
||||||
color INTEGER
|
|
||||||
)"},
|
|
||||||
indoc! {"
|
|
||||||
INSERT INTO test (color) VALUES (1)"},
|
|
||||||
],
|
],
|
||||||
|
|_, old, new| {
|
||||||
|
assert_eq!(old, "CREATE TABLE test (col INTEGER)");
|
||||||
|
assert_eq!(new, "CREATE TABLE test (color INTEGER)");
|
||||||
|
migration_changed = true;
|
||||||
|
false
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify new migration returns error when run
|
// Verify new migration returns error when run
|
||||||
|
@ -284,7 +305,11 @@ mod test {
|
||||||
let connection = Connection::open_memory(Some("test_create_alter_drop"));
|
let connection = Connection::open_memory(Some("test_create_alter_drop"));
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"])
|
.migrate(
|
||||||
|
"first_migration",
|
||||||
|
&["CREATE TABLE table1(a TEXT) STRICT;"],
|
||||||
|
disallow_migration_change,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
connection
|
connection
|
||||||
|
@ -305,6 +330,7 @@ mod test {
|
||||||
|
|
||||||
ALTER TABLE table2 RENAME TO table1;
|
ALTER TABLE table2 RENAME TO table1;
|
||||||
"}],
|
"}],
|
||||||
|
disallow_migration_change,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -312,4 +338,8 @@ mod test {
|
||||||
|
|
||||||
assert_eq!(res, "test text");
|
assert_eq!(res, "test text");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -278,12 +278,8 @@ mod test {
|
||||||
|
|
||||||
enum TestDomain {}
|
enum TestDomain {}
|
||||||
impl Domain for TestDomain {
|
impl Domain for TestDomain {
|
||||||
fn name() -> &'static str {
|
const NAME: &str = "test";
|
||||||
"test"
|
const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"];
|
||||||
}
|
|
||||||
fn migrations() -> &'static [&'static str] {
|
|
||||||
&["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _ in 0..100 {
|
for _ in 0..100 {
|
||||||
|
@ -312,12 +308,9 @@ mod test {
|
||||||
fn wild_zed_lost_failure() {
|
fn wild_zed_lost_failure() {
|
||||||
enum TestWorkspace {}
|
enum TestWorkspace {}
|
||||||
impl Domain for TestWorkspace {
|
impl Domain for TestWorkspace {
|
||||||
fn name() -> &'static str {
|
const NAME: &str = "workspace";
|
||||||
"workspace"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn migrations() -> &'static [&'static str] {
|
const MIGRATIONS: &[&str] = &["
|
||||||
&["
|
|
||||||
CREATE TABLE workspaces(
|
CREATE TABLE workspaces(
|
||||||
workspace_id INTEGER PRIMARY KEY,
|
workspace_id INTEGER PRIMARY KEY,
|
||||||
dock_visible INTEGER, -- Boolean
|
dock_visible INTEGER, -- Boolean
|
||||||
|
@ -336,8 +329,7 @@ mod test {
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
ON UPDATE CASCADE
|
ON UPDATE CASCADE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
"]
|
"];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let builder =
|
let builder =
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
mod tab_switcher_tests;
|
mod tab_switcher_tests;
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::items::entry_git_aware_label_color;
|
use editor::items::{
|
||||||
|
entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color,
|
||||||
|
};
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::StringMatchCandidate;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle,
|
Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle,
|
||||||
Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Render,
|
Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point,
|
||||||
Styled, Task, WeakEntity, Window, actions, rems,
|
Render, Styled, Task, WeakEntity, Window, actions, rems,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
@ -15,11 +17,14 @@ use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{cmp::Reverse, sync::Arc};
|
use std::{cmp::Reverse, sync::Arc};
|
||||||
use ui::{ListItem, ListItemSpacing, Tooltip, prelude::*};
|
use ui::{
|
||||||
|
DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
ModalView, Pane, SaveIntent, Workspace,
|
ModalView, Pane, SaveIntent, Workspace,
|
||||||
item::{ItemHandle, ItemSettings, TabContentParams},
|
item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams},
|
||||||
pane::{Event as PaneEvent, render_item_indicator, tab_details},
|
pane::{Event as PaneEvent, render_item_indicator, tab_details},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -233,6 +238,77 @@ pub struct TabSwitcherDelegate {
|
||||||
restored_items: bool,
|
restored_items: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TabMatch {
|
||||||
|
fn icon(
|
||||||
|
&self,
|
||||||
|
project: &Entity<Project>,
|
||||||
|
selected: bool,
|
||||||
|
window: &Window,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<DecoratedIcon> {
|
||||||
|
let icon = self.item.tab_icon(window, cx)?;
|
||||||
|
let item_settings = ItemSettings::get_global(cx);
|
||||||
|
let show_diagnostics = item_settings.show_diagnostics;
|
||||||
|
let git_status_color = item_settings
|
||||||
|
.git_status
|
||||||
|
.then(|| {
|
||||||
|
let path = self.item.project_path(cx)?;
|
||||||
|
let project = project.read(cx);
|
||||||
|
let entry = project.entry_for_path(&path, cx)?;
|
||||||
|
let git_status = project
|
||||||
|
.project_path_git_status(&path, cx)
|
||||||
|
.map(|status| status.summary())
|
||||||
|
.unwrap_or_default();
|
||||||
|
Some(entry_git_aware_label_color(
|
||||||
|
git_status,
|
||||||
|
entry.is_ignored,
|
||||||
|
selected,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
let colored_icon = icon.color(git_status_color.unwrap_or_default());
|
||||||
|
|
||||||
|
let most_sever_diagostic_level = if show_diagnostics == ShowDiagnostics::Off {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let buffer_store = project.read(cx).buffer_store().read(cx);
|
||||||
|
let buffer = self
|
||||||
|
.item
|
||||||
|
.project_path(cx)
|
||||||
|
.and_then(|path| buffer_store.get_by_path(&path))
|
||||||
|
.map(|buffer| buffer.read(cx));
|
||||||
|
buffer.and_then(|buffer| {
|
||||||
|
buffer
|
||||||
|
.buffer_diagnostics(None)
|
||||||
|
.iter()
|
||||||
|
.map(|diagnostic_entry| diagnostic_entry.diagnostic.severity)
|
||||||
|
.min()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let decorations =
|
||||||
|
entry_diagnostic_aware_icon_decoration_and_color(most_sever_diagostic_level)
|
||||||
|
.filter(|(d, _)| {
|
||||||
|
*d != IconDecorationKind::Triangle
|
||||||
|
|| show_diagnostics != ShowDiagnostics::Errors
|
||||||
|
})
|
||||||
|
.map(|(icon, color)| {
|
||||||
|
let knockout_item_color = if selected {
|
||||||
|
cx.theme().colors().element_selected
|
||||||
|
} else {
|
||||||
|
cx.theme().colors().element_background
|
||||||
|
};
|
||||||
|
IconDecoration::new(icon, knockout_item_color, cx)
|
||||||
|
.color(color.color(cx))
|
||||||
|
.position(Point {
|
||||||
|
x: px(-2.),
|
||||||
|
y: px(-2.),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
Some(DecoratedIcon::new(colored_icon, decorations))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TabSwitcherDelegate {
|
impl TabSwitcherDelegate {
|
||||||
#[allow(clippy::complexity)]
|
#[allow(clippy::complexity)]
|
||||||
fn new(
|
fn new(
|
||||||
|
@ -574,31 +650,7 @@ impl PickerDelegate for TabSwitcherDelegate {
|
||||||
};
|
};
|
||||||
let label = tab_match.item.tab_content(params, window, cx);
|
let label = tab_match.item.tab_content(params, window, cx);
|
||||||
|
|
||||||
let icon = tab_match.item.tab_icon(window, cx).map(|icon| {
|
let icon = tab_match.icon(&self.project, selected, window, cx);
|
||||||
let git_status_color = ItemSettings::get_global(cx)
|
|
||||||
.git_status
|
|
||||||
.then(|| {
|
|
||||||
tab_match
|
|
||||||
.item
|
|
||||||
.project_path(cx)
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|path| {
|
|
||||||
let project = self.project.read(cx);
|
|
||||||
let entry = project.entry_for_path(path, cx)?;
|
|
||||||
let git_status = project
|
|
||||||
.project_path_git_status(path, cx)
|
|
||||||
.map(|status| status.summary())
|
|
||||||
.unwrap_or_default();
|
|
||||||
Some((entry, git_status))
|
|
||||||
})
|
|
||||||
.map(|(entry, git_status)| {
|
|
||||||
entry_git_aware_label_color(git_status, entry.is_ignored, selected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
icon.color(git_status_color.unwrap_or_default())
|
|
||||||
});
|
|
||||||
|
|
||||||
let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
|
let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
|
||||||
let indicator_color = if let Some(ref indicator) = indicator {
|
let indicator_color = if let Some(ref indicator) = indicator {
|
||||||
|
@ -640,7 +692,7 @@ impl PickerDelegate for TabSwitcherDelegate {
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.child(h_flex().w_full().child(label))
|
.child(h_flex().w_full().child(label))
|
||||||
.start_slot::<Icon>(icon)
|
.start_slot::<DecoratedIcon>(icon)
|
||||||
.map(|el| {
|
.map(|el| {
|
||||||
if self.selected_index == ix {
|
if self.selected_index == ix {
|
||||||
el.end_slot::<AnyElement>(close_button)
|
el.end_slot::<AnyElement>(close_button)
|
||||||
|
|
|
@ -9,7 +9,11 @@ use std::path::{Path, PathBuf};
|
||||||
use ui::{App, Context, Pixels, Window};
|
use ui::{App, Context, Pixels, Window};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
|
use db::{
|
||||||
|
query,
|
||||||
|
sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection},
|
||||||
|
sqlez_macros::sql,
|
||||||
|
};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace,
|
ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace,
|
||||||
WorkspaceDb, WorkspaceId,
|
WorkspaceDb, WorkspaceId,
|
||||||
|
@ -375,9 +379,13 @@ impl<'de> Deserialize<'de> for SerializedAxis {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
define_connection! {
|
pub struct TerminalDb(ThreadSafeConnection);
|
||||||
pub static ref TERMINAL_DB: TerminalDb<WorkspaceDb> =
|
|
||||||
&[sql!(
|
impl Domain for TerminalDb {
|
||||||
|
const NAME: &str = stringify!(TerminalDb);
|
||||||
|
|
||||||
|
const MIGRATIONS: &[&str] = &[
|
||||||
|
sql!(
|
||||||
CREATE TABLE terminals (
|
CREATE TABLE terminals (
|
||||||
workspace_id INTEGER,
|
workspace_id INTEGER,
|
||||||
item_id INTEGER UNIQUE,
|
item_id INTEGER UNIQUE,
|
||||||
|
@ -414,6 +422,8 @@ define_connection! {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]);
|
||||||
|
|
||||||
impl TerminalDb {
|
impl TerminalDb {
|
||||||
query! {
|
query! {
|
||||||
pub async fn update_workspace_id(
|
pub async fn update_workspace_id(
|
||||||
|
|
|
@ -561,7 +561,7 @@ impl ContextMenu {
|
||||||
action: Some(action.boxed_clone()),
|
action: Some(action.boxed_clone()),
|
||||||
handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
|
handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
|
||||||
icon: Some(IconName::ArrowUpRight),
|
icon: Some(IconName::ArrowUpRight),
|
||||||
icon_size: IconSize::Small,
|
icon_size: IconSize::XSmall,
|
||||||
icon_position: IconPosition::End,
|
icon_position: IconPosition::End,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|
|
@ -23,6 +23,8 @@ actions!(
|
||||||
HelixInsert,
|
HelixInsert,
|
||||||
/// Appends at the end of the selection.
|
/// Appends at the end of the selection.
|
||||||
HelixAppend,
|
HelixAppend,
|
||||||
|
/// Goes to the location of the last modification.
|
||||||
|
HelixGotoLastModification,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -31,6 +33,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||||
Vim::action(editor, cx, Vim::helix_insert);
|
Vim::action(editor, cx, Vim::helix_insert);
|
||||||
Vim::action(editor, cx, Vim::helix_append);
|
Vim::action(editor, cx, Vim::helix_append);
|
||||||
Vim::action(editor, cx, Vim::helix_yank);
|
Vim::action(editor, cx, Vim::helix_yank);
|
||||||
|
Vim::action(editor, cx, Vim::helix_goto_last_modification);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Vim {
|
impl Vim {
|
||||||
|
@ -430,6 +433,15 @@ impl Vim {
|
||||||
});
|
});
|
||||||
self.switch_mode(Mode::HelixNormal, true, window, cx);
|
self.switch_mode(Mode::HelixNormal, true, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn helix_goto_last_modification(
|
||||||
|
&mut self,
|
||||||
|
_: &HelixGotoLastModification,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.jump(".".into(), false, false, window, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -441,6 +453,7 @@ mod test {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_word_motions(cx: &mut gpui::TestAppContext) {
|
async fn test_word_motions(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.enable_helix();
|
||||||
// «
|
// «
|
||||||
// ˇ
|
// ˇ
|
||||||
// »
|
// »
|
||||||
|
@ -502,6 +515,7 @@ mod test {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_delete(cx: &mut gpui::TestAppContext) {
|
async fn test_delete(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.enable_helix();
|
||||||
|
|
||||||
// test delete a selection
|
// test delete a selection
|
||||||
cx.set_state(
|
cx.set_state(
|
||||||
|
@ -582,6 +596,7 @@ mod test {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
|
async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.enable_helix();
|
||||||
|
|
||||||
cx.set_state(
|
cx.set_state(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
|
@ -635,6 +650,7 @@ mod test {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_newline_char(cx: &mut gpui::TestAppContext) {
|
async fn test_newline_char(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.enable_helix();
|
||||||
|
|
||||||
cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
|
cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
|
||||||
|
|
||||||
|
@ -652,6 +668,7 @@ mod test {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
|
async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.enable_helix();
|
||||||
cx.set_state(
|
cx.set_state(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
«The ˇ»quick brown
|
«The ˇ»quick brown
|
||||||
|
@ -674,6 +691,7 @@ mod test {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_append(cx: &mut gpui::TestAppContext) {
|
async fn test_append(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.enable_helix();
|
||||||
// test from the end of the selection
|
// test from the end of the selection
|
||||||
cx.set_state(
|
cx.set_state(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
|
@ -716,6 +734,7 @@ mod test {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_replace(cx: &mut gpui::TestAppContext) {
|
async fn test_replace(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.enable_helix();
|
||||||
|
|
||||||
// No selection (single character)
|
// No selection (single character)
|
||||||
cx.set_state("ˇaa", Mode::HelixNormal);
|
cx.set_state("ˇaa", Mode::HelixNormal);
|
||||||
|
@ -763,4 +782,72 @@ mod test {
|
||||||
cx.shared_clipboard().assert_eq("worl");
|
cx.shared_clipboard().assert_eq("worl");
|
||||||
cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
|
cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
|
||||||
}
|
}
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.enable_helix();
|
||||||
|
|
||||||
|
// First copy some text to clipboard
|
||||||
|
cx.set_state("«hello worldˇ»", Mode::HelixNormal);
|
||||||
|
cx.simulate_keystrokes("y");
|
||||||
|
|
||||||
|
// Test paste with shift-r on single cursor
|
||||||
|
cx.set_state("foo ˇbar", Mode::HelixNormal);
|
||||||
|
cx.simulate_keystrokes("shift-r");
|
||||||
|
|
||||||
|
cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
|
||||||
|
|
||||||
|
// Test paste with shift-r on selection
|
||||||
|
cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
|
||||||
|
cx.simulate_keystrokes("shift-r");
|
||||||
|
|
||||||
|
cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.enable_helix();
|
||||||
|
|
||||||
|
// Make a modification at a specific location
|
||||||
|
cx.set_state("ˇhello", Mode::HelixNormal);
|
||||||
|
assert_eq!(cx.mode(), Mode::HelixNormal);
|
||||||
|
cx.simulate_keystrokes("i");
|
||||||
|
assert_eq!(cx.mode(), Mode::Insert);
|
||||||
|
cx.simulate_keystrokes("escape");
|
||||||
|
assert_eq!(cx.mode(), Mode::HelixNormal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.enable_helix();
|
||||||
|
|
||||||
|
// Make a modification at a specific location
|
||||||
|
cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
|
||||||
|
cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
|
||||||
|
cx.simulate_keystrokes("i");
|
||||||
|
cx.simulate_keystrokes("escape");
|
||||||
|
cx.simulate_keystrokes("i");
|
||||||
|
cx.simulate_keystrokes("m o d i f i e d space");
|
||||||
|
cx.simulate_keystrokes("escape");
|
||||||
|
|
||||||
|
// TODO: this fails, because state is no longer helix
|
||||||
|
cx.assert_state(
|
||||||
|
"line one\nline modified ˇtwo\nline three",
|
||||||
|
Mode::HelixNormal,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Move cursor away from the modification
|
||||||
|
cx.simulate_keystrokes("up");
|
||||||
|
|
||||||
|
// Use "g ." to go back to last modification
|
||||||
|
cx.simulate_keystrokes("g .");
|
||||||
|
|
||||||
|
// Verify we're back at the modification location and still in HelixNormal mode
|
||||||
|
cx.assert_state(
|
||||||
|
"line one\nline modifiedˇ two\nline three",
|
||||||
|
Mode::HelixNormal,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,10 @@ use crate::{motion::Motion, object::Object};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
|
use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
|
||||||
use db::define_connection;
|
use db::{
|
||||||
use db::sqlez_macros::sql;
|
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||||
|
sqlez_macros::sql,
|
||||||
|
};
|
||||||
use editor::display_map::{is_invisible, replacement};
|
use editor::display_map::{is_invisible, replacement};
|
||||||
use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint};
|
use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -1668,8 +1670,12 @@ impl MarksView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
define_connection! (
|
pub struct VimDb(ThreadSafeConnection);
|
||||||
pub static ref DB: VimDb<WorkspaceDb> = &[
|
|
||||||
|
impl Domain for VimDb {
|
||||||
|
const NAME: &str = stringify!(VimDb);
|
||||||
|
|
||||||
|
const MIGRATIONS: &[&str] = &[
|
||||||
sql! (
|
sql! (
|
||||||
CREATE TABLE vim_marks (
|
CREATE TABLE vim_marks (
|
||||||
workspace_id INTEGER,
|
workspace_id INTEGER,
|
||||||
|
@ -1689,7 +1695,9 @@ define_connection! (
|
||||||
ON vim_global_marks_paths(workspace_id, mark_name);
|
ON vim_global_marks_paths(workspace_id, mark_name);
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
);
|
}
|
||||||
|
|
||||||
|
db::static_connection!(DB, VimDb, [WorkspaceDb]);
|
||||||
|
|
||||||
struct SerializedMark {
|
struct SerializedMark {
|
||||||
path: Arc<Path>,
|
path: Arc<Path>,
|
||||||
|
|
|
@ -162,6 +162,19 @@ impl<T> Receiver<T> {
|
||||||
pending_waker_id: None,
|
pending_waker_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Receiver`] holding an initial value that will never change.
|
||||||
|
pub fn constant(value: T) -> Self {
|
||||||
|
let state = Arc::new(RwLock::new(State {
|
||||||
|
value,
|
||||||
|
wakers: BTreeMap::new(),
|
||||||
|
next_waker_id: WakerId::default(),
|
||||||
|
version: 0,
|
||||||
|
closed: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
Self { state, version: 0 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Clone> Receiver<T> {
|
impl<T: Clone> Receiver<T> {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
use gpui::{EventEmitter, FocusHandle, Focusable};
|
use gpui::{EventEmitter, FocusHandle, Focusable};
|
||||||
use ui::{
|
use ui::{
|
||||||
|
@ -12,7 +12,7 @@ use crate::Item;
|
||||||
/// A view to display when a certain buffer fails to open.
|
/// A view to display when a certain buffer fails to open.
|
||||||
pub struct InvalidBufferView {
|
pub struct InvalidBufferView {
|
||||||
/// Which path was attempted to open.
|
/// Which path was attempted to open.
|
||||||
pub abs_path: Arc<PathBuf>,
|
pub abs_path: Arc<Path>,
|
||||||
/// An error message, happened when opening the buffer.
|
/// An error message, happened when opening the buffer.
|
||||||
pub error: SharedString,
|
pub error: SharedString,
|
||||||
is_local: bool,
|
is_local: bool,
|
||||||
|
@ -21,7 +21,7 @@ pub struct InvalidBufferView {
|
||||||
|
|
||||||
impl InvalidBufferView {
|
impl InvalidBufferView {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
abs_path: PathBuf,
|
abs_path: &Path,
|
||||||
is_local: bool,
|
is_local: bool,
|
||||||
e: &anyhow::Error,
|
e: &anyhow::Error,
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
|
@ -29,7 +29,7 @@ impl InvalidBufferView {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
is_local,
|
is_local,
|
||||||
abs_path: Arc::new(abs_path),
|
abs_path: Arc::from(abs_path),
|
||||||
error: format!("{e}").into(),
|
error: format!("{e}").into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ impl Item for InvalidBufferView {
|
||||||
// Ensure we always render at least the filename.
|
// Ensure we always render at least the filename.
|
||||||
detail += 1;
|
detail += 1;
|
||||||
|
|
||||||
let path = self.abs_path.as_path();
|
let path = self.abs_path.as_ref();
|
||||||
|
|
||||||
let mut prefix = path;
|
let mut prefix = path;
|
||||||
while detail > 0 {
|
while detail > 0 {
|
||||||
|
|
|
@ -23,7 +23,7 @@ use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::PathBuf,
|
path::Path,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
@ -1168,7 +1168,7 @@ pub trait ProjectItem: Item {
|
||||||
/// with the error from that failure as an argument.
|
/// with the error from that failure as an argument.
|
||||||
/// Allows to open an item that can gracefully display and handle errors.
|
/// Allows to open an item that can gracefully display and handle errors.
|
||||||
fn for_broken_project_item(
|
fn for_broken_project_item(
|
||||||
_abs_path: PathBuf,
|
_abs_path: &Path,
|
||||||
_is_local: bool,
|
_is_local: bool,
|
||||||
_e: &anyhow::Error,
|
_e: &anyhow::Error,
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
|
|
|
@ -58,11 +58,7 @@ impl PathList {
|
||||||
let mut paths: Vec<PathBuf> = if serialized.paths.is_empty() {
|
let mut paths: Vec<PathBuf> = if serialized.paths.is_empty() {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
serde_json::from_str::<Vec<PathBuf>>(&serialized.paths)
|
serialized.paths.split('\n').map(PathBuf::from).collect()
|
||||||
.unwrap_or(Vec::new())
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| SanitizedPath::from(s).into())
|
|
||||||
.collect()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut order: Vec<usize> = serialized
|
let mut order: Vec<usize> = serialized
|
||||||
|
@ -85,7 +81,13 @@ impl PathList {
|
||||||
pub fn serialize(&self) -> SerializedPathList {
|
pub fn serialize(&self) -> SerializedPathList {
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
let paths = serde_json::to_string(&self.paths).unwrap_or_default();
|
let mut paths = String::new();
|
||||||
|
for path in self.paths.iter() {
|
||||||
|
if !paths.is_empty() {
|
||||||
|
paths.push('\n');
|
||||||
|
}
|
||||||
|
paths.push_str(&path.to_string_lossy());
|
||||||
|
}
|
||||||
|
|
||||||
let mut order = String::new();
|
let mut order = String::new();
|
||||||
for ix in self.order.iter() {
|
for ix in self.order.iter() {
|
||||||
|
|
|
@ -10,7 +10,11 @@ use std::{
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
|
use db::{
|
||||||
|
query,
|
||||||
|
sqlez::{connection::Connection, domain::Domain},
|
||||||
|
sqlez_macros::sql,
|
||||||
|
};
|
||||||
use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
|
use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
|
||||||
use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
|
use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
|
||||||
|
|
||||||
|
@ -275,186 +279,189 @@ impl sqlez::bindable::Bind for SerializedPixels {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
define_connection! {
|
pub struct WorkspaceDb(ThreadSafeConnection);
|
||||||
pub static ref DB: WorkspaceDb<()> =
|
|
||||||
&[
|
|
||||||
sql!(
|
|
||||||
CREATE TABLE workspaces(
|
|
||||||
workspace_id INTEGER PRIMARY KEY,
|
|
||||||
workspace_location BLOB UNIQUE,
|
|
||||||
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
|
||||||
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
|
|
||||||
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
|
||||||
left_sidebar_open INTEGER, // Boolean
|
|
||||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
|
|
||||||
) STRICT;
|
|
||||||
|
|
||||||
CREATE TABLE pane_groups(
|
impl Domain for WorkspaceDb {
|
||||||
group_id INTEGER PRIMARY KEY,
|
const NAME: &str = stringify!(WorkspaceDb);
|
||||||
workspace_id INTEGER NOT NULL,
|
|
||||||
parent_group_id INTEGER, // NULL indicates that this is a root node
|
|
||||||
position INTEGER, // NULL indicates that this is a root node
|
|
||||||
axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
|
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
|
||||||
ON DELETE CASCADE
|
|
||||||
ON UPDATE CASCADE,
|
|
||||||
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
|
||||||
) STRICT;
|
|
||||||
|
|
||||||
CREATE TABLE panes(
|
const MIGRATIONS: &[&str] = &[
|
||||||
pane_id INTEGER PRIMARY KEY,
|
sql!(
|
||||||
workspace_id INTEGER NOT NULL,
|
CREATE TABLE workspaces(
|
||||||
active INTEGER NOT NULL, // Boolean
|
workspace_id INTEGER PRIMARY KEY,
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
workspace_location BLOB UNIQUE,
|
||||||
ON DELETE CASCADE
|
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||||
ON UPDATE CASCADE
|
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
|
||||||
) STRICT;
|
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||||
|
left_sidebar_open INTEGER, // Boolean
|
||||||
|
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
CREATE TABLE center_panes(
|
CREATE TABLE pane_groups(
|
||||||
pane_id INTEGER PRIMARY KEY,
|
group_id INTEGER PRIMARY KEY,
|
||||||
parent_group_id INTEGER, // NULL means that this is a root pane
|
workspace_id INTEGER NOT NULL,
|
||||||
position INTEGER, // NULL means that this is a root pane
|
parent_group_id INTEGER, // NULL indicates that this is a root node
|
||||||
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
|
position INTEGER, // NULL indicates that this is a root node
|
||||||
ON DELETE CASCADE,
|
axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
|
||||||
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||||
) STRICT;
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE,
|
||||||
|
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
CREATE TABLE items(
|
CREATE TABLE panes(
|
||||||
item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
|
pane_id INTEGER PRIMARY KEY,
|
||||||
workspace_id INTEGER NOT NULL,
|
workspace_id INTEGER NOT NULL,
|
||||||
pane_id INTEGER NOT NULL,
|
active INTEGER NOT NULL, // Boolean
|
||||||
kind TEXT NOT NULL,
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||||
position INTEGER NOT NULL,
|
ON DELETE CASCADE
|
||||||
active INTEGER NOT NULL,
|
ON UPDATE CASCADE
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
) STRICT;
|
||||||
ON DELETE CASCADE
|
|
||||||
ON UPDATE CASCADE,
|
CREATE TABLE center_panes(
|
||||||
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
|
pane_id INTEGER PRIMARY KEY,
|
||||||
ON DELETE CASCADE,
|
parent_group_id INTEGER, // NULL means that this is a root pane
|
||||||
PRIMARY KEY(item_id, workspace_id)
|
position INTEGER, // NULL means that this is a root pane
|
||||||
) STRICT;
|
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
|
||||||
),
|
ON DELETE CASCADE,
|
||||||
sql!(
|
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
||||||
ALTER TABLE workspaces ADD COLUMN window_state TEXT;
|
) STRICT;
|
||||||
ALTER TABLE workspaces ADD COLUMN window_x REAL;
|
|
||||||
ALTER TABLE workspaces ADD COLUMN window_y REAL;
|
CREATE TABLE items(
|
||||||
ALTER TABLE workspaces ADD COLUMN window_width REAL;
|
item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
|
||||||
ALTER TABLE workspaces ADD COLUMN window_height REAL;
|
workspace_id INTEGER NOT NULL,
|
||||||
ALTER TABLE workspaces ADD COLUMN display BLOB;
|
pane_id INTEGER NOT NULL,
|
||||||
),
|
kind TEXT NOT NULL,
|
||||||
// Drop foreign key constraint from workspaces.dock_pane to panes table.
|
position INTEGER NOT NULL,
|
||||||
sql!(
|
active INTEGER NOT NULL,
|
||||||
CREATE TABLE workspaces_2(
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||||
workspace_id INTEGER PRIMARY KEY,
|
ON DELETE CASCADE
|
||||||
workspace_location BLOB UNIQUE,
|
ON UPDATE CASCADE,
|
||||||
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
|
||||||
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
|
ON DELETE CASCADE,
|
||||||
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
PRIMARY KEY(item_id, workspace_id)
|
||||||
left_sidebar_open INTEGER, // Boolean
|
) STRICT;
|
||||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
),
|
||||||
window_state TEXT,
|
sql!(
|
||||||
window_x REAL,
|
ALTER TABLE workspaces ADD COLUMN window_state TEXT;
|
||||||
window_y REAL,
|
ALTER TABLE workspaces ADD COLUMN window_x REAL;
|
||||||
window_width REAL,
|
ALTER TABLE workspaces ADD COLUMN window_y REAL;
|
||||||
window_height REAL,
|
ALTER TABLE workspaces ADD COLUMN window_width REAL;
|
||||||
display BLOB
|
ALTER TABLE workspaces ADD COLUMN window_height REAL;
|
||||||
) STRICT;
|
ALTER TABLE workspaces ADD COLUMN display BLOB;
|
||||||
INSERT INTO workspaces_2 SELECT * FROM workspaces;
|
),
|
||||||
DROP TABLE workspaces;
|
// Drop foreign key constraint from workspaces.dock_pane to panes table.
|
||||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
sql!(
|
||||||
),
|
CREATE TABLE workspaces_2(
|
||||||
// Add panels related information
|
workspace_id INTEGER PRIMARY KEY,
|
||||||
sql!(
|
workspace_location BLOB UNIQUE,
|
||||||
ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
|
dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||||
ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
|
dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
|
||||||
ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
|
dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
|
||||||
ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
|
left_sidebar_open INTEGER, // Boolean
|
||||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
|
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
|
window_state TEXT,
|
||||||
),
|
window_x REAL,
|
||||||
// Add panel zoom persistence
|
window_y REAL,
|
||||||
sql!(
|
window_width REAL,
|
||||||
ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
|
window_height REAL,
|
||||||
ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
|
display BLOB
|
||||||
ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
|
) STRICT;
|
||||||
),
|
INSERT INTO workspaces_2 SELECT * FROM workspaces;
|
||||||
// Add pane group flex data
|
DROP TABLE workspaces;
|
||||||
sql!(
|
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||||
ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
|
),
|
||||||
),
|
// Add panels related information
|
||||||
// Add fullscreen field to workspace
|
sql!(
|
||||||
// Deprecated, `WindowBounds` holds the fullscreen state now.
|
ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
|
||||||
// Preserving so users can downgrade Zed.
|
ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
|
||||||
sql!(
|
ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
|
||||||
ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
|
ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
|
||||||
),
|
ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
|
||||||
// Add preview field to items
|
ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
|
||||||
sql!(
|
),
|
||||||
ALTER TABLE items ADD COLUMN preview INTEGER; //bool
|
// Add panel zoom persistence
|
||||||
),
|
sql!(
|
||||||
// Add centered_layout field to workspace
|
ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
|
||||||
sql!(
|
ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
|
||||||
ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
|
ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
|
||||||
),
|
),
|
||||||
sql!(
|
// Add pane group flex data
|
||||||
CREATE TABLE remote_projects (
|
sql!(
|
||||||
remote_project_id INTEGER NOT NULL UNIQUE,
|
ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
|
||||||
path TEXT,
|
),
|
||||||
dev_server_name TEXT
|
// Add fullscreen field to workspace
|
||||||
);
|
// Deprecated, `WindowBounds` holds the fullscreen state now.
|
||||||
ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
|
// Preserving so users can downgrade Zed.
|
||||||
ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
|
sql!(
|
||||||
),
|
ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
|
||||||
sql!(
|
),
|
||||||
DROP TABLE remote_projects;
|
// Add preview field to items
|
||||||
CREATE TABLE dev_server_projects (
|
sql!(
|
||||||
id INTEGER NOT NULL UNIQUE,
|
ALTER TABLE items ADD COLUMN preview INTEGER; //bool
|
||||||
path TEXT,
|
),
|
||||||
dev_server_name TEXT
|
// Add centered_layout field to workspace
|
||||||
);
|
sql!(
|
||||||
ALTER TABLE workspaces DROP COLUMN remote_project_id;
|
ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
|
||||||
ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
|
),
|
||||||
),
|
sql!(
|
||||||
sql!(
|
CREATE TABLE remote_projects (
|
||||||
ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
|
remote_project_id INTEGER NOT NULL UNIQUE,
|
||||||
),
|
path TEXT,
|
||||||
sql!(
|
dev_server_name TEXT
|
||||||
ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
|
);
|
||||||
),
|
ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
|
||||||
sql!(
|
ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
|
||||||
ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
|
),
|
||||||
),
|
sql!(
|
||||||
sql!(
|
DROP TABLE remote_projects;
|
||||||
ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
|
CREATE TABLE dev_server_projects (
|
||||||
),
|
id INTEGER NOT NULL UNIQUE,
|
||||||
sql!(
|
path TEXT,
|
||||||
CREATE TABLE ssh_projects (
|
dev_server_name TEXT
|
||||||
id INTEGER PRIMARY KEY,
|
);
|
||||||
host TEXT NOT NULL,
|
ALTER TABLE workspaces DROP COLUMN remote_project_id;
|
||||||
port INTEGER,
|
ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
|
||||||
path TEXT NOT NULL,
|
),
|
||||||
user TEXT
|
sql!(
|
||||||
);
|
ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
|
||||||
ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
|
),
|
||||||
),
|
sql!(
|
||||||
sql!(
|
ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
|
||||||
ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
|
),
|
||||||
),
|
sql!(
|
||||||
sql!(
|
ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
|
||||||
CREATE TABLE toolchains (
|
),
|
||||||
workspace_id INTEGER,
|
sql!(
|
||||||
worktree_id INTEGER,
|
ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
|
||||||
language_name TEXT NOT NULL,
|
),
|
||||||
name TEXT NOT NULL,
|
sql!(
|
||||||
path TEXT NOT NULL,
|
CREATE TABLE ssh_projects (
|
||||||
PRIMARY KEY (workspace_id, worktree_id, language_name)
|
id INTEGER PRIMARY KEY,
|
||||||
);
|
host TEXT NOT NULL,
|
||||||
),
|
port INTEGER,
|
||||||
sql!(
|
path TEXT NOT NULL,
|
||||||
ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
|
user TEXT
|
||||||
),
|
);
|
||||||
sql!(
|
ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
|
||||||
|
),
|
||||||
|
sql!(
|
||||||
|
ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
|
||||||
|
),
|
||||||
|
sql!(
|
||||||
|
CREATE TABLE toolchains (
|
||||||
|
workspace_id INTEGER,
|
||||||
|
worktree_id INTEGER,
|
||||||
|
language_name TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (workspace_id, worktree_id, language_name)
|
||||||
|
);
|
||||||
|
),
|
||||||
|
sql!(
|
||||||
|
ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
|
||||||
|
),
|
||||||
|
sql!(
|
||||||
CREATE TABLE breakpoints (
|
CREATE TABLE breakpoints (
|
||||||
workspace_id INTEGER NOT NULL,
|
workspace_id INTEGER NOT NULL,
|
||||||
path TEXT NOT NULL,
|
path TEXT NOT NULL,
|
||||||
|
@ -466,141 +473,165 @@ define_connection! {
|
||||||
ON UPDATE CASCADE
|
ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
),
|
),
|
||||||
sql!(
|
sql!(
|
||||||
ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
|
ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
|
||||||
CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
|
CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
|
||||||
ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
|
ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
|
||||||
),
|
),
|
||||||
sql!(
|
sql!(
|
||||||
ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
|
ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
|
||||||
),
|
),
|
||||||
sql!(
|
sql!(
|
||||||
ALTER TABLE breakpoints DROP COLUMN kind
|
ALTER TABLE breakpoints DROP COLUMN kind
|
||||||
),
|
),
|
||||||
sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
|
sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
|
||||||
sql!(
|
sql!(
|
||||||
ALTER TABLE breakpoints ADD COLUMN condition TEXT;
|
ALTER TABLE breakpoints ADD COLUMN condition TEXT;
|
||||||
ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
|
ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
|
||||||
),
|
),
|
||||||
sql!(CREATE TABLE toolchains2 (
|
sql!(CREATE TABLE toolchains2 (
|
||||||
workspace_id INTEGER,
|
workspace_id INTEGER,
|
||||||
worktree_id INTEGER,
|
worktree_id INTEGER,
|
||||||
language_name TEXT NOT NULL,
|
language_name TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
path TEXT NOT NULL,
|
path TEXT NOT NULL,
|
||||||
raw_json TEXT NOT NULL,
|
raw_json TEXT NOT NULL,
|
||||||
relative_worktree_path TEXT NOT NULL,
|
relative_worktree_path TEXT NOT NULL,
|
||||||
PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
|
PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
|
||||||
INSERT INTO toolchains2
|
INSERT INTO toolchains2
|
||||||
SELECT * FROM toolchains;
|
SELECT * FROM toolchains;
|
||||||
DROP TABLE toolchains;
|
DROP TABLE toolchains;
|
||||||
ALTER TABLE toolchains2 RENAME TO toolchains;
|
ALTER TABLE toolchains2 RENAME TO toolchains;
|
||||||
),
|
),
|
||||||
sql!(
|
sql!(
|
||||||
CREATE TABLE ssh_connections (
|
CREATE TABLE ssh_connections (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
host TEXT NOT NULL,
|
host TEXT NOT NULL,
|
||||||
port INTEGER,
|
port INTEGER,
|
||||||
user TEXT
|
user TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO ssh_connections (host, port, user)
|
INSERT INTO ssh_connections (host, port, user)
|
||||||
SELECT DISTINCT host, port, user
|
SELECT DISTINCT host, port, user
|
||||||
FROM ssh_projects;
|
FROM ssh_projects;
|
||||||
|
|
||||||
CREATE TABLE workspaces_2(
|
CREATE TABLE workspaces_2(
|
||||||
workspace_id INTEGER PRIMARY KEY,
|
workspace_id INTEGER PRIMARY KEY,
|
||||||
paths TEXT,
|
paths TEXT,
|
||||||
paths_order TEXT,
|
paths_order TEXT,
|
||||||
ssh_connection_id INTEGER REFERENCES ssh_connections(id),
|
ssh_connection_id INTEGER REFERENCES ssh_connections(id),
|
||||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
window_state TEXT,
|
window_state TEXT,
|
||||||
window_x REAL,
|
window_x REAL,
|
||||||
window_y REAL,
|
window_y REAL,
|
||||||
window_width REAL,
|
window_width REAL,
|
||||||
window_height REAL,
|
window_height REAL,
|
||||||
display BLOB,
|
display BLOB,
|
||||||
left_dock_visible INTEGER,
|
left_dock_visible INTEGER,
|
||||||
left_dock_active_panel TEXT,
|
left_dock_active_panel TEXT,
|
||||||
right_dock_visible INTEGER,
|
right_dock_visible INTEGER,
|
||||||
right_dock_active_panel TEXT,
|
right_dock_active_panel TEXT,
|
||||||
bottom_dock_visible INTEGER,
|
bottom_dock_visible INTEGER,
|
||||||
bottom_dock_active_panel TEXT,
|
bottom_dock_active_panel TEXT,
|
||||||
left_dock_zoom INTEGER,
|
left_dock_zoom INTEGER,
|
||||||
right_dock_zoom INTEGER,
|
right_dock_zoom INTEGER,
|
||||||
bottom_dock_zoom INTEGER,
|
bottom_dock_zoom INTEGER,
|
||||||
fullscreen INTEGER,
|
fullscreen INTEGER,
|
||||||
centered_layout INTEGER,
|
centered_layout INTEGER,
|
||||||
session_id TEXT,
|
session_id TEXT,
|
||||||
window_id INTEGER
|
window_id INTEGER
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
INSERT
|
INSERT
|
||||||
INTO workspaces_2
|
INTO workspaces_2
|
||||||
SELECT
|
SELECT
|
||||||
workspaces.workspace_id,
|
workspaces.workspace_id,
|
||||||
CASE
|
CASE
|
||||||
WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
|
WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
|
||||||
|
ELSE
|
||||||
|
CASE
|
||||||
|
WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
|
||||||
|
NULL
|
||||||
|
ELSE
|
||||||
|
replace(workspaces.local_paths_array, ',', CHAR(10))
|
||||||
|
END
|
||||||
|
END as paths,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN ssh_projects.id IS NOT NULL THEN ""
|
||||||
|
ELSE workspaces.local_paths_order_array
|
||||||
|
END as paths_order,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN ssh_projects.id IS NOT NULL THEN (
|
||||||
|
SELECT ssh_connections.id
|
||||||
|
FROM ssh_connections
|
||||||
|
WHERE
|
||||||
|
ssh_connections.host IS ssh_projects.host AND
|
||||||
|
ssh_connections.port IS ssh_projects.port AND
|
||||||
|
ssh_connections.user IS ssh_projects.user
|
||||||
|
)
|
||||||
|
ELSE NULL
|
||||||
|
END as ssh_connection_id,
|
||||||
|
|
||||||
|
workspaces.timestamp,
|
||||||
|
workspaces.window_state,
|
||||||
|
workspaces.window_x,
|
||||||
|
workspaces.window_y,
|
||||||
|
workspaces.window_width,
|
||||||
|
workspaces.window_height,
|
||||||
|
workspaces.display,
|
||||||
|
workspaces.left_dock_visible,
|
||||||
|
workspaces.left_dock_active_panel,
|
||||||
|
workspaces.right_dock_visible,
|
||||||
|
workspaces.right_dock_active_panel,
|
||||||
|
workspaces.bottom_dock_visible,
|
||||||
|
workspaces.bottom_dock_active_panel,
|
||||||
|
workspaces.left_dock_zoom,
|
||||||
|
workspaces.right_dock_zoom,
|
||||||
|
workspaces.bottom_dock_zoom,
|
||||||
|
workspaces.fullscreen,
|
||||||
|
workspaces.centered_layout,
|
||||||
|
workspaces.session_id,
|
||||||
|
workspaces.window_id
|
||||||
|
FROM
|
||||||
|
workspaces LEFT JOIN
|
||||||
|
ssh_projects ON
|
||||||
|
workspaces.ssh_project_id = ssh_projects.id;
|
||||||
|
|
||||||
|
DROP TABLE ssh_projects;
|
||||||
|
DROP TABLE workspaces;
|
||||||
|
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
|
||||||
|
),
|
||||||
|
// Fix any data from when workspaces.paths were briefly encoded as JSON arrays
|
||||||
|
sql!(
|
||||||
|
UPDATE workspaces
|
||||||
|
SET paths = CASE
|
||||||
|
WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN
|
||||||
|
replace(
|
||||||
|
substr(paths, 3, length(paths) - 4),
|
||||||
|
'"' || ',' || '"',
|
||||||
|
CHAR(10)
|
||||||
|
)
|
||||||
ELSE
|
ELSE
|
||||||
CASE
|
replace(paths, ',', CHAR(10))
|
||||||
WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
|
END
|
||||||
NULL
|
WHERE paths IS NOT NULL
|
||||||
ELSE
|
),
|
||||||
json('[' || '"' || replace(workspaces.local_paths_array, ',', '"' || "," || '"') || '"' || ']')
|
|
||||||
END
|
|
||||||
END as paths,
|
|
||||||
|
|
||||||
CASE
|
|
||||||
WHEN ssh_projects.id IS NOT NULL THEN ""
|
|
||||||
ELSE workspaces.local_paths_order_array
|
|
||||||
END as paths_order,
|
|
||||||
|
|
||||||
CASE
|
|
||||||
WHEN ssh_projects.id IS NOT NULL THEN (
|
|
||||||
SELECT ssh_connections.id
|
|
||||||
FROM ssh_connections
|
|
||||||
WHERE
|
|
||||||
ssh_connections.host IS ssh_projects.host AND
|
|
||||||
ssh_connections.port IS ssh_projects.port AND
|
|
||||||
ssh_connections.user IS ssh_projects.user
|
|
||||||
)
|
|
||||||
ELSE NULL
|
|
||||||
END as ssh_connection_id,
|
|
||||||
|
|
||||||
workspaces.timestamp,
|
|
||||||
workspaces.window_state,
|
|
||||||
workspaces.window_x,
|
|
||||||
workspaces.window_y,
|
|
||||||
workspaces.window_width,
|
|
||||||
workspaces.window_height,
|
|
||||||
workspaces.display,
|
|
||||||
workspaces.left_dock_visible,
|
|
||||||
workspaces.left_dock_active_panel,
|
|
||||||
workspaces.right_dock_visible,
|
|
||||||
workspaces.right_dock_active_panel,
|
|
||||||
workspaces.bottom_dock_visible,
|
|
||||||
workspaces.bottom_dock_active_panel,
|
|
||||||
workspaces.left_dock_zoom,
|
|
||||||
workspaces.right_dock_zoom,
|
|
||||||
workspaces.bottom_dock_zoom,
|
|
||||||
workspaces.fullscreen,
|
|
||||||
workspaces.centered_layout,
|
|
||||||
workspaces.session_id,
|
|
||||||
workspaces.window_id
|
|
||||||
FROM
|
|
||||||
workspaces LEFT JOIN
|
|
||||||
ssh_projects ON
|
|
||||||
workspaces.ssh_project_id = ssh_projects.id;
|
|
||||||
|
|
||||||
DROP TABLE ssh_projects;
|
|
||||||
DROP TABLE workspaces;
|
|
||||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Allow recovering from bad migration that was initially shipped to nightly
|
||||||
|
// when introducing the ssh_connections table.
|
||||||
|
fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool {
|
||||||
|
old.starts_with("CREATE TABLE ssh_connections")
|
||||||
|
&& new.starts_with("CREATE TABLE ssh_connections")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db::static_connection!(DB, WorkspaceDb, []);
|
||||||
|
|
||||||
impl WorkspaceDb {
|
impl WorkspaceDb {
|
||||||
/// Returns a serialized workspace for the given worktree_roots. If the passed array
|
/// Returns a serialized workspace for the given worktree_roots. If the passed array
|
||||||
/// is empty, the most recent workspace is returned instead. If no workspace for the
|
/// is empty, the most recent workspace is returned instead. If no workspace for the
|
||||||
|
@ -1803,6 +1834,7 @@ mod tests {
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)],
|
)],
|
||||||
|
|_, _, _| false,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
})
|
})
|
||||||
|
@ -1851,6 +1883,7 @@ mod tests {
|
||||||
REFERENCES workspaces(workspace_id)
|
REFERENCES workspaces(workspace_id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
) STRICT;)],
|
) STRICT;)],
|
||||||
|
|_, _, _| false,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -613,48 +613,59 @@ impl ProjectItemRegistry {
|
||||||
self.build_project_item_for_path_fns
|
self.build_project_item_for_path_fns
|
||||||
.push(|project, project_path, window, cx| {
|
.push(|project, project_path, window, cx| {
|
||||||
let project_path = project_path.clone();
|
let project_path = project_path.clone();
|
||||||
let abs_path = project.read(cx).absolute_path(&project_path, cx);
|
let is_file = project
|
||||||
|
.read(cx)
|
||||||
|
.entry_for_path(&project_path, cx)
|
||||||
|
.is_some_and(|entry| entry.is_file());
|
||||||
|
let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
|
||||||
let is_local = project.read(cx).is_local();
|
let is_local = project.read(cx).is_local();
|
||||||
let project_item =
|
let project_item =
|
||||||
<T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
|
<T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
|
||||||
let project = project.clone();
|
let project = project.clone();
|
||||||
Some(window.spawn(cx, async move |cx| match project_item.await {
|
Some(window.spawn(cx, async move |cx| {
|
||||||
Ok(project_item) => {
|
match project_item.await.with_context(|| {
|
||||||
let project_item = project_item;
|
format!(
|
||||||
let project_entry_id: Option<ProjectEntryId> =
|
"opening project path {:?}",
|
||||||
project_item.read_with(cx, project::ProjectItem::entry_id)?;
|
entry_abs_path.as_deref().unwrap_or(&project_path.path)
|
||||||
let build_workspace_item = Box::new(
|
)
|
||||||
|pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
|
}) {
|
||||||
Box::new(cx.new(|cx| {
|
Ok(project_item) => {
|
||||||
T::for_project_item(
|
let project_item = project_item;
|
||||||
project,
|
let project_entry_id: Option<ProjectEntryId> =
|
||||||
Some(pane),
|
project_item.read_with(cx, project::ProjectItem::entry_id)?;
|
||||||
project_item,
|
let build_workspace_item = Box::new(
|
||||||
window,
|
|pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
|
||||||
cx,
|
Box::new(cx.new(|cx| {
|
||||||
)
|
T::for_project_item(
|
||||||
})) as Box<dyn ItemHandle>
|
project,
|
||||||
},
|
Some(pane),
|
||||||
) as Box<_>;
|
project_item,
|
||||||
Ok((project_entry_id, build_workspace_item))
|
window,
|
||||||
}
|
cx,
|
||||||
Err(e) => match abs_path {
|
)
|
||||||
Some(abs_path) => match cx.update(|window, cx| {
|
})) as Box<dyn ItemHandle>
|
||||||
T::for_broken_project_item(abs_path, is_local, &e, window, cx)
|
},
|
||||||
})? {
|
) as Box<_>;
|
||||||
Some(broken_project_item_view) => {
|
Ok((project_entry_id, build_workspace_item))
|
||||||
let build_workspace_item = Box::new(
|
}
|
||||||
|
Err(e) => match entry_abs_path.as_deref().filter(|_| is_file) {
|
||||||
|
Some(abs_path) => match cx.update(|window, cx| {
|
||||||
|
T::for_broken_project_item(abs_path, is_local, &e, window, cx)
|
||||||
|
})? {
|
||||||
|
Some(broken_project_item_view) => {
|
||||||
|
let build_workspace_item = Box::new(
|
||||||
move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
|
move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
|
||||||
cx.new(|_| broken_project_item_view).boxed_clone()
|
cx.new(|_| broken_project_item_view).boxed_clone()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
as Box<_>;
|
as Box<_>;
|
||||||
Ok((None, build_workspace_item))
|
Ok((None, build_workspace_item))
|
||||||
}
|
}
|
||||||
|
None => Err(e)?,
|
||||||
|
},
|
||||||
None => Err(e)?,
|
None => Err(e)?,
|
||||||
},
|
},
|
||||||
None => Err(e)?,
|
}
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4011,52 +4022,6 @@ impl Workspace {
|
||||||
maybe_pane_handle
|
maybe_pane_handle
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn split_pane_with_item(
|
|
||||||
&mut self,
|
|
||||||
pane_to_split: WeakEntity<Pane>,
|
|
||||||
split_direction: SplitDirection,
|
|
||||||
from: WeakEntity<Pane>,
|
|
||||||
item_id_to_move: EntityId,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let Some(pane_to_split) = pane_to_split.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(from) = from.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_pane = self.add_pane(window, cx);
|
|
||||||
move_item(&from, &new_pane, item_id_to_move, 0, true, window, cx);
|
|
||||||
self.center
|
|
||||||
.split(&pane_to_split, &new_pane, split_direction)
|
|
||||||
.unwrap();
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn split_pane_with_project_entry(
|
|
||||||
&mut self,
|
|
||||||
pane_to_split: WeakEntity<Pane>,
|
|
||||||
split_direction: SplitDirection,
|
|
||||||
project_entry: ProjectEntryId,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Option<Task<Result<()>>> {
|
|
||||||
let pane_to_split = pane_to_split.upgrade()?;
|
|
||||||
let new_pane = self.add_pane(window, cx);
|
|
||||||
self.center
|
|
||||||
.split(&pane_to_split, &new_pane, split_direction)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
|
|
||||||
let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx);
|
|
||||||
Some(cx.foreground_executor().spawn(async move {
|
|
||||||
task.await?;
|
|
||||||
Ok(())
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let active_item = self.active_pane.read(cx).active_item();
|
let active_item = self.active_pane.read(cx).active_item();
|
||||||
for pane in &self.panes {
|
for pane in &self.panes {
|
||||||
|
@ -6622,15 +6587,29 @@ impl Render for Workspace {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.children(self.zoomed.as_ref().and_then(|view| {
|
.children(self.zoomed.as_ref().and_then(|view| {
|
||||||
Some(div()
|
let zoomed_view = view.upgrade()?;
|
||||||
|
let div = div()
|
||||||
.occlude()
|
.occlude()
|
||||||
.absolute()
|
.absolute()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.border_color(colors.border)
|
.border_color(colors.border)
|
||||||
.bg(colors.background)
|
.bg(colors.background)
|
||||||
.child(view.upgrade()?)
|
.child(zoomed_view)
|
||||||
.inset_0()
|
.inset_0()
|
||||||
.shadow_lg())
|
.shadow_lg();
|
||||||
|
|
||||||
|
if !WorkspaceSettings::get_global(cx).zoomed_padding {
|
||||||
|
return Some(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(match self.zoomed_position {
|
||||||
|
Some(DockPosition::Left) => div.right_2().border_r_1(),
|
||||||
|
Some(DockPosition::Right) => div.left_2().border_l_1(),
|
||||||
|
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
|
||||||
|
None => {
|
||||||
|
div.top_2().bottom_2().left_2().right_2().border_1()
|
||||||
|
}
|
||||||
|
})
|
||||||
}))
|
}))
|
||||||
.children(self.render_notifications(window, cx)),
|
.children(self.render_notifications(window, cx)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,6 +29,7 @@ pub struct WorkspaceSettings {
|
||||||
pub on_last_window_closed: OnLastWindowClosed,
|
pub on_last_window_closed: OnLastWindowClosed,
|
||||||
pub resize_all_panels_in_dock: Vec<DockPosition>,
|
pub resize_all_panels_in_dock: Vec<DockPosition>,
|
||||||
pub close_on_file_delete: bool,
|
pub close_on_file_delete: bool,
|
||||||
|
pub zoomed_padding: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)]
|
#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
|
@ -202,6 +203,12 @@ pub struct WorkspaceSettingsContent {
|
||||||
///
|
///
|
||||||
/// Default: false
|
/// Default: false
|
||||||
pub close_on_file_delete: Option<bool>,
|
pub close_on_file_delete: Option<bool>,
|
||||||
|
/// Whether to show padding for zoomed panels.
|
||||||
|
/// When enabled, zoomed bottom panels will have some top padding,
|
||||||
|
/// while zoomed left/right panels will have padding to the right/left (respectively).
|
||||||
|
///
|
||||||
|
/// Default: true
|
||||||
|
pub zoomed_padding: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
@ -4434,7 +4434,6 @@ mod tests {
|
||||||
assert_eq!(actions_without_namespace, Vec::<&str>::new());
|
assert_eq!(actions_without_namespace, Vec::<&str>::new());
|
||||||
|
|
||||||
let expected_namespaces = vec![
|
let expected_namespaces = vec![
|
||||||
"acp",
|
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"agent",
|
"agent",
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
|
use db::{
|
||||||
|
query,
|
||||||
|
sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection},
|
||||||
|
sqlez_macros::sql,
|
||||||
|
};
|
||||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||||
|
|
||||||
define_connection! {
|
pub struct ComponentPreviewDb(ThreadSafeConnection);
|
||||||
pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb<WorkspaceDb> =
|
|
||||||
&[sql!(
|
impl Domain for ComponentPreviewDb {
|
||||||
|
const NAME: &str = stringify!(ComponentPreviewDb);
|
||||||
|
|
||||||
|
const MIGRATIONS: &[&str] = &[sql!(
|
||||||
CREATE TABLE component_previews (
|
CREATE TABLE component_previews (
|
||||||
workspace_id INTEGER,
|
workspace_id INTEGER,
|
||||||
item_id INTEGER UNIQUE,
|
item_id INTEGER UNIQUE,
|
||||||
|
@ -13,9 +20,11 @@ define_connection! {
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
)];
|
)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db::static_connection!(COMPONENT_PREVIEW_DB, ComponentPreviewDb, [WorkspaceDb]);
|
||||||
|
|
||||||
impl ComponentPreviewDb {
|
impl ComponentPreviewDb {
|
||||||
pub async fn save_active_page(
|
pub async fn save_active_page(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -292,7 +292,9 @@ pub mod agent {
|
||||||
Chat,
|
Chat,
|
||||||
/// Toggles the language model selector dropdown.
|
/// Toggles the language model selector dropdown.
|
||||||
#[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
|
#[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
|
||||||
ToggleModelSelector
|
ToggleModelSelector,
|
||||||
|
/// Triggers re-authentication on Gemini
|
||||||
|
ReauthenticateAgent
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue