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