Compare commits
2 Commits
lw/focusab
...
on-app-qui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2da9108cd | ||
|
|
155e29f5d9 |
10
.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml
vendored
10
.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml
vendored
@@ -1,15 +1,15 @@
|
||||
name: Bug Report (Windows Alpha)
|
||||
description: Zed Windows Alpha Related Bugs
|
||||
name: Bug Report (Windows)
|
||||
description: Zed Windows-Related Bugs
|
||||
type: "Bug"
|
||||
labels: ["windows"]
|
||||
title: "Windows Alpha: <a short description of the Windows bug>"
|
||||
title: "Windows: <a short description of the Windows bug>"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Describe the bug with a one-line summary, and provide detailed reproduction steps
|
||||
description: Describe the bug with a one line summary, and provide detailed reproduction steps
|
||||
value: |
|
||||
<!-- Please insert a one-line summary of the issue below -->
|
||||
<!-- Please insert a one line summary of the issue below -->
|
||||
SUMMARY_SENTENCE_HERE
|
||||
|
||||
### Description
|
||||
|
||||
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -29,7 +29,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"ui",
|
||||
"url",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -197,7 +196,6 @@ dependencies = [
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"context_server",
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
@@ -231,7 +229,6 @@ dependencies = [
|
||||
"task",
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"text",
|
||||
"theme",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
@@ -11157,7 +11154,6 @@ dependencies = [
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
@@ -11244,7 +11240,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"futures 0.3.31",
|
||||
"http_client",
|
||||
"log",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -18030,7 +18025,6 @@ dependencies = [
|
||||
"command_palette_hooks",
|
||||
"db",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"futures 0.3.31",
|
||||
"git_ui",
|
||||
"gpui",
|
||||
@@ -20689,7 +20683,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_emmet"
|
||||
version = "0.0.5"
|
||||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
@@ -20928,7 +20922,6 @@ dependencies = [
|
||||
"menu",
|
||||
"postage",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
|
||||
@@ -714,7 +714,6 @@ features = [
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Ole",
|
||||
"Win32_System_Performance",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
|
||||
@@ -333,14 +333,10 @@
|
||||
"ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific
|
||||
"ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
|
||||
"ctrl-x ctrl-z": "editor::Cancel",
|
||||
"ctrl-x ctrl-e": "vim::LineDown",
|
||||
"ctrl-x ctrl-y": "vim::LineUp",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-t": "vim::Indent",
|
||||
"ctrl-d": "vim::Outdent",
|
||||
"ctrl-y": "vim::InsertFromAbove",
|
||||
"ctrl-e": "vim::InsertFromBelow",
|
||||
"ctrl-k": ["vim::PushDigraph", {}],
|
||||
"ctrl-v": ["vim::PushLiteral", {}],
|
||||
"ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.
|
||||
|
||||
@@ -34,7 +34,6 @@ settings.workspace = true
|
||||
smol.workspace = true
|
||||
terminal.workspace = true
|
||||
ui.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
mod connection;
|
||||
mod diff;
|
||||
mod mention;
|
||||
mod terminal;
|
||||
|
||||
pub use connection::*;
|
||||
pub use diff::*;
|
||||
pub use mention::*;
|
||||
pub use terminal::*;
|
||||
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp};
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result};
|
||||
use editor::Bias;
|
||||
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
|
||||
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
|
||||
use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task};
|
||||
use itertools::Itertools;
|
||||
use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff};
|
||||
use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, text_diff};
|
||||
use markdown::Markdown;
|
||||
use project::{AgentLocation, Project};
|
||||
use std::collections::HashMap;
|
||||
@@ -23,7 +21,12 @@ use std::error::Error;
|
||||
use std::fmt::Formatter;
|
||||
use std::process::ExitStatus;
|
||||
use std::rc::Rc;
|
||||
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
mem,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::App;
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -50,6 +53,38 @@ impl UserMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MentionPath<'a>(&'a Path);
|
||||
|
||||
impl<'a> MentionPath<'a> {
|
||||
const PREFIX: &'static str = "@file:";
|
||||
|
||||
pub fn new(path: &'a Path) -> Self {
|
||||
MentionPath(path)
|
||||
}
|
||||
|
||||
pub fn try_parse(url: &'a str) -> Option<Self> {
|
||||
let path = url.strip_prefix(Self::PREFIX)?;
|
||||
Some(MentionPath(Path::new(path)))
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MentionPath<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"[@{}]({}{})",
|
||||
self.0.file_name().unwrap_or_default().display(),
|
||||
Self::PREFIX,
|
||||
self.0.display()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct AssistantMessage {
|
||||
pub chunks: Vec<AssistantMessageChunk>,
|
||||
@@ -122,17 +157,9 @@ impl AgentThreadEntry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn location(&self, ix: usize) -> Option<(acp::ToolCallLocation, AgentLocation)> {
|
||||
if let AgentThreadEntry::ToolCall(ToolCall {
|
||||
locations,
|
||||
resolved_locations,
|
||||
..
|
||||
}) = self
|
||||
{
|
||||
Some((
|
||||
locations.get(ix)?.clone(),
|
||||
resolved_locations.get(ix)?.clone()?,
|
||||
))
|
||||
pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> {
|
||||
if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self {
|
||||
Some(locations)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -147,7 +174,6 @@ pub struct ToolCall {
|
||||
pub content: Vec<ToolCallContent>,
|
||||
pub status: ToolCallStatus,
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
pub resolved_locations: Vec<Option<AgentLocation>>,
|
||||
pub raw_input: Option<serde_json::Value>,
|
||||
pub raw_output: Option<serde_json::Value>,
|
||||
}
|
||||
@@ -176,7 +202,6 @@ impl ToolCall {
|
||||
.map(|content| ToolCallContent::from_acp(content, language_registry.clone(), cx))
|
||||
.collect(),
|
||||
locations: tool_call.locations,
|
||||
resolved_locations: Vec::default(),
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
raw_output: tool_call.raw_output,
|
||||
@@ -229,15 +254,6 @@ impl ToolCall {
|
||||
}
|
||||
|
||||
if let Some(raw_output) = raw_output {
|
||||
if self.content.is_empty() {
|
||||
if let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx)
|
||||
{
|
||||
self.content
|
||||
.push(ToolCallContent::ContentBlock(ContentBlock::Markdown {
|
||||
markdown,
|
||||
}));
|
||||
}
|
||||
}
|
||||
self.raw_output = Some(raw_output);
|
||||
}
|
||||
}
|
||||
@@ -270,57 +286,6 @@ impl ToolCall {
|
||||
}
|
||||
markdown
|
||||
}
|
||||
|
||||
async fn resolve_location(
|
||||
location: acp::ToolCallLocation,
|
||||
project: WeakEntity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<AgentLocation> {
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
if let Some(path) = project.project_path_for_absolute_path(&location.path, cx) {
|
||||
Some(project.open_buffer(path, cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok()??;
|
||||
let buffer = buffer.await.log_err()?;
|
||||
let position = buffer
|
||||
.update(cx, |buffer, _| {
|
||||
if let Some(row) = location.line {
|
||||
let snapshot = buffer.snapshot();
|
||||
let column = snapshot.indent_size_for_line(row).len;
|
||||
let point = snapshot.clip_point(Point::new(row, column), Bias::Left);
|
||||
snapshot.anchor_before(point)
|
||||
} else {
|
||||
Anchor::MIN
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_locations(
|
||||
&self,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<Option<AgentLocation>>> {
|
||||
let locations = self.locations.clone();
|
||||
project.update(cx, |_, cx| {
|
||||
cx.spawn(async move |project, cx| {
|
||||
let mut new_locations = Vec::new();
|
||||
for location in locations {
|
||||
new_locations.push(Self::resolve_location(location, project.clone(), cx).await);
|
||||
}
|
||||
new_locations
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -360,7 +325,6 @@ impl Display for ToolCallStatus {
|
||||
pub enum ContentBlock {
|
||||
Empty,
|
||||
Markdown { markdown: Entity<Markdown> },
|
||||
ResourceLink { resource_link: acp::ResourceLink },
|
||||
}
|
||||
|
||||
impl ContentBlock {
|
||||
@@ -392,67 +356,36 @@ impl ContentBlock {
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
if matches!(self, ContentBlock::Empty) {
|
||||
if let acp::ContentBlock::ResourceLink(resource_link) = block {
|
||||
*self = ContentBlock::ResourceLink { resource_link };
|
||||
return;
|
||||
let new_content = match block {
|
||||
acp::ContentBlock::Text(text_content) => text_content.text.clone(),
|
||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
||||
if let Some(path) = resource_link.uri.strip_prefix("file://") {
|
||||
format!("{}", MentionPath(path.as_ref()))
|
||||
} else {
|
||||
resource_link.uri.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let new_content = self.extract_content_from_block(block);
|
||||
acp::ContentBlock::Image(_)
|
||||
| acp::ContentBlock::Audio(_)
|
||||
| acp::ContentBlock::Resource(_) => String::new(),
|
||||
};
|
||||
|
||||
match self {
|
||||
ContentBlock::Empty => {
|
||||
*self = Self::create_markdown_block(new_content, language_registry, cx);
|
||||
*self = ContentBlock::Markdown {
|
||||
markdown: cx.new(|cx| {
|
||||
Markdown::new(
|
||||
new_content.into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
};
|
||||
}
|
||||
ContentBlock::Markdown { markdown } => {
|
||||
markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx));
|
||||
}
|
||||
ContentBlock::ResourceLink { resource_link } => {
|
||||
let existing_content = Self::resource_link_to_content(&resource_link.uri);
|
||||
let combined = format!("{}\n{}", existing_content, new_content);
|
||||
|
||||
*self = Self::create_markdown_block(combined, language_registry, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_link_to_content(uri: &str) -> String {
|
||||
if let Some(uri) = MentionUri::parse(&uri).log_err() {
|
||||
uri.to_link()
|
||||
} else {
|
||||
uri.to_string().clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_markdown_block(
|
||||
content: String,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> ContentBlock {
|
||||
ContentBlock::Markdown {
|
||||
markdown: cx
|
||||
.new(|cx| Markdown::new(content.into(), Some(language_registry.clone()), None, cx)),
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_content_from_block(&self, block: acp::ContentBlock) -> String {
|
||||
match block {
|
||||
acp::ContentBlock::Text(text_content) => text_content.text.clone(),
|
||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
||||
Self::resource_link_to_content(&resource_link.uri)
|
||||
}
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||
resource:
|
||||
acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents {
|
||||
uri,
|
||||
..
|
||||
}),
|
||||
..
|
||||
}) => Self::resource_link_to_content(&uri),
|
||||
acp::ContentBlock::Image(_)
|
||||
| acp::ContentBlock::Audio(_)
|
||||
| acp::ContentBlock::Resource(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,7 +393,6 @@ impl ContentBlock {
|
||||
match self {
|
||||
ContentBlock::Empty => "",
|
||||
ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
|
||||
ContentBlock::ResourceLink { resource_link } => &resource_link.uri,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,14 +400,6 @@ impl ContentBlock {
|
||||
match self {
|
||||
ContentBlock::Empty => None,
|
||||
ContentBlock::Markdown { markdown } => Some(markdown),
|
||||
ContentBlock::ResourceLink { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resource_link(&self) -> Option<&acp::ResourceLink> {
|
||||
match self {
|
||||
ContentBlock::ResourceLink { resource_link } => Some(resource_link),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -865,11 +789,7 @@ impl AcpThread {
|
||||
.context("Tool call not found")?;
|
||||
match update {
|
||||
ToolCallUpdate::UpdateFields(update) => {
|
||||
let location_updated = update.fields.locations.is_some();
|
||||
current_call.update_fields(update.fields, languages, cx);
|
||||
if location_updated {
|
||||
self.resolve_locations(update.id.clone(), cx);
|
||||
}
|
||||
}
|
||||
ToolCallUpdate::UpdateDiff(update) => {
|
||||
current_call.content.clear();
|
||||
@@ -906,7 +826,8 @@ impl AcpThread {
|
||||
) {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let call = ToolCall::from_acp(tool_call, status, language_registry, cx);
|
||||
let id = call.id.clone();
|
||||
|
||||
let location = call.locations.last().cloned();
|
||||
|
||||
if let Some((ix, current_call)) = self.tool_call_mut(&call.id) {
|
||||
*current_call = call;
|
||||
@@ -914,9 +835,11 @@ impl AcpThread {
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
} else {
|
||||
self.push_entry(AgentThreadEntry::ToolCall(call), cx);
|
||||
};
|
||||
}
|
||||
|
||||
self.resolve_locations(id, cx);
|
||||
if let Some(location) = location {
|
||||
self.set_project_location(location, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> {
|
||||
@@ -937,50 +860,35 @@ impl AcpThread {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resolve_locations(&mut self, id: acp::ToolCallId, cx: &mut Context<Self>) {
|
||||
let project = self.project.clone();
|
||||
let Some((_, tool_call)) = self.tool_call_mut(&id) else {
|
||||
return;
|
||||
};
|
||||
let task = tool_call.resolve_locations(project, cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let resolved_locations = task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
let project = this.project.clone();
|
||||
let Some((ix, tool_call)) = this.tool_call_mut(&id) else {
|
||||
return;
|
||||
};
|
||||
if let Some(Some(location)) = resolved_locations.last() {
|
||||
project.update(cx, |project, cx| {
|
||||
if let Some(agent_location) = project.agent_location() {
|
||||
let should_ignore = agent_location.buffer == location.buffer
|
||||
&& location
|
||||
.buffer
|
||||
.update(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
let old_position =
|
||||
agent_location.position.to_point(&snapshot);
|
||||
let new_position = location.position.to_point(&snapshot);
|
||||
// ignore this so that when we get updates from the edit tool
|
||||
// the position doesn't reset to the startof line
|
||||
old_position.row == new_position.row
|
||||
&& old_position.column > new_position.column
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
if !should_ignore {
|
||||
project.set_agent_location(Some(location.clone()), cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if tool_call.resolved_locations != resolved_locations {
|
||||
tool_call.resolved_locations = resolved_locations;
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
}
|
||||
pub fn set_project_location(&self, location: acp::ToolCallLocation, cx: &mut Context<Self>) {
|
||||
self.project.update(cx, |project, cx| {
|
||||
let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
|
||||
return;
|
||||
};
|
||||
let buffer = project.open_buffer(path, cx);
|
||||
cx.spawn(async move |project, cx| {
|
||||
let buffer = buffer.await?;
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let position = if let Some(line) = location.line {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let point = snapshot.clip_point(Point::new(line, 0), Bias::Left);
|
||||
snapshot.anchor_before(point)
|
||||
} else {
|
||||
Anchor::MIN
|
||||
};
|
||||
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn request_tool_call_authorization(
|
||||
@@ -1149,11 +1057,8 @@ impl AcpThread {
|
||||
|
||||
cx.spawn(async move |this, cx| match rx.await {
|
||||
Ok(Err(e)) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.send_task.take();
|
||||
cx.emit(AcpThreadEvent::Error)
|
||||
})
|
||||
.log_err();
|
||||
this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Error))
|
||||
.log_err();
|
||||
Err(e)?
|
||||
}
|
||||
result => {
|
||||
@@ -1361,48 +1266,6 @@ impl AcpThread {
|
||||
}
|
||||
}
|
||||
|
||||
fn markdown_for_raw_output(
|
||||
raw_output: &serde_json::Value,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<Markdown>> {
|
||||
match raw_output {
|
||||
serde_json::Value::Null => None,
|
||||
serde_json::Value::Bool(value) => Some(cx.new(|cx| {
|
||||
Markdown::new(
|
||||
value.to_string().into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})),
|
||||
serde_json::Value::Number(value) => Some(cx.new(|cx| {
|
||||
Markdown::new(
|
||||
value.to_string().into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})),
|
||||
serde_json::Value::String(value) => Some(cx.new(|cx| {
|
||||
Markdown::new(
|
||||
value.clone().into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})),
|
||||
value => Some(cx.new(|cx| {
|
||||
Markdown::new(
|
||||
format!("```json\n{}\n```", value).into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1415,7 +1278,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt as _;
|
||||
use std::{cell::RefCell, path::Path, rc::Rc, time::Duration};
|
||||
use std::{cell::RefCell, rc::Rc, time::Duration};
|
||||
|
||||
use util::path;
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Result, bail};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum MentionUri {
|
||||
File(PathBuf),
|
||||
Symbol(PathBuf, String),
|
||||
Thread(acp::SessionId),
|
||||
Rule(String),
|
||||
}
|
||||
|
||||
impl MentionUri {
|
||||
pub fn parse(input: &str) -> Result<Self> {
|
||||
let url = url::Url::parse(input)?;
|
||||
let path = url.path();
|
||||
match url.scheme() {
|
||||
"file" => {
|
||||
if let Some(fragment) = url.fragment() {
|
||||
Ok(Self::Symbol(path.into(), fragment.into()))
|
||||
} else {
|
||||
let file_path =
|
||||
PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
|
||||
|
||||
Ok(Self::File(file_path))
|
||||
}
|
||||
}
|
||||
"zed" => {
|
||||
if let Some(thread) = path.strip_prefix("/agent/thread/") {
|
||||
Ok(Self::Thread(acp::SessionId(thread.into())))
|
||||
} else if let Some(rule) = path.strip_prefix("/agent/rule/") {
|
||||
Ok(Self::Rule(rule.into()))
|
||||
} else {
|
||||
bail!("invalid zed url: {:?}", input);
|
||||
}
|
||||
}
|
||||
other => bail!("unrecognized scheme {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
MentionUri::File(path) => path.file_name().unwrap().to_string_lossy().into_owned(),
|
||||
MentionUri::Symbol(_path, name) => name.clone(),
|
||||
MentionUri::Thread(thread) => thread.to_string(),
|
||||
MentionUri::Rule(rule) => rule.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_link(&self) -> String {
|
||||
let name = self.name();
|
||||
let uri = self.to_uri();
|
||||
format!("[{name}]({uri})")
|
||||
}
|
||||
|
||||
pub fn to_uri(&self) -> String {
|
||||
match self {
|
||||
MentionUri::File(path) => {
|
||||
format!("file://{}", path.display())
|
||||
}
|
||||
MentionUri::Symbol(path, name) => {
|
||||
format!("file://{}#{}", path.display(), name)
|
||||
}
|
||||
MentionUri::Thread(thread) => {
|
||||
format!("zed:///agent/thread/{}", thread.0)
|
||||
}
|
||||
MentionUri::Rule(rule) => {
|
||||
format!("zed:///agent/rule/{}", rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mention_uri_parse_and_display() {
|
||||
// Test file URI
|
||||
let file_uri = "file:///path/to/file.rs";
|
||||
let parsed = MentionUri::parse(file_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::File(path) => assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"),
|
||||
_ => panic!("Expected File variant"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri(), file_uri);
|
||||
|
||||
// Test symbol URI
|
||||
let symbol_uri = "file:///path/to/file.rs#MySymbol";
|
||||
let parsed = MentionUri::parse(symbol_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Symbol(path, symbol) => {
|
||||
assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
|
||||
assert_eq!(symbol, "MySymbol");
|
||||
}
|
||||
_ => panic!("Expected Symbol variant"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri(), symbol_uri);
|
||||
|
||||
// Test thread URI
|
||||
let thread_uri = "zed:///agent/thread/session123";
|
||||
let parsed = MentionUri::parse(thread_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Thread(session_id) => assert_eq!(session_id.0.as_ref(), "session123"),
|
||||
_ => panic!("Expected Thread variant"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri(), thread_uri);
|
||||
|
||||
// Test rule URI
|
||||
let rule_uri = "zed:///agent/rule/my_rule";
|
||||
let parsed = MentionUri::parse(rule_uri).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Rule(rule) => assert_eq!(rule, "my_rule"),
|
||||
_ => panic!("Expected Rule variant"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri(), rule_uri);
|
||||
|
||||
// Test invalid scheme
|
||||
assert!(MentionUri::parse("http://example.com").is_err());
|
||||
|
||||
// Test invalid zed path
|
||||
assert!(MentionUri::parse("zed:///invalid/path").is_err());
|
||||
}
|
||||
}
|
||||
@@ -716,10 +716,18 @@ impl ActivityIndicator {
|
||||
})),
|
||||
tooltip_message: Some(Self::version_tooltip_message(&version)),
|
||||
}),
|
||||
AutoUpdateStatus::Updated { version } => Some(Content {
|
||||
AutoUpdateStatus::Updated {
|
||||
binary_path,
|
||||
version,
|
||||
} => Some(Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
|
||||
on_click: Some(Arc::new({
|
||||
let reload = workspace::Reload {
|
||||
binary_path: Some(binary_path.clone()),
|
||||
};
|
||||
move |_, _, cx| workspace::reload(&reload, cx)
|
||||
})),
|
||||
tooltip_message: Some(Self::version_tooltip_message(&version)),
|
||||
}),
|
||||
AutoUpdateStatus::Errored => Some(Content {
|
||||
|
||||
@@ -2268,15 +2268,6 @@ impl Thread {
|
||||
max_attempts: 3,
|
||||
})
|
||||
}
|
||||
Other(err)
|
||||
if err.is::<PaymentRequiredError>()
|
||||
|| err.is::<ModelRequestLimitReachedError>() =>
|
||||
{
|
||||
// Retrying won't help for Payment Required or Model Request Limit errors (where
|
||||
// the user must upgrade to usage-based billing to get more requests, or else wait
|
||||
// for a significant amount of time for the request limit to reset).
|
||||
None
|
||||
}
|
||||
// Conservatively assume that any other errors are non-retryable
|
||||
HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
|
||||
@@ -23,7 +23,6 @@ assistant_tools.workspace = true
|
||||
chrono.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
@@ -49,7 +48,6 @@ settings.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
terminal.workspace = true
|
||||
text.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
@@ -62,7 +60,6 @@ workspace-hack.workspace = true
|
||||
ctor.workspace = true
|
||||
client = { workspace = true, "features" = ["test-support"] }
|
||||
clock = { workspace = true, "features" = ["test-support"] }
|
||||
context_server = { workspace = true, "features" = ["test-support"] }
|
||||
editor = { workspace = true, "features" = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
fs = { workspace = true, "features" = ["test-support"] }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{AgentResponseEvent, Thread, templates::Templates};
|
||||
use crate::{
|
||||
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool,
|
||||
FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MessageContent, MovePathTool, NowTool,
|
||||
OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool,
|
||||
CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool,
|
||||
GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool,
|
||||
ThinkingTool, ToolCallAuthorization, WebSearchTool,
|
||||
};
|
||||
use acp_thread::ModelSelector;
|
||||
use agent_client_protocol as acp;
|
||||
@@ -55,7 +55,6 @@ pub struct NativeAgent {
|
||||
project_context: Rc<RefCell<ProjectContext>>,
|
||||
project_context_needs_refresh: watch::Sender<()>,
|
||||
_maintain_project_context: Task<Result<()>>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
/// Shared templates for all threads
|
||||
templates: Arc<Templates>,
|
||||
project: Entity<Project>,
|
||||
@@ -91,9 +90,6 @@ impl NativeAgent {
|
||||
_maintain_project_context: cx.spawn(async move |this, cx| {
|
||||
Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await
|
||||
}),
|
||||
context_server_registry: cx.new(|cx| {
|
||||
ContextServerRegistry::new(project.read(cx).context_server_store(), cx)
|
||||
}),
|
||||
templates,
|
||||
project,
|
||||
prompt_store,
|
||||
@@ -389,13 +385,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
// Create AcpThread
|
||||
let acp_thread = cx.update(|cx| {
|
||||
cx.new(|cx| {
|
||||
acp_thread::AcpThread::new(
|
||||
"agent2",
|
||||
self.clone(),
|
||||
project.clone(),
|
||||
session_id.clone(),
|
||||
cx,
|
||||
)
|
||||
acp_thread::AcpThread::new("agent2", self.clone(), project.clone(), session_id.clone(), cx)
|
||||
})
|
||||
})?;
|
||||
let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?;
|
||||
@@ -423,21 +413,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
log::warn!("No default model configured in settings");
|
||||
anyhow!(
|
||||
"No default model. Please configure a default model in settings."
|
||||
)
|
||||
anyhow!("No default model configured. Please configure a default model in settings.")
|
||||
})?;
|
||||
|
||||
let thread = cx.new(|cx| {
|
||||
let mut thread = Thread::new(
|
||||
project.clone(),
|
||||
agent.project_context.clone(),
|
||||
agent.context_server_registry.clone(),
|
||||
action_log.clone(),
|
||||
agent.templates.clone(),
|
||||
default_model,
|
||||
cx,
|
||||
);
|
||||
let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model);
|
||||
thread.add_tool(CreateDirectoryTool::new(project.clone()));
|
||||
thread.add_tool(CopyPathTool::new(project.clone()));
|
||||
thread.add_tool(DiagnosticsTool::new(project.clone()));
|
||||
@@ -470,7 +450,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
acp_thread: acp_thread.downgrade(),
|
||||
_subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
|
||||
this.sessions.remove(acp_thread.session_id());
|
||||
}),
|
||||
})
|
||||
},
|
||||
);
|
||||
})?;
|
||||
@@ -516,13 +496,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
})?;
|
||||
log::debug!("Found session for: {}", session_id);
|
||||
|
||||
let message: Vec<MessageContent> = params
|
||||
.prompt
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>();
|
||||
// Convert prompt to message
|
||||
let message = convert_prompt_to_message(params.prompt);
|
||||
log::info!("Converted prompt to message: {} chars", message.len());
|
||||
log::debug!("Message content: {:?}", message);
|
||||
log::debug!("Message content: {}", message);
|
||||
|
||||
// Get model using the ModelSelector capability (always available for agent2)
|
||||
// Get the selected model from the thread directly
|
||||
@@ -626,6 +603,39 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert ACP content blocks to a message string
|
||||
fn convert_prompt_to_message(blocks: Vec<acp::ContentBlock>) -> String {
|
||||
log::debug!("Converting {} content blocks to message", blocks.len());
|
||||
let mut message = String::new();
|
||||
|
||||
for block in blocks {
|
||||
match block {
|
||||
acp::ContentBlock::Text(text) => {
|
||||
log::trace!("Processing text block: {} chars", text.text.len());
|
||||
message.push_str(&text.text);
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(link) => {
|
||||
log::trace!("Processing resource link: {}", link.uri);
|
||||
message.push_str(&format!(" @{} ", link.uri));
|
||||
}
|
||||
acp::ContentBlock::Image(_) => {
|
||||
log::trace!("Processing image block");
|
||||
message.push_str(" [image] ");
|
||||
}
|
||||
acp::ContentBlock::Audio(_) => {
|
||||
log::trace!("Processing audio block");
|
||||
message.push_str(" [audio] ");
|
||||
}
|
||||
acp::ContentBlock::Resource(resource) => {
|
||||
log::trace!("Processing resource block: {:?}", resource.resource);
|
||||
message.push_str(&format!(" [resource: {:?}] ", resource.resource));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use super::*;
|
||||
use crate::MessageContent;
|
||||
use acp_thread::AgentConnection;
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp};
|
||||
use agent_settings::AgentProfileId;
|
||||
use anyhow::Result;
|
||||
use client::{Client, UserStore};
|
||||
use fs::{FakeFs, Fs};
|
||||
@@ -14,8 +12,8 @@ use gpui::{
|
||||
use indoc::indoc;
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
|
||||
LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, Role, StopReason,
|
||||
fake_provider::FakeLanguageModel,
|
||||
LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
|
||||
StopReason, fake_provider::FakeLanguageModel,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::ProjectContext;
|
||||
@@ -167,9 +165,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}),
|
||||
"{}",
|
||||
thread.to_markdown()
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -273,14 +269,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
assert_eq!(
|
||||
message.content,
|
||||
vec![
|
||||
language_model::MessageContent::ToolResult(LanguageModelToolResult {
|
||||
MessageContent::ToolResult(LanguageModelToolResult {
|
||||
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
|
||||
tool_name: ToolRequiringPermission.name().into(),
|
||||
is_error: false,
|
||||
content: "Allowed".into(),
|
||||
output: Some("Allowed".into())
|
||||
}),
|
||||
language_model::MessageContent::ToolResult(LanguageModelToolResult {
|
||||
MessageContent::ToolResult(LanguageModelToolResult {
|
||||
tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
|
||||
tool_name: ToolRequiringPermission.name().into(),
|
||||
is_error: true,
|
||||
@@ -313,15 +309,13 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
let message = completion.messages.last().unwrap();
|
||||
assert_eq!(
|
||||
message.content,
|
||||
vec![language_model::MessageContent::ToolResult(
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
|
||||
tool_name: ToolRequiringPermission.name().into(),
|
||||
is_error: false,
|
||||
content: "Allowed".into(),
|
||||
output: Some("Allowed".into())
|
||||
}
|
||||
)]
|
||||
vec![MessageContent::ToolResult(LanguageModelToolResult {
|
||||
tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
|
||||
tool_name: ToolRequiringPermission.name().into(),
|
||||
is_error: false,
|
||||
content: "Allowed".into(),
|
||||
output: Some("Allowed".into())
|
||||
})]
|
||||
);
|
||||
|
||||
// Simulate a final tool call, ensuring we don't trigger authorization.
|
||||
@@ -340,15 +334,13 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
let message = completion.messages.last().unwrap();
|
||||
assert_eq!(
|
||||
message.content,
|
||||
vec![language_model::MessageContent::ToolResult(
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: "tool_id_4".into(),
|
||||
tool_name: ToolRequiringPermission.name().into(),
|
||||
is_error: false,
|
||||
content: "Allowed".into(),
|
||||
output: Some("Allowed".into())
|
||||
}
|
||||
)]
|
||||
vec![MessageContent::ToolResult(LanguageModelToolResult {
|
||||
tool_use_id: "tool_id_4".into(),
|
||||
tool_name: ToolRequiringPermission.name().into(),
|
||||
is_error: false,
|
||||
content: "Allowed".into(),
|
||||
output: Some("Allowed".into())
|
||||
})]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -477,82 +469,6 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_profiles(cx: &mut TestAppContext) {
|
||||
let ThreadTest {
|
||||
model, thread, fs, ..
|
||||
} = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
thread.update(cx, |thread, _cx| {
|
||||
thread.add_tool(DelayTool);
|
||||
thread.add_tool(EchoTool);
|
||||
thread.add_tool(InfiniteTool);
|
||||
});
|
||||
|
||||
// Override profiles and wait for settings to be loaded.
|
||||
fs.insert_file(
|
||||
paths::settings_file(),
|
||||
json!({
|
||||
"agent": {
|
||||
"profiles": {
|
||||
"test-1": {
|
||||
"name": "Test Profile 1",
|
||||
"tools": {
|
||||
EchoTool.name(): true,
|
||||
DelayTool.name(): true,
|
||||
}
|
||||
},
|
||||
"test-2": {
|
||||
"name": "Test Profile 2",
|
||||
"tools": {
|
||||
InfiniteTool.name(): true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
.into_bytes(),
|
||||
)
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
// Test that test-1 profile (default) has echo and delay tools
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_profile(AgentProfileId("test-1".into()));
|
||||
thread.send("test", cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let mut pending_completions = fake_model.pending_completions();
|
||||
assert_eq!(pending_completions.len(), 1);
|
||||
let completion = pending_completions.pop().unwrap();
|
||||
let tool_names: Vec<String> = completion
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.clone())
|
||||
.collect();
|
||||
assert_eq!(tool_names, vec![DelayTool.name(), EchoTool.name()]);
|
||||
fake_model.end_last_completion_stream();
|
||||
|
||||
// Switch to test-2 profile, and verify that it has only the infinite tool.
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_profile(AgentProfileId("test-2".into()));
|
||||
thread.send("test2", cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let mut pending_completions = fake_model.pending_completions();
|
||||
assert_eq!(pending_completions.len(), 1);
|
||||
let completion = pending_completions.pop().unwrap();
|
||||
let tool_names: Vec<String> = completion
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.clone())
|
||||
.collect();
|
||||
assert_eq!(tool_names, vec![InfiniteTool.name()]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[ignore = "can't run on CI yet"]
|
||||
async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
@@ -679,7 +595,6 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
agent_settings::init(cx);
|
||||
});
|
||||
cx.executor().forbid_parking();
|
||||
|
||||
@@ -875,7 +790,6 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
id: acp::ToolCallId("1".into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
raw_output: Some("Finished thinking.".into()),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
@@ -899,7 +813,6 @@ struct ThreadTest {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
thread: Entity<Thread>,
|
||||
project_context: Rc<RefCell<ProjectContext>>,
|
||||
fs: Arc<FakeFs>,
|
||||
}
|
||||
|
||||
enum TestModel {
|
||||
@@ -922,57 +835,30 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
fs.insert_file(
|
||||
paths::settings_file(),
|
||||
json!({
|
||||
"agent": {
|
||||
"default_profile": "test-profile",
|
||||
"profiles": {
|
||||
"test-profile": {
|
||||
"name": "Test Profile",
|
||||
"tools": {
|
||||
EchoTool.name(): true,
|
||||
DelayTool.name(): true,
|
||||
WordListTool.name(): true,
|
||||
ToolRequiringPermission.name(): true,
|
||||
InfiniteTool.name(): true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
.into_bytes(),
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::init(cx);
|
||||
watch_settings(fs.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
agent_settings::init(cx);
|
||||
gpui_tokio::init(cx);
|
||||
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
|
||||
client::init_settings(cx);
|
||||
let client = Client::production(cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
|
||||
watch_settings(fs.clone(), cx);
|
||||
});
|
||||
|
||||
let templates = Templates::new();
|
||||
|
||||
fs.insert_tree(path!("/test"), json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
|
||||
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
|
||||
|
||||
let model = cx
|
||||
.update(|cx| {
|
||||
gpui_tokio::init(cx);
|
||||
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
|
||||
client::init_settings(cx);
|
||||
let client = Client::production(cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
|
||||
if let TestModel::Fake = model {
|
||||
Task::ready(Arc::new(FakeLanguageModel::default()) as Arc<_>)
|
||||
} else {
|
||||
@@ -995,25 +881,20 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
|
||||
.await;
|
||||
|
||||
let project_context = Rc::new(RefCell::new(ProjectContext::default()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let thread = cx.new(|cx| {
|
||||
let thread = cx.new(|_| {
|
||||
Thread::new(
|
||||
project,
|
||||
project_context.clone(),
|
||||
context_server_registry,
|
||||
action_log,
|
||||
templates,
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
ThreadTest {
|
||||
model,
|
||||
thread,
|
||||
project_context,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates};
|
||||
use acp_thread::MentionUri;
|
||||
use crate::{SystemPromptTemplate, Template, Templates};
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::adapt_schema_to_format;
|
||||
use cloud_llm_client::{CompletionIntent, CompletionMode};
|
||||
@@ -14,10 +13,10 @@ use futures::{
|
||||
};
|
||||
use gpui::{App, Context, Entity, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage,
|
||||
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
|
||||
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason,
|
||||
};
|
||||
use log;
|
||||
use project::Project;
|
||||
@@ -26,8 +25,7 @@ use schemars::{JsonSchema, Schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use smol::stream::StreamExt;
|
||||
use std::fmt::Write;
|
||||
use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc};
|
||||
use std::{cell::RefCell, collections::BTreeMap, fmt::Write, rc::Rc, sync::Arc};
|
||||
use util::{ResultExt, markdown::MarkdownCodeBlock};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -36,23 +34,6 @@ pub struct AgentMessage {
|
||||
pub content: Vec<MessageContent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MessageContent {
|
||||
Text(String),
|
||||
Thinking {
|
||||
text: String,
|
||||
signature: Option<String>,
|
||||
},
|
||||
Mention {
|
||||
uri: MentionUri,
|
||||
content: String,
|
||||
},
|
||||
RedactedThinking(String),
|
||||
Image(LanguageModelImage),
|
||||
ToolUse(LanguageModelToolUse),
|
||||
ToolResult(LanguageModelToolResult),
|
||||
}
|
||||
|
||||
impl AgentMessage {
|
||||
pub fn to_markdown(&self) -> String {
|
||||
let mut markdown = format!("## {}\n", self.role);
|
||||
@@ -112,9 +93,6 @@ impl AgentMessage {
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
MessageContent::Mention { uri, .. } => {
|
||||
write!(markdown, "{}", uri.to_link()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,8 +126,6 @@ pub struct Thread {
|
||||
running_turn: Option<Task<()>>,
|
||||
pending_tool_uses: HashMap<LanguageModelToolUseId, LanguageModelToolUse>,
|
||||
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
profile_id: AgentProfileId,
|
||||
project_context: Rc<RefCell<ProjectContext>>,
|
||||
templates: Arc<Templates>,
|
||||
pub selected_model: Arc<dyn LanguageModel>,
|
||||
@@ -161,21 +137,16 @@ impl Thread {
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
project_context: Rc<RefCell<ProjectContext>>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
action_log: Entity<ActionLog>,
|
||||
templates: Arc<Templates>,
|
||||
default_model: Arc<dyn LanguageModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
||||
Self {
|
||||
messages: Vec::new(),
|
||||
completion_mode: CompletionMode::Normal,
|
||||
running_turn: None,
|
||||
pending_tool_uses: HashMap::default(),
|
||||
tools: BTreeMap::default(),
|
||||
context_server_registry,
|
||||
profile_id,
|
||||
project_context,
|
||||
templates,
|
||||
selected_model: default_model,
|
||||
@@ -208,10 +179,6 @@ impl Thread {
|
||||
self.tools.remove(name).is_some()
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
|
||||
self.profile_id = profile_id;
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self) {
|
||||
self.running_turn.take();
|
||||
|
||||
@@ -236,11 +203,10 @@ impl Thread {
|
||||
/// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn.
|
||||
pub fn send(
|
||||
&mut self,
|
||||
content: impl Into<UserMessage>,
|
||||
content: impl Into<MessageContent>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> mpsc::UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>> {
|
||||
let content = content.into().0;
|
||||
|
||||
let content = content.into();
|
||||
let model = self.selected_model.clone();
|
||||
log::info!("Thread::send called with model: {:?}", model.name());
|
||||
log::debug!("Thread::send content: {:?}", content);
|
||||
@@ -253,7 +219,7 @@ impl Thread {
|
||||
let user_message_ix = self.messages.len();
|
||||
self.messages.push(AgentMessage {
|
||||
role: Role::User,
|
||||
content,
|
||||
content: vec![content],
|
||||
});
|
||||
log::info!("Total messages in thread: {}", self.messages.len());
|
||||
self.running_turn = Some(cx.spawn(async move |thread, cx| {
|
||||
@@ -332,7 +298,6 @@ impl Thread {
|
||||
} else {
|
||||
acp::ToolCallStatus::Completed
|
||||
}),
|
||||
raw_output: tool_result.output.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -376,7 +341,7 @@ impl Thread {
|
||||
log::debug!("System message built");
|
||||
AgentMessage {
|
||||
role: Role::System,
|
||||
content: vec![prompt.as_str().into()],
|
||||
content: vec![prompt.into()],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,23 +604,21 @@ impl Thread {
|
||||
let messages = self.build_request_messages();
|
||||
log::info!("Request will include {} messages", messages.len());
|
||||
|
||||
let tools = if let Some(tools) = self.tools(cx).log_err() {
|
||||
tools
|
||||
.filter_map(|tool| {
|
||||
let tool_name = tool.name().to_string();
|
||||
log::trace!("Including tool: {}", tool_name);
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool_name,
|
||||
description: tool.description().to_string(),
|
||||
input_schema: tool
|
||||
.input_schema(self.selected_model.tool_input_format())
|
||||
.log_err()?,
|
||||
})
|
||||
let tools: Vec<LanguageModelRequestTool> = self
|
||||
.tools
|
||||
.values()
|
||||
.filter_map(|tool| {
|
||||
let tool_name = tool.name().to_string();
|
||||
log::trace!("Including tool: {}", tool_name);
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool_name,
|
||||
description: tool.description(cx).to_string(),
|
||||
input_schema: tool
|
||||
.input_schema(self.selected_model.tool_input_format())
|
||||
.log_err()?,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
|
||||
log::info!("Request includes {} tools", tools.len());
|
||||
|
||||
@@ -676,35 +639,6 @@ impl Thread {
|
||||
request
|
||||
}
|
||||
|
||||
fn tools<'a>(&'a self, cx: &'a App) -> Result<impl Iterator<Item = &'a Arc<dyn AnyAgentTool>>> {
|
||||
let profile = AgentSettings::get_global(cx)
|
||||
.profiles
|
||||
.get(&self.profile_id)
|
||||
.context("profile not found")?;
|
||||
|
||||
Ok(self
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool_name, tool)| {
|
||||
if profile.is_tool_enabled(tool_name) {
|
||||
Some(tool)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.chain(self.context_server_registry.read(cx).servers().flat_map(
|
||||
|(server_id, tools)| {
|
||||
tools.iter().filter_map(|(tool_name, tool)| {
|
||||
if profile.is_context_server_tool_enabled(&server_id.0, tool_name) {
|
||||
Some(tool)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
fn build_request_messages(&self) -> Vec<LanguageModelRequestMessage> {
|
||||
log::trace!(
|
||||
"Building request messages from {} thread messages",
|
||||
@@ -724,7 +658,11 @@ impl Thread {
|
||||
},
|
||||
message.content.len()
|
||||
);
|
||||
message.to_request()
|
||||
LanguageModelRequestMessage {
|
||||
role: message.role,
|
||||
content: message.content.clone(),
|
||||
cache: false,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
messages
|
||||
@@ -739,20 +677,6 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UserMessage(Vec<MessageContent>);
|
||||
|
||||
impl From<Vec<MessageContent>> for UserMessage {
|
||||
fn from(content: Vec<MessageContent>) -> Self {
|
||||
UserMessage(content)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<MessageContent>> From<T> for UserMessage {
|
||||
fn from(content: T) -> Self {
|
||||
UserMessage(vec![content.into()])
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AgentTool
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
@@ -762,7 +686,7 @@ where
|
||||
|
||||
fn name(&self) -> SharedString;
|
||||
|
||||
fn description(&self) -> SharedString {
|
||||
fn description(&self, _cx: &mut App) -> SharedString {
|
||||
let schema = schemars::schema_for!(Self::Input);
|
||||
SharedString::new(
|
||||
schema
|
||||
@@ -798,13 +722,13 @@ where
|
||||
pub struct Erased<T>(T);
|
||||
|
||||
pub struct AgentToolOutput {
|
||||
pub llm_output: LanguageModelToolResultContent,
|
||||
pub raw_output: serde_json::Value,
|
||||
llm_output: LanguageModelToolResultContent,
|
||||
raw_output: serde_json::Value,
|
||||
}
|
||||
|
||||
pub trait AnyAgentTool {
|
||||
fn name(&self) -> SharedString;
|
||||
fn description(&self) -> SharedString;
|
||||
fn description(&self, cx: &mut App) -> SharedString;
|
||||
fn kind(&self) -> acp::ToolKind;
|
||||
fn initial_title(&self, input: serde_json::Value) -> SharedString;
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
|
||||
@@ -824,8 +748,8 @@ where
|
||||
self.0.name()
|
||||
}
|
||||
|
||||
fn description(&self) -> SharedString {
|
||||
self.0.description()
|
||||
fn description(&self, cx: &mut App) -> SharedString {
|
||||
self.0.description(cx)
|
||||
}
|
||||
|
||||
fn kind(&self) -> agent_client_protocol::ToolKind {
|
||||
@@ -1135,207 +1059,3 @@ impl std::ops::DerefMut for ToolCallEventStreamReceiver {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentMessage {
|
||||
fn to_request(&self) -> language_model::LanguageModelRequestMessage {
|
||||
let mut message = LanguageModelRequestMessage {
|
||||
role: self.role,
|
||||
content: Vec::with_capacity(self.content.len()),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
const OPEN_CONTEXT: &str = "<context>\n\
|
||||
The following items were attached by the user. \
|
||||
They are up-to-date and don't need to be re-read.\n\n";
|
||||
|
||||
const OPEN_FILES_TAG: &str = "<files>";
|
||||
const OPEN_SYMBOLS_TAG: &str = "<symbols>";
|
||||
const OPEN_THREADS_TAG: &str = "<threads>";
|
||||
const OPEN_RULES_TAG: &str =
|
||||
"<rules>\nThe user has specified the following rules that should be applied:\n";
|
||||
|
||||
let mut file_context = OPEN_FILES_TAG.to_string();
|
||||
let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
|
||||
let mut thread_context = OPEN_THREADS_TAG.to_string();
|
||||
let mut rules_context = OPEN_RULES_TAG.to_string();
|
||||
|
||||
for chunk in &self.content {
|
||||
let chunk = match chunk {
|
||||
MessageContent::Text(text) => language_model::MessageContent::Text(text.clone()),
|
||||
MessageContent::Thinking { text, signature } => {
|
||||
language_model::MessageContent::Thinking {
|
||||
text: text.clone(),
|
||||
signature: signature.clone(),
|
||||
}
|
||||
}
|
||||
MessageContent::RedactedThinking(value) => {
|
||||
language_model::MessageContent::RedactedThinking(value.clone())
|
||||
}
|
||||
MessageContent::ToolUse(value) => {
|
||||
language_model::MessageContent::ToolUse(value.clone())
|
||||
}
|
||||
MessageContent::ToolResult(value) => {
|
||||
language_model::MessageContent::ToolResult(value.clone())
|
||||
}
|
||||
MessageContent::Image(value) => {
|
||||
language_model::MessageContent::Image(value.clone())
|
||||
}
|
||||
MessageContent::Mention { uri, content } => {
|
||||
match uri {
|
||||
MentionUri::File(path) | MentionUri::Symbol(path, _) => {
|
||||
write!(
|
||||
&mut symbol_context,
|
||||
"\n{}",
|
||||
MarkdownCodeBlock {
|
||||
tag: &codeblock_tag(&path),
|
||||
text: &content.to_string(),
|
||||
}
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
MentionUri::Thread(_session_id) => {
|
||||
write!(&mut thread_context, "\n{}\n", content).ok();
|
||||
}
|
||||
MentionUri::Rule(_user_prompt_id) => {
|
||||
write!(
|
||||
&mut rules_context,
|
||||
"\n{}",
|
||||
MarkdownCodeBlock {
|
||||
tag: "",
|
||||
text: &content
|
||||
}
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
language_model::MessageContent::Text(uri.to_link())
|
||||
}
|
||||
};
|
||||
|
||||
message.content.push(chunk);
|
||||
}
|
||||
|
||||
let len_before_context = message.content.len();
|
||||
|
||||
if file_context.len() > OPEN_FILES_TAG.len() {
|
||||
file_context.push_str("</files>\n");
|
||||
message
|
||||
.content
|
||||
.push(language_model::MessageContent::Text(file_context));
|
||||
}
|
||||
|
||||
if symbol_context.len() > OPEN_SYMBOLS_TAG.len() {
|
||||
symbol_context.push_str("</symbols>\n");
|
||||
message
|
||||
.content
|
||||
.push(language_model::MessageContent::Text(symbol_context));
|
||||
}
|
||||
|
||||
if thread_context.len() > OPEN_THREADS_TAG.len() {
|
||||
thread_context.push_str("</threads>\n");
|
||||
message
|
||||
.content
|
||||
.push(language_model::MessageContent::Text(thread_context));
|
||||
}
|
||||
|
||||
if rules_context.len() > OPEN_RULES_TAG.len() {
|
||||
rules_context.push_str("</user_rules>\n");
|
||||
message
|
||||
.content
|
||||
.push(language_model::MessageContent::Text(rules_context));
|
||||
}
|
||||
|
||||
if message.content.len() > len_before_context {
|
||||
message.content.insert(
|
||||
len_before_context,
|
||||
language_model::MessageContent::Text(OPEN_CONTEXT.into()),
|
||||
);
|
||||
message
|
||||
.content
|
||||
.push(language_model::MessageContent::Text("</context>".into()));
|
||||
}
|
||||
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
fn codeblock_tag(full_path: &Path) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
|
||||
let _ = write!(result, "{} ", extension);
|
||||
}
|
||||
|
||||
let _ = write!(result, "{}", full_path.display());
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
impl From<acp::ContentBlock> for MessageContent {
|
||||
fn from(value: acp::ContentBlock) -> Self {
|
||||
match value {
|
||||
acp::ContentBlock::Text(text_content) => MessageContent::Text(text_content.text),
|
||||
acp::ContentBlock::Image(image_content) => {
|
||||
MessageContent::Image(convert_image(image_content))
|
||||
}
|
||||
acp::ContentBlock::Audio(_) => {
|
||||
// TODO
|
||||
MessageContent::Text("[audio]".to_string())
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
||||
match MentionUri::parse(&resource_link.uri) {
|
||||
Ok(uri) => Self::Mention {
|
||||
uri,
|
||||
content: String::new(),
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("Failed to parse mention link: {}", err);
|
||||
MessageContent::Text(format!(
|
||||
"[{}]({})",
|
||||
resource_link.name, resource_link.uri
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
acp::ContentBlock::Resource(resource) => match resource.resource {
|
||||
acp::EmbeddedResourceResource::TextResourceContents(resource) => {
|
||||
match MentionUri::parse(&resource.uri) {
|
||||
Ok(uri) => Self::Mention {
|
||||
uri,
|
||||
content: resource.text,
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("Failed to parse mention link: {}", err);
|
||||
MessageContent::Text(
|
||||
MarkdownCodeBlock {
|
||||
tag: &resource.uri,
|
||||
text: &resource.text,
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
acp::EmbeddedResourceResource::BlobResourceContents(_) => {
|
||||
// TODO
|
||||
MessageContent::Text("[blob]".to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage {
|
||||
LanguageModelImage {
|
||||
source: image_content.data.into(),
|
||||
// TODO: make this optional?
|
||||
size: gpui::Size::new(0.into(), 0.into()),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for MessageContent {
|
||||
fn from(text: &str) -> Self {
|
||||
MessageContent::Text(text.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
mod context_server_registry;
|
||||
mod copy_path_tool;
|
||||
mod create_directory_tool;
|
||||
mod delete_path_tool;
|
||||
@@ -16,7 +15,6 @@ mod terminal_tool;
|
||||
mod thinking_tool;
|
||||
mod web_search_tool;
|
||||
|
||||
pub use context_server_registry::*;
|
||||
pub use copy_path_tool::*;
|
||||
pub use create_directory_tool::*;
|
||||
pub use delete_path_tool::*;
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream};
|
||||
use agent_client_protocol::ToolKind;
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use context_server::ContextServerId;
|
||||
use gpui::{App, Context, Entity, SharedString, Task};
|
||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct ContextServerRegistry {
|
||||
server_store: Entity<ContextServerStore>,
|
||||
registered_servers: HashMap<ContextServerId, RegisteredContextServer>,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
struct RegisteredContextServer {
|
||||
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||
load_tools: Task<Result<()>>,
|
||||
}
|
||||
|
||||
impl ContextServerRegistry {
|
||||
pub fn new(server_store: Entity<ContextServerStore>, cx: &mut Context<Self>) -> Self {
|
||||
let mut this = Self {
|
||||
server_store: server_store.clone(),
|
||||
registered_servers: HashMap::default(),
|
||||
_subscription: cx.subscribe(&server_store, Self::handle_context_server_store_event),
|
||||
};
|
||||
for server in server_store.read(cx).running_servers() {
|
||||
this.reload_tools_for_server(server.id(), cx);
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
pub fn servers(
|
||||
&self,
|
||||
) -> impl Iterator<
|
||||
Item = (
|
||||
&ContextServerId,
|
||||
&BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||
),
|
||||
> {
|
||||
self.registered_servers
|
||||
.iter()
|
||||
.map(|(id, server)| (id, &server.tools))
|
||||
}
|
||||
|
||||
fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
|
||||
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = server.client() else {
|
||||
return;
|
||||
};
|
||||
if !client.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
return;
|
||||
}
|
||||
|
||||
let registered_server =
|
||||
self.registered_servers
|
||||
.entry(server_id.clone())
|
||||
.or_insert(RegisteredContextServer {
|
||||
tools: BTreeMap::default(),
|
||||
load_tools: Task::ready(Ok(())),
|
||||
});
|
||||
registered_server.load_tools = cx.spawn(async move |this, cx| {
|
||||
let response = client
|
||||
.request::<context_server::types::requests::ListTools>(())
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let Some(registered_server) = this.registered_servers.get_mut(&server_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
registered_server.tools.clear();
|
||||
if let Some(response) = response.log_err() {
|
||||
for tool in response.tools {
|
||||
let tool = Arc::new(ContextServerTool::new(
|
||||
this.server_store.clone(),
|
||||
server.id(),
|
||||
tool,
|
||||
));
|
||||
registered_server.tools.insert(tool.name(), tool);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_context_server_store_event(
|
||||
&mut self,
|
||||
_: Entity<ContextServerStore>,
|
||||
event: &project::context_server_store::Event,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
|
||||
match status {
|
||||
ContextServerStatus::Starting => {}
|
||||
ContextServerStatus::Running => {
|
||||
self.reload_tools_for_server(server_id.clone(), cx);
|
||||
}
|
||||
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
||||
self.registered_servers.remove(&server_id);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextServerTool {
|
||||
store: Entity<ContextServerStore>,
|
||||
server_id: ContextServerId,
|
||||
tool: context_server::types::Tool,
|
||||
}
|
||||
|
||||
impl ContextServerTool {
|
||||
fn new(
|
||||
store: Entity<ContextServerStore>,
|
||||
server_id: ContextServerId,
|
||||
tool: context_server::types::Tool,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
server_id,
|
||||
tool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyAgentTool for ContextServerTool {
|
||||
fn name(&self) -> SharedString {
|
||||
self.tool.name.clone().into()
|
||||
}
|
||||
|
||||
fn description(&self) -> SharedString {
|
||||
self.tool.description.clone().unwrap_or_default().into()
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: serde_json::Value) -> SharedString {
|
||||
format!("Run MCP tool `{}`", self.tool.name).into()
|
||||
}
|
||||
|
||||
fn input_schema(
|
||||
&self,
|
||||
format: language_model::LanguageModelToolSchemaFormat,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut schema = self.tool.input_schema.clone();
|
||||
assistant_tool::adapt_schema_to_format(&mut schema, format)?;
|
||||
Ok(match schema {
|
||||
serde_json::Value::Null => {
|
||||
serde_json::json!({ "type": "object", "properties": [] })
|
||||
}
|
||||
serde_json::Value::Object(map) if map.is_empty() => {
|
||||
serde_json::json!({ "type": "object", "properties": [] })
|
||||
}
|
||||
_ => schema,
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<AgentToolOutput>> {
|
||||
let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else {
|
||||
return Task::ready(Err(anyhow!("Context server not found")));
|
||||
};
|
||||
let tool_name = self.tool.name.clone();
|
||||
let server_clone = server.clone();
|
||||
let input_clone = input.clone();
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
let Some(protocol) = server_clone.client() else {
|
||||
bail!("Context server not initialized");
|
||||
};
|
||||
|
||||
let arguments = if let serde_json::Value::Object(map) = input_clone {
|
||||
Some(map.into_iter().collect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
log::trace!(
|
||||
"Running tool: {} with arguments: {:?}",
|
||||
tool_name,
|
||||
arguments
|
||||
);
|
||||
let response = protocol
|
||||
.request::<context_server::types::requests::CallTool>(
|
||||
context_server::types::CallToolParams {
|
||||
name: tool_name,
|
||||
arguments,
|
||||
meta: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut result = String::new();
|
||||
for content in response.content {
|
||||
match content {
|
||||
context_server::types::ToolResponseContent::Text { text } => {
|
||||
result.push_str(&text);
|
||||
}
|
||||
context_server::types::ToolResponseContent::Image { .. } => {
|
||||
log::warn!("Ignoring image content from tool response");
|
||||
}
|
||||
context_server::types::ToolResponseContent::Audio { .. } => {
|
||||
log::warn!("Ignoring audio content from tool response");
|
||||
}
|
||||
context_server::types::ToolResponseContent::Resource { .. } => {
|
||||
log::warn!("Ignoring resource content from tool response");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(AgentToolOutput {
|
||||
raw_output: result.clone().into(),
|
||||
llm_output: result.into(),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ impl AgentTool for DiagnosticsTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
_event_stream: ToolCallEventStream,
|
||||
event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
match input.path {
|
||||
@@ -119,6 +119,11 @@ impl AgentTool for DiagnosticsTool {
|
||||
range.start.row + 1,
|
||||
entry.diagnostic.message
|
||||
)?;
|
||||
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![output.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if output.is_empty() {
|
||||
@@ -153,9 +158,18 @@ impl AgentTool for DiagnosticsTool {
|
||||
}
|
||||
|
||||
if has_diagnostics {
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![output.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
Task::ready(Ok(output))
|
||||
} else {
|
||||
Task::ready(Ok("No errors or warnings found in the project.".into()))
|
||||
let text = "No errors or warnings found in the project.";
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![text.into()]),
|
||||
..Default::default()
|
||||
});
|
||||
Task::ready(Ok(text.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::{AgentTool, Thread, ToolCallEventStream};
|
||||
use acp_thread::Diff;
|
||||
use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashSet;
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task};
|
||||
use indoc::formatdoc;
|
||||
use language::ToPoint;
|
||||
use language::language_settings::{self, FormatOnSave};
|
||||
use language_model::LanguageModelToolResultContent;
|
||||
use paths;
|
||||
@@ -226,16 +225,6 @@ impl AgentTool for EditFileTool {
|
||||
Ok(path) => path,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
let abs_path = project.read(cx).absolute_path(&project_path, cx);
|
||||
if let Some(abs_path) = abs_path.clone() {
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
locations: Some(vec![acp::ToolCallLocation {
|
||||
path: abs_path,
|
||||
line: None,
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
let request = self.thread.update(cx, |thread, cx| {
|
||||
thread.build_completion_request(CompletionIntent::ToolResults, cx)
|
||||
@@ -294,38 +283,13 @@ impl AgentTool for EditFileTool {
|
||||
|
||||
let mut hallucinated_old_text = false;
|
||||
let mut ambiguous_ranges = Vec::new();
|
||||
let mut emitted_location = false;
|
||||
while let Some(event) = events.next().await {
|
||||
match event {
|
||||
EditAgentOutputEvent::Edited(range) => {
|
||||
if !emitted_location {
|
||||
let line = buffer.update(cx, |buffer, _cx| {
|
||||
range.start.to_point(&buffer.snapshot()).row
|
||||
}).ok();
|
||||
if let Some(abs_path) = abs_path.clone() {
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
emitted_location = true;
|
||||
}
|
||||
},
|
||||
EditAgentOutputEvent::Edited => {},
|
||||
EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
|
||||
EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
|
||||
EditAgentOutputEvent::ResolvingEditRange(range) => {
|
||||
diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx))?;
|
||||
// if !emitted_location {
|
||||
// let line = buffer.update(cx, |buffer, _cx| {
|
||||
// range.start.to_point(&buffer.snapshot()).row
|
||||
// }).ok();
|
||||
// if let Some(abs_path) = abs_path.clone() {
|
||||
// event_stream.update_fields(ToolCallUpdateFields {
|
||||
// locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
|
||||
// ..Default::default()
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
diff.update(cx, |card, cx| card.reveal_range(range, cx))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,8 +454,9 @@ fn resolve_path(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Templates;
|
||||
|
||||
use super::*;
|
||||
use crate::{ContextServerRegistry, Templates};
|
||||
use action_log::ActionLog;
|
||||
use client::TelemetrySettings;
|
||||
use fs::Fs;
|
||||
@@ -510,20 +475,9 @@ mod tests {
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log,
|
||||
Templates::new(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let thread =
|
||||
cx.new(|_| Thread::new(project, Rc::default(), action_log, Templates::new(), model));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = EditFileToolInput {
|
||||
@@ -707,18 +661,14 @@ mod tests {
|
||||
});
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
let thread = cx.new(|_| {
|
||||
Thread::new(
|
||||
project,
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -842,19 +792,15 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
let thread = cx.new(|_| {
|
||||
Thread::new(
|
||||
project,
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -968,19 +914,15 @@ mod tests {
|
||||
init_test(cx);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
let thread = cx.new(|_| {
|
||||
Thread::new(
|
||||
project,
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
@@ -1099,19 +1041,15 @@ mod tests {
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/project", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
let thread = cx.new(|_| {
|
||||
Thread::new(
|
||||
project,
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
@@ -1210,18 +1148,14 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
let thread = cx.new(|_| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
Rc::default(),
|
||||
context_server_registry.clone(),
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
@@ -1291,18 +1225,14 @@ mod tests {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
let thread = cx.new(|_| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
Rc::default(),
|
||||
context_server_registry.clone(),
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
@@ -1375,18 +1305,14 @@ mod tests {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
let thread = cx.new(|_| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
Rc::default(),
|
||||
context_server_registry.clone(),
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
@@ -1456,18 +1382,14 @@ mod tests {
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
let thread = cx.new(|_| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
|
||||
@@ -136,7 +136,7 @@ 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 text = cx.background_spawn({
|
||||
@@ -149,6 +149,12 @@ impl AgentTool for FetchTool {
|
||||
if text.trim().is_empty() {
|
||||
bail!("no textual content found");
|
||||
}
|
||||
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![text.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Ok(text)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -139,6 +139,9 @@ impl AgentTool for FindPathTool {
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
raw_output: Some(serde_json::json!({
|
||||
"paths": &matches,
|
||||
})),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ impl AgentTool for GrepTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
_event_stream: ToolCallEventStream,
|
||||
event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
@@ -282,22 +282,33 @@ impl AgentTool for GrepTool {
|
||||
}
|
||||
}
|
||||
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![output.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
matches_found += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if matches_found == 0 {
|
||||
Ok("No matches found".into())
|
||||
let output = if matches_found == 0 {
|
||||
"No matches found".to_string()
|
||||
} else if has_more_matches {
|
||||
Ok(format!(
|
||||
format!(
|
||||
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
|
||||
input.offset + 1,
|
||||
input.offset + matches_found,
|
||||
input.offset + RESULTS_PER_PAGE,
|
||||
))
|
||||
)
|
||||
} else {
|
||||
Ok(format!("Found {matches_found} matches:\n{output}"))
|
||||
}
|
||||
format!("Found {matches_found} matches:\n{output}")
|
||||
};
|
||||
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![output.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Ok(output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,20 @@ impl AgentTool for NowTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
_event_stream: ToolCallEventStream,
|
||||
event_stream: ToolCallEventStream,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let now = match input.timezone {
|
||||
Timezone::Utc => Utc::now().to_rfc3339(),
|
||||
Timezone::Local => Local::now().to_rfc3339(),
|
||||
};
|
||||
Task::ready(Ok(format!("The current datetime is {now}.")))
|
||||
let content = format!("The current datetime is {now}.");
|
||||
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![content.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Task::ready(Ok(content))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, ToolCallUpdateFields};
|
||||
use agent_client_protocol::{self as acp};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::outline;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use indoc::formatdoc;
|
||||
use language::Point;
|
||||
use language::{Anchor, Point};
|
||||
use language_model::{LanguageModelImage, LanguageModelToolResultContent};
|
||||
use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
|
||||
use schemars::JsonSchema;
|
||||
@@ -97,7 +97,7 @@ impl AgentTool for ReadFileTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
event_stream: ToolCallEventStream,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<LanguageModelToolResultContent>> {
|
||||
let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else {
|
||||
@@ -166,9 +166,7 @@ impl AgentTool for ReadFileTool {
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = cx
|
||||
.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
})?
|
||||
.await?;
|
||||
if buffer.read_with(cx, |buffer, _| {
|
||||
@@ -180,10 +178,19 @@ impl AgentTool for ReadFileTool {
|
||||
anyhow::bail!("{file_path} not found");
|
||||
}
|
||||
|
||||
let mut anchor = None;
|
||||
project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: Anchor::MIN,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
// Check if specific line ranges are provided
|
||||
let result = if input.start_line.is_some() || input.end_line.is_some() {
|
||||
if input.start_line.is_some() || input.end_line.is_some() {
|
||||
let mut anchor = None;
|
||||
let result = buffer.read_with(cx, |buffer, _cx| {
|
||||
let text = buffer.text();
|
||||
// .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0.
|
||||
@@ -207,6 +214,18 @@ impl AgentTool for ReadFileTool {
|
||||
log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
if let Some(anchor) = anchor {
|
||||
project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: anchor,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(result.into())
|
||||
} else {
|
||||
// No line ranges specified, so check file size to see if it's too big.
|
||||
@@ -217,7 +236,7 @@ impl AgentTool for ReadFileTool {
|
||||
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx);
|
||||
log.buffer_read(buffer, cx);
|
||||
})?;
|
||||
|
||||
Ok(result.into())
|
||||
@@ -225,8 +244,7 @@ impl AgentTool for ReadFileTool {
|
||||
// File is too big, so return the outline
|
||||
// and a suggestion to read again with line numbers.
|
||||
let outline =
|
||||
outline::file_outline(project.clone(), file_path, action_log, None, cx)
|
||||
.await?;
|
||||
outline::file_outline(project, file_path, action_log, None, cx).await?;
|
||||
Ok(formatdoc! {"
|
||||
This file was too big to read all at once.
|
||||
|
||||
@@ -243,28 +261,7 @@ impl AgentTool for ReadFileTool {
|
||||
}
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
if let Some(abs_path) = project.absolute_path(&project_path, cx) {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: anchor.unwrap_or(text::Anchor::MIN),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
locations: Some(vec![acp::ToolCallLocation {
|
||||
path: abs_path,
|
||||
line: input.start_line.map(|line| line.saturating_sub(1)),
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
})?;
|
||||
|
||||
result
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,20 +48,6 @@ pub struct AgentProfileSettings {
|
||||
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
|
||||
}
|
||||
|
||||
impl AgentProfileSettings {
|
||||
pub fn is_tool_enabled(&self, tool_name: &str) -> bool {
|
||||
self.tools.get(tool_name) == Some(&true)
|
||||
}
|
||||
|
||||
pub fn is_context_server_tool_enabled(&self, server_id: &str, tool_name: &str) -> bool {
|
||||
self.enable_all_context_servers
|
||||
|| self
|
||||
.context_servers
|
||||
.get(server_id)
|
||||
.map_or(false, |preset| preset.tools.get(tool_name) == Some(&true))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ContextServerPreset {
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use acp_thread::MentionUri;
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use editor::display_map::CreaseId;
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use file_icons::FileIcons;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, WorktreeId};
|
||||
use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, WorktreeId};
|
||||
use rope::Point;
|
||||
use text::{Anchor, ToPoint};
|
||||
use ui::prelude::*;
|
||||
@@ -25,63 +23,21 @@ use crate::context_picker::file_context_picker::{extract_file_name_and_directory
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MentionSet {
|
||||
paths_by_crease_id: HashMap<CreaseId, MentionUri>,
|
||||
paths_by_crease_id: HashMap<CreaseId, ProjectPath>,
|
||||
}
|
||||
|
||||
impl MentionSet {
|
||||
pub fn insert(&mut self, crease_id: CreaseId, path: PathBuf) {
|
||||
self.paths_by_crease_id
|
||||
.insert(crease_id, MentionUri::File(path));
|
||||
pub fn insert(&mut self, crease_id: CreaseId, path: ProjectPath) {
|
||||
self.paths_by_crease_id.insert(crease_id, path);
|
||||
}
|
||||
|
||||
pub fn path_for_crease_id(&self, crease_id: CreaseId) -> Option<ProjectPath> {
|
||||
self.paths_by_crease_id.get(&crease_id).cloned()
|
||||
}
|
||||
|
||||
pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
|
||||
self.paths_by_crease_id.drain().map(|(id, _)| id)
|
||||
}
|
||||
|
||||
pub fn contents(
|
||||
&self,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<HashMap<CreaseId, Mention>>> {
|
||||
let contents = self
|
||||
.paths_by_crease_id
|
||||
.iter()
|
||||
.map(|(crease_id, uri)| match uri {
|
||||
MentionUri::File(path) => {
|
||||
let crease_id = *crease_id;
|
||||
let uri = uri.clone();
|
||||
let path = path.to_path_buf();
|
||||
let buffer_task = project.update(cx, |project, cx| {
|
||||
let path = project
|
||||
.find_project_path(path, cx)
|
||||
.context("Failed to find project path")?;
|
||||
anyhow::Ok(project.open_buffer(path, cx))
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = buffer_task?.await?;
|
||||
let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||
|
||||
anyhow::Ok((crease_id, Mention { uri, content }))
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
// TODO
|
||||
unimplemented!()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
let contents = try_join_all(contents).await?.into_iter().collect();
|
||||
anyhow::Ok(contents)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Mention {
|
||||
pub uri: MentionUri,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
pub struct ContextPickerCompletionProvider {
|
||||
@@ -112,7 +68,6 @@ impl ContextPickerCompletionProvider {
|
||||
source_range: Range<Anchor>,
|
||||
editor: Entity<Editor>,
|
||||
mention_set: Arc<Mutex<MentionSet>>,
|
||||
project: Entity<Project>,
|
||||
cx: &App,
|
||||
) -> Completion {
|
||||
let (file_name, directory) =
|
||||
@@ -157,7 +112,6 @@ impl ContextPickerCompletionProvider {
|
||||
new_text_len - 1,
|
||||
editor,
|
||||
mention_set,
|
||||
project,
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -205,7 +159,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let source_range = snapshot.anchor_before(state.source_range.start)
|
||||
..snapshot.anchor_after(state.source_range.end);
|
||||
@@ -242,7 +195,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
mention_set.clone(),
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -302,7 +254,6 @@ fn confirm_completion_callback(
|
||||
content_len: usize,
|
||||
editor: Entity<Editor>,
|
||||
mention_set: Arc<Mutex<MentionSet>>,
|
||||
project: Entity<Project>,
|
||||
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
|
||||
Arc::new(move |_, window, cx| {
|
||||
let crease_text = crease_text.clone();
|
||||
@@ -310,7 +261,6 @@ fn confirm_completion_callback(
|
||||
let editor = editor.clone();
|
||||
let project_path = project_path.clone();
|
||||
let mention_set = mention_set.clone();
|
||||
let project = project.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
let crease_id = crate::context_picker::insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
@@ -322,13 +272,8 @@ fn confirm_completion_callback(
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let Some(path) = project.read(cx).absolute_path(&project_path, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(crease_id) = crease_id {
|
||||
mention_set.lock().insert(crease_id, path);
|
||||
mention_set.lock().insert(crease_id, project_path);
|
||||
}
|
||||
});
|
||||
false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use acp_thread::{
|
||||
AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
|
||||
LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
|
||||
LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
|
||||
};
|
||||
use acp_thread::{AgentConnection, Plan};
|
||||
use action_log::ActionLog;
|
||||
@@ -27,9 +27,7 @@ use language::{Buffer, Language};
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use parking_lot::Mutex;
|
||||
use project::{CompletionIntent, Project};
|
||||
use rope::Point;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
|
||||
time::Duration,
|
||||
@@ -378,101 +376,81 @@ impl AcpThreadView {
|
||||
let mut ix = 0;
|
||||
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
|
||||
let project = self.project.clone();
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
editor.display_map.update(cx, |map, cx| {
|
||||
let snapshot = map.snapshot(cx);
|
||||
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
|
||||
// Skip creases that have been edited out of the message buffer.
|
||||
if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let contents = self.mention_set.lock().contents(project, cx);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let contents = match contents.await {
|
||||
Ok(contents) => contents,
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.last_error =
|
||||
Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx)));
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.message_editor.update(cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
editor.display_map.update(cx, |map, cx| {
|
||||
let snapshot = map.snapshot(cx);
|
||||
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
|
||||
// Skip creases that have been edited out of the message buffer.
|
||||
if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(mention) = contents.get(&crease_id) {
|
||||
let crease_range =
|
||||
crease.range().to_offset(&snapshot.buffer_snapshot);
|
||||
if crease_range.start > ix {
|
||||
chunks.push(text[ix..crease_range.start].into());
|
||||
}
|
||||
chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||
annotations: None,
|
||||
resource: acp::EmbeddedResourceResource::TextResourceContents(
|
||||
acp::TextResourceContents {
|
||||
mime_type: None,
|
||||
text: mention.content.clone(),
|
||||
uri: mention.uri.to_uri(),
|
||||
},
|
||||
),
|
||||
}));
|
||||
ix = crease_range.end;
|
||||
}
|
||||
if let Some(project_path) =
|
||||
self.mention_set.lock().path_for_crease_id(crease_id)
|
||||
{
|
||||
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
|
||||
if crease_range.start > ix {
|
||||
chunks.push(text[ix..crease_range.start].into());
|
||||
}
|
||||
|
||||
if ix < text.len() {
|
||||
let last_chunk = text[ix..].trim_end();
|
||||
if !last_chunk.is_empty() {
|
||||
chunks.push(last_chunk.into());
|
||||
}
|
||||
if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
|
||||
let path_str = abs_path.display().to_string();
|
||||
chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
uri: path_str.clone(),
|
||||
name: path_str,
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
}));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if chunks.is_empty() {
|
||||
return;
|
||||
ix = crease_range.end;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(thread) = this.thread() else {
|
||||
return;
|
||||
};
|
||||
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Err(err) = result {
|
||||
this.last_error =
|
||||
Some(cx.new(|cx| {
|
||||
Markdown::new(err.to_string().into(), None, None, cx)
|
||||
}))
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mention_set = this.mention_set.clone();
|
||||
|
||||
this.set_editor_is_expanded(false, cx);
|
||||
|
||||
this.message_editor.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.remove_creases(mention_set.lock().drain(), cx)
|
||||
});
|
||||
|
||||
this.scroll_to_bottom(cx);
|
||||
|
||||
this.message_history.borrow_mut().push(chunks);
|
||||
if ix < text.len() {
|
||||
let last_chunk = text[ix..].trim_end();
|
||||
if !last_chunk.is_empty() {
|
||||
chunks.push(last_chunk.into());
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if chunks.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(thread) = self.thread() else {
|
||||
return;
|
||||
};
|
||||
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Err(err) = result {
|
||||
this.last_error =
|
||||
Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx)))
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mention_set = self.mention_set.clone();
|
||||
|
||||
self.set_editor_is_expanded(false, cx);
|
||||
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.remove_creases(mention_set.lock().drain(), cx)
|
||||
});
|
||||
|
||||
self.scroll_to_bottom(cx);
|
||||
|
||||
self.message_history.borrow_mut().push(chunks);
|
||||
}
|
||||
|
||||
fn previous_history_message(
|
||||
@@ -585,19 +563,16 @@ impl AcpThreadView {
|
||||
acp::ContentBlock::Text(text_content) => {
|
||||
text.push_str(&text_content.text);
|
||||
}
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||
resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
|
||||
..
|
||||
}) => {
|
||||
let path = PathBuf::from(&resource.uri);
|
||||
let project_path = project.read(cx).project_path_for_absolute_path(&path, cx);
|
||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
||||
let path = Path::new(&resource_link.uri);
|
||||
let start = text.len();
|
||||
let content = MentionUri::File(path).to_uri();
|
||||
let content = MentionPath::new(&path).to_string();
|
||||
text.push_str(&content);
|
||||
let end = text.len();
|
||||
if let Some(project_path) = project_path {
|
||||
let filename: SharedString = project_path
|
||||
.path
|
||||
if let Some(project_path) =
|
||||
project.read(cx).project_path_for_absolute_path(&path, cx)
|
||||
{
|
||||
let filename: SharedString = path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
@@ -608,8 +583,7 @@ impl AcpThreadView {
|
||||
}
|
||||
acp::ContentBlock::Image(_)
|
||||
| acp::ContentBlock::Audio(_)
|
||||
| acp::ContentBlock::Resource(_)
|
||||
| acp::ContentBlock::ResourceLink(_) => {}
|
||||
| acp::ContentBlock::Resource(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,21 +602,18 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
let anchor = snapshot.anchor_before(range.start);
|
||||
if let Some(project_path) = project.read(cx).absolute_path(&project_path, cx) {
|
||||
let crease_id = crate::context_picker::insert_crease_for_mention(
|
||||
anchor.excerpt_id,
|
||||
anchor.text_anchor,
|
||||
range.end - range.start,
|
||||
filename,
|
||||
crease_icon_path,
|
||||
message_editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(crease_id) = crease_id {
|
||||
mention_set.lock().insert(crease_id, project_path);
|
||||
}
|
||||
let crease_id = crate::context_picker::insert_crease_for_mention(
|
||||
anchor.excerpt_id,
|
||||
anchor.text_anchor,
|
||||
range.end - range.start,
|
||||
filename,
|
||||
crease_icon_path,
|
||||
message_editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if let Some(crease_id) = crease_id {
|
||||
mention_set.lock().insert(crease_id, project_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,10 +1080,10 @@ impl AcpThreadView {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted);
|
||||
|
||||
let base_container = h_flex().size_4().justify_center();
|
||||
|
||||
if is_collapsible {
|
||||
base_container
|
||||
h_flex()
|
||||
.size_4()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.group_hover(&group_name, |s| s.invisible().w_0())
|
||||
@@ -1143,7 +1114,7 @@ impl AcpThreadView {
|
||||
),
|
||||
)
|
||||
} else {
|
||||
base_container.child(tool_icon)
|
||||
div().child(tool_icon)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1206,10 +1177,8 @@ impl AcpThreadView {
|
||||
ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx),
|
||||
_ => false,
|
||||
});
|
||||
let use_card_layout = needs_confirmation || is_edit || has_diff;
|
||||
|
||||
let is_collapsible = !tool_call.content.is_empty() && !use_card_layout;
|
||||
|
||||
let is_collapsible =
|
||||
!tool_call.content.is_empty() && !needs_confirmation && !is_edit && !has_diff;
|
||||
let is_open = tool_call.content.is_empty()
|
||||
|| needs_confirmation
|
||||
|| has_nonempty_diff
|
||||
@@ -1228,39 +1197,9 @@ impl AcpThreadView {
|
||||
linear_color_stop(color.opacity(0.2), 0.),
|
||||
))
|
||||
};
|
||||
let gradient_color = if use_card_layout {
|
||||
self.tool_card_header_bg(cx)
|
||||
} else {
|
||||
cx.theme().colors().panel_background
|
||||
};
|
||||
|
||||
let tool_output_display = match &tool_call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
|
||||
.w_full()
|
||||
.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.child(self.render_tool_call_content(content, tool_call, window, cx))
|
||||
.into_any_element()
|
||||
}))
|
||||
.child(self.render_permission_buttons(
|
||||
options,
|
||||
entry_ix,
|
||||
tool_call.id.clone(),
|
||||
tool_call.content.is_empty(),
|
||||
cx,
|
||||
)),
|
||||
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => v_flex()
|
||||
.w_full()
|
||||
.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.child(self.render_tool_call_content(content, tool_call, window, cx))
|
||||
.into_any_element()
|
||||
})),
|
||||
ToolCallStatus::Rejected => v_flex().size_0(),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.when(use_card_layout, |this| {
|
||||
.when(needs_confirmation || is_edit || has_diff, |this| {
|
||||
this.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
@@ -1274,11 +1213,13 @@ impl AcpThreadView {
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.map(|this| {
|
||||
if use_card_layout {
|
||||
if needs_confirmation || is_edit || has_diff {
|
||||
this.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.rounded_t_md()
|
||||
.border_b_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.bg(self.tool_card_header_bg(cx))
|
||||
} else {
|
||||
this.opacity(0.8).hover(|style| style.opacity(1.))
|
||||
@@ -1289,6 +1230,13 @@ impl AcpThreadView {
|
||||
.group(&card_header_id)
|
||||
.relative()
|
||||
.w_full()
|
||||
.map(|this| {
|
||||
if tool_call.locations.len() == 1 {
|
||||
this.gap_0()
|
||||
} else {
|
||||
this.gap_1p5()
|
||||
}
|
||||
})
|
||||
.text_size(self.tool_name_font_size())
|
||||
.child(self.render_tool_call_icon(
|
||||
card_header_id,
|
||||
@@ -1332,7 +1280,6 @@ impl AcpThreadView {
|
||||
.id("non-card-label-container")
|
||||
.w_full()
|
||||
.relative()
|
||||
.ml_1p5()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -1349,7 +1296,17 @@ impl AcpThreadView {
|
||||
),
|
||||
)),
|
||||
)
|
||||
.child(gradient_overlay(gradient_color))
|
||||
.map(|this| {
|
||||
if needs_confirmation {
|
||||
this.child(gradient_overlay(
|
||||
self.tool_card_header_bg(cx),
|
||||
))
|
||||
} else {
|
||||
this.child(gradient_overlay(
|
||||
cx.theme().colors().panel_background,
|
||||
))
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
@@ -1366,7 +1323,54 @@ impl AcpThreadView {
|
||||
)
|
||||
.children(status_icon),
|
||||
)
|
||||
.when(is_open, |this| this.child(tool_output_display))
|
||||
.when(is_open, |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.text_xs()
|
||||
.when(is_collapsible, |this| {
|
||||
this.mt_1()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_lg()
|
||||
})
|
||||
.map(|this| {
|
||||
if is_open {
|
||||
match &tool_call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { options, .. } => this
|
||||
.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.py_1p5()
|
||||
.child(self.render_tool_call_content(
|
||||
content, tool_call, window, cx,
|
||||
))
|
||||
.into_any_element()
|
||||
}))
|
||||
.child(self.render_permission_buttons(
|
||||
options,
|
||||
entry_ix,
|
||||
tool_call.id.clone(),
|
||||
tool_call.content.is_empty(),
|
||||
cx,
|
||||
)),
|
||||
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
|
||||
this.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.py_1p5()
|
||||
.child(self.render_tool_call_content(
|
||||
content, tool_call, window, cx,
|
||||
))
|
||||
.into_any_element()
|
||||
}))
|
||||
}
|
||||
ToolCallStatus::Rejected => this,
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_tool_call_content(
|
||||
@@ -1378,100 +1382,27 @@ impl AcpThreadView {
|
||||
) -> AnyElement {
|
||||
match content {
|
||||
ToolCallContent::ContentBlock(content) => {
|
||||
if let Some(resource_link) = content.resource_link() {
|
||||
self.render_resource_link(resource_link, cx)
|
||||
} else if let Some(markdown) = content.markdown() {
|
||||
self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx)
|
||||
if let Some(md) = content.markdown() {
|
||||
div()
|
||||
.p_2()
|
||||
.child(
|
||||
self.render_markdown(
|
||||
md.clone(),
|
||||
default_markdown_style(false, window, cx),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Empty.into_any_element()
|
||||
}
|
||||
}
|
||||
ToolCallContent::Diff(diff) => {
|
||||
self.render_diff_editor(&diff.read(cx).multibuffer(), cx)
|
||||
}
|
||||
ToolCallContent::Diff(diff) => self.render_diff_editor(&diff.read(cx).multibuffer()),
|
||||
ToolCallContent::Terminal(terminal) => {
|
||||
self.render_terminal_tool_call(terminal, tool_call, window, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_markdown_output(
|
||||
&self,
|
||||
markdown: Entity<Markdown>,
|
||||
tool_call_id: acp::ToolCallId,
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id.clone()));
|
||||
|
||||
v_flex()
|
||||
.mt_1p5()
|
||||
.ml(px(7.))
|
||||
.px_3p5()
|
||||
.gap_2()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(self.render_markdown(markdown, default_markdown_style(false, window, cx)))
|
||||
.child(
|
||||
Button::new(button_id, "Collapse Output", cx)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::ChevronUp)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call_id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_resource_link(
|
||||
&self,
|
||||
resource_link: &acp::ResourceLink,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let uri: SharedString = resource_link.uri.clone().into();
|
||||
|
||||
let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") {
|
||||
path.to_string().into()
|
||||
} else {
|
||||
uri.clone()
|
||||
};
|
||||
|
||||
let button_id = SharedString::from(format!("item-{}", uri.clone()));
|
||||
|
||||
div()
|
||||
.ml(px(7.))
|
||||
.pl_2p5()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
Button::new(button_id, label, cx)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.truncate(true)
|
||||
.on_click(cx.listener({
|
||||
let workspace = self.workspace.clone();
|
||||
move |_, _, window, cx: &mut Context<Self>| {
|
||||
Self::open_link(uri.clone(), &workspace, window, cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_permission_buttons(
|
||||
&self,
|
||||
options: &[acp::PermissionOption],
|
||||
@@ -1498,7 +1429,7 @@ impl AcpThreadView {
|
||||
)
|
||||
.child(h_flex().gap_0p5().children(options.iter().map(|option| {
|
||||
let option_id = SharedString::from(option.id.0.clone());
|
||||
Button::new((option_id, entry_ix), option.name.clone(), cx)
|
||||
Button::new((option_id, entry_ix), option.name.clone())
|
||||
.map(|this| match option.kind {
|
||||
acp::PermissionOptionKind::AllowOnce => {
|
||||
this.icon(IconName::Check).icon_color(Color::Success)
|
||||
@@ -1532,15 +1463,9 @@ impl AcpThreadView {
|
||||
})))
|
||||
}
|
||||
|
||||
fn render_diff_editor(
|
||||
&self,
|
||||
multibuffer: &Entity<MultiBuffer>,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
|
||||
v_flex()
|
||||
.h_full()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
|
||||
editor.clone().into_any_element()
|
||||
@@ -1628,7 +1553,6 @@ impl AcpThreadView {
|
||||
Button::new(
|
||||
SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
|
||||
"Stop",
|
||||
cx,
|
||||
)
|
||||
.icon(IconName::Stop)
|
||||
.icon_position(IconPosition::Start)
|
||||
@@ -1754,9 +1678,7 @@ impl AcpThreadView {
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
v_flex()
|
||||
.py_1p5()
|
||||
.pl_2()
|
||||
.pr_1p5()
|
||||
.p_2()
|
||||
.gap_0p5()
|
||||
.bg(header_bg)
|
||||
.text_xs()
|
||||
@@ -1933,7 +1855,7 @@ impl AcpThreadView {
|
||||
{
|
||||
let upgrade_message = upgrade_message.clone();
|
||||
let upgrade_command = upgrade_command.clone();
|
||||
container = container.child(Button::new("upgrade", upgrade_message, cx).on_click(
|
||||
container = container.child(Button::new("upgrade", upgrade_message).on_click(
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
@@ -2012,26 +1934,24 @@ impl AcpThreadView {
|
||||
parent.child(self.render_plan_entries(plan, window, cx))
|
||||
})
|
||||
})
|
||||
.when(!plan.is_empty() && !changed_buffers.is_empty(), |this| {
|
||||
this.child(Divider::horizontal().color(DividerColor::Border))
|
||||
})
|
||||
.when(!changed_buffers.is_empty(), |this| {
|
||||
this.child(self.render_edits_summary(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
self.edits_expanded,
|
||||
pending_edits,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(self.edits_expanded, |parent| {
|
||||
parent.child(self.render_edited_files(
|
||||
this.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(self.render_edits_summary(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
self.edits_expanded,
|
||||
pending_edits,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.when(self.edits_expanded, |parent| {
|
||||
parent.child(self.render_edited_files(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
pending_edits,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
})
|
||||
.into_any()
|
||||
.into()
|
||||
@@ -2265,7 +2185,7 @@ impl AcpThreadView {
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::Border))
|
||||
.child(
|
||||
Button::new("reject-all-changes", "Reject All", cx)
|
||||
Button::new("reject-all-changes", "Reject All")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.when(pending_edits, |this| {
|
||||
@@ -2290,7 +2210,7 @@ impl AcpThreadView {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("keep-all-changes", "Keep All", cx)
|
||||
Button::new("keep-all-changes", "Keep All")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.when(pending_edits, |this| {
|
||||
@@ -2396,7 +2316,7 @@ impl AcpThreadView {
|
||||
.gap_1()
|
||||
.visible_on_hover("edited-code")
|
||||
.child(
|
||||
Button::new("review", "Review", cx)
|
||||
Button::new("review", "Review")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
@@ -2407,7 +2327,7 @@ impl AcpThreadView {
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::BorderVariant))
|
||||
.child(
|
||||
Button::new("reject-file", "Reject", cx)
|
||||
Button::new("reject-file", "Reject")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.on_click({
|
||||
@@ -2427,7 +2347,7 @@ impl AcpThreadView {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("keep-file", "Keep", cx)
|
||||
Button::new("keep-file", "Keep")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.on_click({
|
||||
@@ -2642,31 +2562,25 @@ impl AcpThreadView {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(mention) = MentionUri::parse(&url).log_err() {
|
||||
workspace.update(cx, |workspace, cx| match mention {
|
||||
MentionUri::File(path) => {
|
||||
let project = workspace.project();
|
||||
let Some((path, entry)) = project.update(cx, |project, cx| {
|
||||
let path = project.find_project_path(path, cx)?;
|
||||
let entry = project.entry_for_path(&path, cx)?;
|
||||
Some((path, entry))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
if let Some(mention_path) = MentionPath::try_parse(&url) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project();
|
||||
let Some((path, entry)) = project.update(cx, |project, cx| {
|
||||
let path = project.find_project_path(mention_path.path(), cx)?;
|
||||
let entry = project.entry_for_path(&path, cx)?;
|
||||
Some((path, entry))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if entry.is_dir() {
|
||||
project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
||||
});
|
||||
} else {
|
||||
workspace
|
||||
.open_path(path, None, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// TODO
|
||||
unimplemented!()
|
||||
if entry.is_dir() {
|
||||
project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
||||
});
|
||||
} else {
|
||||
workspace
|
||||
.open_path(path, None, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -2681,24 +2595,26 @@ impl AcpThreadView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
let (tool_call_location, agent_location) = self
|
||||
let location = self
|
||||
.thread()?
|
||||
.read(cx)
|
||||
.entries()
|
||||
.get(entry_ix)?
|
||||
.location(location_ix)?;
|
||||
.locations()?
|
||||
.get(location_ix)?;
|
||||
|
||||
let project_path = self
|
||||
.project
|
||||
.read(cx)
|
||||
.find_project_path(&tool_call_location.path, cx)?;
|
||||
.find_project_path(&location.path, cx)?;
|
||||
|
||||
let open_task = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
.update(cx, |worskpace, cx| {
|
||||
worskpace.open_path(project_path, None, true, window, cx)
|
||||
})
|
||||
.log_err()?;
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let item = open_task.await?;
|
||||
@@ -2708,22 +2624,17 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
active_editor.update_in(cx, |editor, window, cx| {
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
let buffer = multibuffer.as_singleton();
|
||||
if agent_location.buffer.upgrade() == buffer {
|
||||
let excerpt_id = multibuffer.excerpt_ids().first().cloned();
|
||||
let anchor = editor::Anchor::in_buffer(
|
||||
excerpt_id.unwrap(),
|
||||
buffer.unwrap().read(cx).remote_id(),
|
||||
agent_location.position,
|
||||
);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let first_hunk = editor
|
||||
.diff_hunks_in_ranges(
|
||||
&[editor::Anchor::min()..editor::Anchor::max()],
|
||||
&snapshot,
|
||||
)
|
||||
.next();
|
||||
if let Some(first_hunk) = first_hunk {
|
||||
let first_hunk_start = first_hunk.multi_buffer_range().start;
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_anchor_ranges([anchor..anchor]);
|
||||
})
|
||||
} else {
|
||||
let row = tool_call_location.line.unwrap_or_default();
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]);
|
||||
selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
|
||||
})
|
||||
}
|
||||
})?;
|
||||
@@ -2953,8 +2864,7 @@ impl AcpThreadView {
|
||||
|
||||
fn render_thread_controls(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(Tooltip::text("Open Thread as Markdown"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
@@ -2965,8 +2875,7 @@ impl AcpThreadView {
|
||||
}));
|
||||
|
||||
let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(Tooltip::text("Scroll To Top"))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
@@ -2977,6 +2886,7 @@ impl AcpThreadView {
|
||||
.w_full()
|
||||
.mr_1()
|
||||
.pb_2()
|
||||
.gap_1()
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.opacity(0.4)
|
||||
.hover(|style| style.opacity(1.))
|
||||
@@ -3065,7 +2975,6 @@ impl AcpThreadView {
|
||||
anchor..anchor,
|
||||
self.message_editor.clone(),
|
||||
self.mention_set.clone(),
|
||||
self.project.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -3093,8 +3002,6 @@ impl Focusable for AcpThreadView {
|
||||
|
||||
impl Render for AcpThreadView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let has_messages = self.list_state.item_count() > 0;
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.key_context("AcpThread")
|
||||
@@ -3115,7 +3022,6 @@ impl Render for AcpThreadView {
|
||||
Button::new(
|
||||
SharedString::from(method.id.0.clone()),
|
||||
method.name.clone(),
|
||||
cx,
|
||||
)
|
||||
.on_click({
|
||||
let method_id = method.id.clone();
|
||||
@@ -3142,7 +3048,7 @@ impl Render for AcpThreadView {
|
||||
let thread_clone = thread.clone();
|
||||
|
||||
v_flex().flex_1().map(|this| {
|
||||
if has_messages {
|
||||
if self.list_state.item_count() > 0 {
|
||||
this.child(
|
||||
list(
|
||||
self.list_state.clone(),
|
||||
@@ -3161,32 +3067,23 @@ impl Render for AcpThreadView {
|
||||
.into_any(),
|
||||
)
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
.children(
|
||||
match thread_clone.read(cx).status() {
|
||||
ThreadStatus::Idle
|
||||
| ThreadStatus::WaitingForToolConfirmation => None,
|
||||
ThreadStatus::Generating => div()
|
||||
.px_5()
|
||||
.py_2()
|
||||
.child(LoadingLabel::new("").size(LabelSize::Small))
|
||||
.into(),
|
||||
},
|
||||
)
|
||||
.children(match thread_clone.read(cx).status() {
|
||||
ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => {
|
||||
None
|
||||
}
|
||||
ThreadStatus::Generating => div()
|
||||
.px_5()
|
||||
.py_2()
|
||||
.child(LoadingLabel::new("").size(LabelSize::Small))
|
||||
.into(),
|
||||
})
|
||||
.children(self.render_activity_bar(&thread_clone, window, cx))
|
||||
} else {
|
||||
this.child(self.render_empty_state(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
|
||||
// the scrollbar to stop exactly at the activity bar start.
|
||||
.when(has_messages, |this| match &self.thread_state {
|
||||
ThreadState::Ready { thread, .. } => {
|
||||
this.children(self.render_activity_bar(thread, window, cx))
|
||||
}
|
||||
_ => this,
|
||||
})
|
||||
.when_some(self.last_error.clone(), |el, error| {
|
||||
el.child(
|
||||
div()
|
||||
@@ -3220,7 +3117,7 @@ fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
|
||||
style.base_text_style = text_style;
|
||||
style.link_callback = Some(Rc::new(move |url, cx| {
|
||||
if MentionUri::parse(url).is_ok() {
|
||||
if MentionPath::try_parse(url).is_some() {
|
||||
let colors = cx.theme().colors();
|
||||
Some(TextStyleRefinement {
|
||||
background_color: Some(colors.element_background),
|
||||
|
||||
@@ -2281,7 +2281,7 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
let restore_checkpoint_button =
|
||||
Button::new(("restore-checkpoint", ix), "Restore Checkpoint", cx)
|
||||
Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
|
||||
.icon(if error.is_some() {
|
||||
IconName::XCircle
|
||||
} else {
|
||||
@@ -2371,7 +2371,7 @@ impl ActiveThread {
|
||||
.gap_1()
|
||||
.justify_end()
|
||||
.child(
|
||||
Button::new("dismiss-feedback-message", "Cancel", cx)
|
||||
Button::new("dismiss-feedback-message", "Cancel")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
@@ -2394,7 +2394,6 @@ impl ActiveThread {
|
||||
Button::new(
|
||||
"submit-feedback-message",
|
||||
"Share Feedback",
|
||||
cx,
|
||||
)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.label_size(LabelSize::Small)
|
||||
@@ -3218,7 +3217,6 @@ impl ActiveThread {
|
||||
Button::new(
|
||||
"always-allow-tool-action",
|
||||
"Always Allow",
|
||||
cx,
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::CheckDouble)
|
||||
@@ -3256,7 +3254,7 @@ impl ActiveThread {
|
||||
})
|
||||
.child({
|
||||
let tool_id = tool_use.id.clone();
|
||||
Button::new("allow-tool-action", "Allow", cx)
|
||||
Button::new("allow-tool-action", "Allow")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Check)
|
||||
.icon_position(IconPosition::Start)
|
||||
@@ -3276,7 +3274,7 @@ impl ActiveThread {
|
||||
.child({
|
||||
let tool_id = tool_use.id.clone();
|
||||
let tool_name: Arc<str> = tool_use.name.into();
|
||||
Button::new("deny-tool", "Deny", cx)
|
||||
Button::new("deny-tool", "Deny")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Close)
|
||||
.icon_position(IconPosition::Start)
|
||||
|
||||
@@ -281,7 +281,6 @@ impl AgentConfiguration {
|
||||
Button::new(
|
||||
SharedString::from(format!("new-thread-{provider_id}")),
|
||||
"Start New Thread",
|
||||
cx,
|
||||
)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
@@ -340,7 +339,7 @@ impl AgentConfiguration {
|
||||
.child(
|
||||
PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
Button::new("add-provider", "Add Provider", cx)
|
||||
Button::new("add-provider", "Add Provider")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -553,7 +552,7 @@ impl AgentConfiguration {
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex().w_full().child(
|
||||
Button::new("add-context-server", "Add Custom Server", cx)
|
||||
Button::new("add-context-server", "Add Custom Server")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.full_width()
|
||||
@@ -570,7 +569,6 @@ impl AgentConfiguration {
|
||||
Button::new(
|
||||
"install-context-server-extensions",
|
||||
"Install MCP Extensions",
|
||||
cx,
|
||||
)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
|
||||
@@ -281,7 +281,7 @@ impl AddLlmProviderModal {
|
||||
.justify_between()
|
||||
.child(Label::new("Models").size(LabelSize::Small))
|
||||
.child(
|
||||
Button::new("add-model", "Add Model", cx)
|
||||
Button::new("add-model", "Add Model")
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -324,7 +324,7 @@ impl AddLlmProviderModal {
|
||||
.child(model.max_tokens.clone())
|
||||
.when(has_more_than_one_model, |this| {
|
||||
this.child(
|
||||
Button::new(("remove-model", ix), "Remove Model", cx)
|
||||
Button::new(("remove-model", ix), "Remove Model")
|
||||
.icon(IconName::Trash)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -400,7 +400,7 @@ impl Render for AddLlmProviderModal {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("cancel", "Cancel", cx)
|
||||
Button::new("cancel", "Cancel")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Cancel,
|
||||
@@ -415,7 +415,7 @@ impl Render for AddLlmProviderModal {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("save-server", "Save Provider", cx)
|
||||
Button::new("save-server", "Save Provider")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
|
||||
@@ -564,7 +564,7 @@ impl ConfigureContextServerModal {
|
||||
} = &self.source
|
||||
{
|
||||
Some(
|
||||
Button::new("open-repository", "Open Repository", cx)
|
||||
Button::new("open-repository", "Open Repository")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -600,7 +600,6 @@ impl ConfigureContextServerModal {
|
||||
} else {
|
||||
"Dismiss"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx)
|
||||
@@ -618,7 +617,6 @@ impl ConfigureContextServerModal {
|
||||
} else {
|
||||
"Configure Server"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.disabled(is_connecting)
|
||||
.key_binding(
|
||||
|
||||
@@ -726,7 +726,7 @@ impl Render for AgentDiffPane {
|
||||
.gap_2()
|
||||
.child("No changes to review")
|
||||
.child(
|
||||
Button::new("continue-iterating", "Continue Iterating", cx)
|
||||
Button::new("continue-iterating", "Continue Iterating")
|
||||
.style(ButtonStyle::Filled)
|
||||
.icon(IconName::ForwardArrow)
|
||||
.icon_position(IconPosition::Start)
|
||||
@@ -806,7 +806,7 @@ fn render_diff_hunk_controls(
|
||||
.block_mouse_except_scroll()
|
||||
.shadow_md()
|
||||
.children(vec![
|
||||
Button::new(("reject", row as u64), "Reject", cx)
|
||||
Button::new(("reject", row as u64), "Reject")
|
||||
.disabled(is_created_file)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
@@ -834,7 +834,7 @@ fn render_diff_hunk_controls(
|
||||
})
|
||||
}
|
||||
}),
|
||||
Button::new(("keep", row as u64), "Keep", cx)
|
||||
Button::new(("keep", row as u64), "Keep")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&Keep, &editor.read(cx).focus_handle(cx), window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
@@ -1147,7 +1147,7 @@ impl Render for AgentDiffToolbar {
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Button::new("reject-all", "Reject All", cx)
|
||||
Button::new("reject-all", "Reject All")
|
||||
.key_binding({
|
||||
KeyBinding::for_action_in(
|
||||
&RejectAll,
|
||||
@@ -1162,7 +1162,7 @@ impl Render for AgentDiffToolbar {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("keep-all", "Keep All", cx)
|
||||
Button::new("keep-all", "Keep All")
|
||||
.key_binding({
|
||||
KeyBinding::for_action_in(
|
||||
&KeepAll,
|
||||
@@ -1242,7 +1242,7 @@ impl Render for AgentDiffToolbar {
|
||||
.child(
|
||||
h_group_sm()
|
||||
.child(
|
||||
Button::new("reject-all", "Reject All", cx)
|
||||
Button::new("reject-all", "Reject All")
|
||||
.key_binding({
|
||||
KeyBinding::for_action_in(
|
||||
&RejectAll,
|
||||
@@ -1257,7 +1257,7 @@ impl Render for AgentDiffToolbar {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("keep-all", "Keep All", cx)
|
||||
Button::new("keep-all", "Keep All")
|
||||
.key_binding({
|
||||
KeyBinding::for_action_in(
|
||||
&KeepAll,
|
||||
|
||||
@@ -2444,7 +2444,7 @@ impl AgentPanel {
|
||||
.gap_1()
|
||||
.max_w_48()
|
||||
.child(
|
||||
Button::new("context", "Add Context", cx)
|
||||
Button::new("context", "Add Context")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::FileCode)
|
||||
.icon_position(IconPosition::Start)
|
||||
@@ -2465,7 +2465,7 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("mode", "Switch Model", cx)
|
||||
Button::new("mode", "Switch Model")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::DatabaseZap)
|
||||
.icon_position(IconPosition::Start)
|
||||
@@ -2486,7 +2486,7 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("settings", "View Settings", cx)
|
||||
Button::new("settings", "View Settings")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Settings)
|
||||
.icon_position(IconPosition::Start)
|
||||
@@ -2529,7 +2529,7 @@ impl AgentPanel {
|
||||
self.render_empty_state_section_header(
|
||||
"Recent",
|
||||
Some(
|
||||
Button::new("view-history", "View All", cx)
|
||||
Button::new("view-history", "View All")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
@@ -2727,7 +2727,7 @@ impl AgentPanel {
|
||||
.severity(ui::Severity::Warning)
|
||||
.child(Label::new(configuration_error.to_string()))
|
||||
.action_slot(
|
||||
Button::new("settings", "Configure Provider", cx)
|
||||
Button::new("settings", "Configure Provider")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
@@ -2784,7 +2784,7 @@ impl AgentPanel {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("continue-conversation", "Continue", cx)
|
||||
Button::new("continue-conversation", "Continue")
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
@@ -2802,7 +2802,7 @@ impl AgentPanel {
|
||||
)
|
||||
.when(model.supports_burn_mode(), |this| {
|
||||
this.child(
|
||||
Button::new("continue-burn-mode", "Continue with Burn Mode", cx)
|
||||
Button::new("continue-burn-mode", "Continue with Burn Mode")
|
||||
.style(ButtonStyle::Filled)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
@@ -2873,7 +2873,7 @@ impl AgentPanel {
|
||||
thread: &Entity<ActiveThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
Button::new("upgrade", "Upgrade", cx)
|
||||
Button::new("upgrade", "Upgrade")
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(cx.listener({
|
||||
@@ -2965,7 +2965,7 @@ impl AgentPanel {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
let retry_button = Button::new("retry", "Retry", cx)
|
||||
let retry_button = Button::new("retry", "Retry")
|
||||
.icon(IconName::RotateCw)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -3009,7 +3009,7 @@ impl AgentPanel {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
let retry_button = Button::new("retry", "Retry", cx)
|
||||
let retry_button = Button::new("retry", "Retry")
|
||||
.icon(IconName::RotateCw)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -3034,26 +3034,22 @@ impl AgentPanel {
|
||||
.primary_action(retry_button);
|
||||
|
||||
if can_enable_burn_mode {
|
||||
let burn_mode_button =
|
||||
Button::new("enable_burn_retry", "Enable Burn Mode and Retry", cx)
|
||||
.icon(IconName::ZedBurnMode)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let thread = thread.clone();
|
||||
move |_, window, cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.clear_last_error();
|
||||
thread.thread().update(cx, |thread, cx| {
|
||||
thread.enable_burn_mode_and_retry(
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let burn_mode_button = Button::new("enable_burn_retry", "Enable Burn Mode and Retry")
|
||||
.icon(IconName::ZedBurnMode)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let thread = thread.clone();
|
||||
move |_, window, cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.clear_last_error();
|
||||
thread.thread().update(cx, |thread, cx| {
|
||||
thread.enable_burn_mode_and_retry(Some(window.window_handle()), cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
callout = callout.secondary_action(burn_mode_button);
|
||||
}
|
||||
|
||||
|
||||
@@ -478,7 +478,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
match codegen_status {
|
||||
CodegenStatus::Idle => {
|
||||
vec![
|
||||
Button::new("start", mode.start_label(), cx)
|
||||
Button::new("start", mode.start_label())
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Return)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -745,11 +745,11 @@ impl<T: 'static> PromptEditor<T> {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("dismiss", "Dismiss", cx)
|
||||
Button::new("dismiss", "Dismiss")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(cx.listener(Self::toggle_rate_limit_notice)),
|
||||
)
|
||||
.child(Button::new("more-info", "More Info", cx).on_click(
|
||||
.child(Button::new("more-info", "More Info").on_click(
|
||||
|_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(zed_actions::OpenAccountSettings),
|
||||
|
||||
@@ -548,7 +548,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
.justify_between()
|
||||
.when(cx.has_flag::<ZedProFeatureFlag>(), |this| {
|
||||
this.child(match plan {
|
||||
Plan::ZedPro => Button::new("zed-pro", "Zed Pro", cx)
|
||||
Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
|
||||
.icon(IconName::ZedAssistant)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -564,13 +564,12 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
} else {
|
||||
"Try Pro"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.on_click(|_, _, cx| cx.open_url(TRY_ZED_PRO_URL)),
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Button::new("configure", "Configure", cx)
|
||||
Button::new("configure", "Configure")
|
||||
.icon(IconName::Settings)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
|
||||
@@ -1074,7 +1074,7 @@ impl MessageEditor {
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::Border))
|
||||
.child(
|
||||
Button::new("reject-all-changes", "Reject All", cx)
|
||||
Button::new("reject-all-changes", "Reject All")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.when(pending_edits, |this| {
|
||||
@@ -1094,7 +1094,7 @@ impl MessageEditor {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("accept-all-changes", "Accept All", cx)
|
||||
Button::new("accept-all-changes", "Accept All")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.when(pending_edits, |this| {
|
||||
@@ -1204,7 +1204,7 @@ impl MessageEditor {
|
||||
.gap_1()
|
||||
.visible_on_hover("edited-code")
|
||||
.child(
|
||||
Button::new("review", "Review", cx)
|
||||
Button::new("review", "Review")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
@@ -1221,7 +1221,7 @@ impl MessageEditor {
|
||||
Divider::vertical().color(DividerColor::BorderVariant),
|
||||
)
|
||||
.child(
|
||||
Button::new("reject-file", "Reject", cx)
|
||||
Button::new("reject-file", "Reject")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.on_click({
|
||||
@@ -1236,7 +1236,7 @@ impl MessageEditor {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("accept-file", "Accept", cx)
|
||||
Button::new("accept-file", "Accept")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.on_click({
|
||||
@@ -1332,7 +1332,7 @@ impl MessageEditor {
|
||||
.title(title)
|
||||
.description(description)
|
||||
.primary_action(
|
||||
Button::new("start-new-thread", "Start New Thread", cx)
|
||||
Button::new("start-new-thread", "Start New Thread")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let from_thread_id = Some(this.thread.read(cx).id().clone());
|
||||
|
||||
@@ -167,7 +167,7 @@ impl Render for ProfileSelector {
|
||||
if configured_model.model.supports_tools() {
|
||||
let this = cx.entity().clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let trigger_button = Button::new("profile-selector-model", selected_profile, cx)
|
||||
let trigger_button = Button::new("profile-selector-model", selected_profile)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ChevronDown)
|
||||
@@ -201,7 +201,7 @@ impl Render for ProfileSelector {
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
Button::new("tools-not-supported-button", "Tools Unsupported", cx)
|
||||
Button::new("tools-not-supported-button", "Tools Unsupported")
|
||||
.disabled(true)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
|
||||
@@ -1200,7 +1200,7 @@ impl TextThreadEditor {
|
||||
})
|
||||
.children(match &message.status {
|
||||
MessageStatus::Error(error) => Some(
|
||||
Button::new("show-error", "Error", cx)
|
||||
Button::new("show-error", "Error")
|
||||
.color(Color::Error)
|
||||
.selected_label_color(Color::Error)
|
||||
.selected_icon_color(Color::Error)
|
||||
@@ -1920,7 +1920,7 @@ impl TextThreadEditor {
|
||||
None => (ButtonStyle::Filled, None),
|
||||
};
|
||||
|
||||
Button::new("send_button", "Send", cx)
|
||||
Button::new("send_button", "Send")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(self.sending_disabled(cx))
|
||||
.style(style)
|
||||
@@ -2124,16 +2124,14 @@ impl TextThreadEditor {
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(
|
||||
Button::new("subscribe", "Subscribe", cx).on_click(cx.listener(
|
||||
|this, _, _window, cx| {
|
||||
this.last_error = None;
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss", cx).on_click(cx.listener(
|
||||
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|
||||
|this, _, _window, cx| {
|
||||
this.last_error = None;
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
},
|
||||
)))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, _window, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
@@ -2167,14 +2165,17 @@ impl TextThreadEditor {
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message.clone())),
|
||||
)
|
||||
.child(h_flex().justify_end().mt_1().child(
|
||||
Button::new("dismiss", "Dismiss", cx).on_click(cx.listener(
|
||||
|this, _, _window, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, _window, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ impl Render for AgentNotification {
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.child(
|
||||
Button::new("open", "View Panel", cx)
|
||||
Button::new("open", "View Panel")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click({
|
||||
@@ -180,15 +180,11 @@ impl Render for AgentNotification {
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("dismiss", "Dismiss", cx)
|
||||
.full_width()
|
||||
.on_click({
|
||||
cx.listener(move |_, _event, _, cx| {
|
||||
cx.emit(AgentNotificationEvent::Dismissed);
|
||||
})
|
||||
}),
|
||||
),
|
||||
.child(Button::new("dismiss", "Dismiss").full_width().on_click({
|
||||
cx.listener(move |_, _event, _, cx| {
|
||||
cx.emit(AgentNotificationEvent::Dismissed);
|
||||
})
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ impl RenderOnce for EndTrialUpsell {
|
||||
)
|
||||
.child(plan_definitions.pro_plan(false))
|
||||
.child(
|
||||
Button::new("cta-button", "Upgrade to Zed Pro", cx)
|
||||
Button::new("cta-button", "Upgrade to Zed Pro")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| {
|
||||
|
||||
@@ -147,13 +147,13 @@ impl Render for AgentOnboardingModal {
|
||||
)),
|
||||
));
|
||||
|
||||
let open_panel_button = Button::new("open-panel", "Get Started with the Agent Panel", cx)
|
||||
let open_panel_button = Button::new("open-panel", "Get Started with the Agent Panel")
|
||||
.icon_size(IconSize::Indicator)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::open_panel));
|
||||
|
||||
let blog_post_button = Button::new("view-blog", "Check out the Blog Post", cx)
|
||||
let blog_post_button = Button::new("view-blog", "Check out the Blog Post")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
|
||||
@@ -99,7 +99,7 @@ impl RenderOnce for UsageCallout {
|
||||
.title(title)
|
||||
.description(message)
|
||||
.primary_action(
|
||||
Button::new("upgrade", button_text, cx)
|
||||
Button::new("upgrade", button_text)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.open_url(&url);
|
||||
|
||||
@@ -130,7 +130,7 @@ impl RenderOnce for ApiKeysWithoutProviders {
|
||||
"Add your own keys to use AI without signing in.",
|
||||
)))
|
||||
.child(
|
||||
Button::new("configure-providers", "Configure Providers", cx)
|
||||
Button::new("configure-providers", "Configure Providers")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click(move |_, window, cx| {
|
||||
|
||||
@@ -94,7 +94,7 @@ impl ZedAiOnboarding {
|
||||
self
|
||||
}
|
||||
|
||||
fn render_accept_terms_of_service(&self, cx: &mut App) -> AnyElement {
|
||||
fn render_accept_terms_of_service(&self) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
@@ -105,7 +105,7 @@ impl ZedAiOnboarding {
|
||||
.mb_2(),
|
||||
)
|
||||
.child(
|
||||
Button::new("terms_of_service", "Review Terms of Service", cx)
|
||||
Button::new("terms_of_service", "Review Terms of Service")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
@@ -117,7 +117,7 @@ impl ZedAiOnboarding {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("accept_terms", "Accept", cx)
|
||||
Button::new("accept_terms", "Accept")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click({
|
||||
@@ -130,7 +130,7 @@ impl ZedAiOnboarding {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_sign_in_disclaimer(&self, cx: &mut App) -> AnyElement {
|
||||
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
|
||||
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
|
||||
let plan_definitions = PlanDefinitions;
|
||||
|
||||
@@ -144,7 +144,7 @@ impl ZedAiOnboarding {
|
||||
)
|
||||
.child(plan_definitions.pro_plan(false))
|
||||
.child(
|
||||
Button::new("sign_in", "Try Zed Pro for Free", cx)
|
||||
Button::new("sign_in", "Try Zed Pro for Free")
|
||||
.disabled(signing_in)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
@@ -187,7 +187,7 @@ impl ZedAiOnboarding {
|
||||
)
|
||||
.child(plan_definitions.pro_plan(true))
|
||||
.child(
|
||||
Button::new("pro", "Get Started", cx)
|
||||
Button::new("pro", "Get Started")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| {
|
||||
@@ -268,7 +268,7 @@ impl ZedAiOnboarding {
|
||||
)
|
||||
.child(plan_definitions.pro_trial(true))
|
||||
.child(
|
||||
Button::new("pro", "Start Free Trial", cx)
|
||||
Button::new("pro", "Start Free Trial")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| {
|
||||
@@ -320,7 +320,7 @@ impl ZedAiOnboarding {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_pro_plan_state(&self, cx: &mut App) -> AnyElement {
|
||||
fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
|
||||
let plan_definitions = PlanDefinitions;
|
||||
|
||||
v_flex()
|
||||
@@ -333,7 +333,7 @@ impl ZedAiOnboarding {
|
||||
)
|
||||
.child(plan_definitions.pro_plan(false))
|
||||
.child(
|
||||
Button::new("pro", "Continue with Zed Pro", cx)
|
||||
Button::new("pro", "Continue with Zed Pro")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
@@ -358,7 +358,7 @@ impl RenderOnce for ZedAiOnboarding {
|
||||
Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
|
||||
}
|
||||
} else {
|
||||
self.render_accept_terms_of_service(cx)
|
||||
self.render_accept_terms_of_service()
|
||||
}
|
||||
} else {
|
||||
self.render_sign_in_disclaimer(cx)
|
||||
|
||||
@@ -186,7 +186,7 @@ impl RenderOnce for AiUpsellCard {
|
||||
)
|
||||
.child(plan_definitions.pro_plan(true))
|
||||
.child(
|
||||
Button::new("pro", "Get Started", cx)
|
||||
Button::new("pro", "Get Started")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(move |_, _window, cx| {
|
||||
@@ -209,25 +209,19 @@ impl RenderOnce for AiUpsellCard {
|
||||
.child(
|
||||
footer_container
|
||||
.child(
|
||||
Button::new(
|
||||
"start_trial",
|
||||
"Start 14-day Free Pro Trial",
|
||||
cx,
|
||||
)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.when_some(self.tab_index, |this, tab_index| {
|
||||
this.tab_index(tab_index)
|
||||
})
|
||||
.on_click(
|
||||
move |_, _window, cx| {
|
||||
Button::new("start_trial", "Start 14-day Free Pro Trial")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.when_some(self.tab_index, |this, tab_index| {
|
||||
this.tab_index(tab_index)
|
||||
})
|
||||
.on_click(move |_, _window, cx| {
|
||||
telemetry::event!(
|
||||
"Start Trial Clicked",
|
||||
state = "post-sign-in"
|
||||
);
|
||||
cx.open_url(&zed_urls::start_trial_url(cx))
|
||||
},
|
||||
),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Label::new("No credit card required")
|
||||
@@ -267,7 +261,7 @@ impl RenderOnce for AiUpsellCard {
|
||||
)
|
||||
.child(plans_section)
|
||||
.child(
|
||||
Button::new("sign_in", "Sign In", cx)
|
||||
Button::new("sign_in", "Sign In")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
|
||||
|
||||
@@ -50,7 +50,6 @@ impl Render for EditPredictionOnboarding {
|
||||
} else {
|
||||
"Configure Copilot"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
|
||||
@@ -65,7 +65,7 @@ pub enum EditAgentOutputEvent {
|
||||
ResolvingEditRange(Range<Anchor>),
|
||||
UnresolvedEditRange,
|
||||
AmbiguousEditRange(Vec<Range<usize>>),
|
||||
Edited(Range<Anchor>),
|
||||
Edited,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -178,9 +178,7 @@ impl EditAgent {
|
||||
)
|
||||
});
|
||||
output_events_tx
|
||||
.unbounded_send(EditAgentOutputEvent::Edited(
|
||||
language::Anchor::MIN..language::Anchor::MAX,
|
||||
))
|
||||
.unbounded_send(EditAgentOutputEvent::Edited)
|
||||
.ok();
|
||||
})?;
|
||||
|
||||
@@ -202,9 +200,7 @@ impl EditAgent {
|
||||
});
|
||||
})?;
|
||||
output_events_tx
|
||||
.unbounded_send(EditAgentOutputEvent::Edited(
|
||||
language::Anchor::MIN..language::Anchor::MAX,
|
||||
))
|
||||
.unbounded_send(EditAgentOutputEvent::Edited)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
@@ -340,8 +336,8 @@ impl EditAgent {
|
||||
// Edit the buffer and report edits to the action log as part of the
|
||||
// same effect cycle, otherwise the edit will be reported as if the
|
||||
// user made it.
|
||||
let (min_edit_start, max_edit_end) = cx.update(|cx| {
|
||||
let (min_edit_start, max_edit_end) = buffer.update(cx, |buffer, cx| {
|
||||
cx.update(|cx| {
|
||||
let max_edit_end = buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits.iter().cloned(), None, cx);
|
||||
let max_edit_end = buffer
|
||||
.summaries_for_anchors::<Point, _>(
|
||||
@@ -349,16 +345,7 @@ impl EditAgent {
|
||||
)
|
||||
.max()
|
||||
.unwrap();
|
||||
let min_edit_start = buffer
|
||||
.summaries_for_anchors::<Point, _>(
|
||||
edits.iter().map(|(range, _)| &range.start),
|
||||
)
|
||||
.min()
|
||||
.unwrap();
|
||||
(
|
||||
buffer.anchor_after(min_edit_start),
|
||||
buffer.anchor_before(max_edit_end),
|
||||
)
|
||||
buffer.anchor_before(max_edit_end)
|
||||
});
|
||||
self.action_log
|
||||
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
@@ -371,10 +358,9 @@ impl EditAgent {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
(min_edit_start, max_edit_end)
|
||||
})?;
|
||||
output_events
|
||||
.unbounded_send(EditAgentOutputEvent::Edited(min_edit_start..max_edit_end))
|
||||
.unbounded_send(EditAgentOutputEvent::Edited)
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -769,7 +755,6 @@ mod tests {
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use pretty_assertions::assert_matches;
|
||||
use project::{AgentLocation, Project};
|
||||
use rand::prelude::*;
|
||||
use rand::rngs::StdRng;
|
||||
@@ -1007,10 +992,7 @@ mod tests {
|
||||
|
||||
model.send_last_completion_stream_text_chunk("<new_text>abX");
|
||||
cx.run_until_parked();
|
||||
assert_matches!(
|
||||
drain_events(&mut events).as_slice(),
|
||||
[EditAgentOutputEvent::Edited(_)]
|
||||
);
|
||||
assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"abXc\ndef\nghi\njkl"
|
||||
@@ -1025,10 +1007,7 @@ mod tests {
|
||||
|
||||
model.send_last_completion_stream_text_chunk("cY");
|
||||
cx.run_until_parked();
|
||||
assert_matches!(
|
||||
drain_events(&mut events).as_slice(),
|
||||
[EditAgentOutputEvent::Edited { .. }]
|
||||
);
|
||||
assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"abXcY\ndef\nghi\njkl"
|
||||
@@ -1139,9 +1118,9 @@ mod tests {
|
||||
|
||||
model.send_last_completion_stream_text_chunk("GHI</new_text>");
|
||||
cx.run_until_parked();
|
||||
assert_matches!(
|
||||
drain_events(&mut events).as_slice(),
|
||||
[EditAgentOutputEvent::Edited { .. }]
|
||||
assert_eq!(
|
||||
drain_events(&mut events),
|
||||
vec![EditAgentOutputEvent::Edited]
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
@@ -1186,9 +1165,9 @@ mod tests {
|
||||
);
|
||||
|
||||
cx.run_until_parked();
|
||||
assert_matches!(
|
||||
drain_events(&mut events).as_slice(),
|
||||
[EditAgentOutputEvent::Edited(_)]
|
||||
assert_eq!(
|
||||
drain_events(&mut events),
|
||||
vec![EditAgentOutputEvent::Edited]
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
@@ -1204,9 +1183,9 @@ mod tests {
|
||||
|
||||
chunks_tx.unbounded_send("```\njkl\n").unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_matches!(
|
||||
drain_events(&mut events).as_slice(),
|
||||
[EditAgentOutputEvent::Edited { .. }]
|
||||
assert_eq!(
|
||||
drain_events(&mut events),
|
||||
vec![EditAgentOutputEvent::Edited]
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
@@ -1222,9 +1201,9 @@ mod tests {
|
||||
|
||||
chunks_tx.unbounded_send("mno\n").unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_matches!(
|
||||
drain_events(&mut events).as_slice(),
|
||||
[EditAgentOutputEvent::Edited { .. }]
|
||||
assert_eq!(
|
||||
drain_events(&mut events),
|
||||
vec![EditAgentOutputEvent::Edited]
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
@@ -1240,9 +1219,9 @@ mod tests {
|
||||
|
||||
chunks_tx.unbounded_send("pqr\n```").unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_matches!(
|
||||
drain_events(&mut events).as_slice(),
|
||||
[EditAgentOutputEvent::Edited(_)],
|
||||
assert_eq!(
|
||||
drain_events(&mut events),
|
||||
vec![EditAgentOutputEvent::Edited]
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
|
||||
@@ -307,7 +307,7 @@ impl Tool for EditFileTool {
|
||||
let mut ambiguous_ranges = Vec::new();
|
||||
while let Some(event) = events.next().await {
|
||||
match event {
|
||||
EditAgentOutputEvent::Edited { .. } => {
|
||||
EditAgentOutputEvent::Edited => {
|
||||
if let Some(card) = card_clone.as_ref() {
|
||||
card.update(cx, |card, cx| card.update_diff(cx))?;
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ impl ToolCard for FindPathToolCard {
|
||||
let workspace_clone = workspace.clone();
|
||||
let button_label = path.to_string_lossy().to_string();
|
||||
|
||||
Button::new(("path", index), button_label, cx)
|
||||
Button::new(("path", index), button_label)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::End)
|
||||
|
||||
@@ -174,7 +174,7 @@ impl ToolCard for WebSearchToolCard {
|
||||
let title = result.title.clone();
|
||||
let url = SharedString::from(result.url.clone());
|
||||
|
||||
Button::new(("result", index), title, cx)
|
||||
Button::new(("result", index), title)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
|
||||
@@ -59,9 +59,16 @@ pub enum VersionCheckType {
|
||||
pub enum AutoUpdateStatus {
|
||||
Idle,
|
||||
Checking,
|
||||
Downloading { version: VersionCheckType },
|
||||
Installing { version: VersionCheckType },
|
||||
Updated { version: VersionCheckType },
|
||||
Downloading {
|
||||
version: VersionCheckType,
|
||||
},
|
||||
Installing {
|
||||
version: VersionCheckType,
|
||||
},
|
||||
Updated {
|
||||
binary_path: PathBuf,
|
||||
version: VersionCheckType,
|
||||
},
|
||||
Errored,
|
||||
}
|
||||
|
||||
@@ -76,7 +83,6 @@ pub struct AutoUpdater {
|
||||
current_version: SemanticVersion,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
pending_poll: Option<Task<Option<()>>>,
|
||||
quit_subscription: Option<gpui::Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
@@ -158,7 +164,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
||||
workspace.register_action(|_, action, window, cx| check(action, window, cx));
|
||||
workspace.register_action(|_, action: &Check, window, cx| check(action, window, cx));
|
||||
|
||||
workspace.register_action(|_, action, _, cx| {
|
||||
view_release_notes(action, cx);
|
||||
@@ -168,7 +174,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
|
||||
let version = release_channel::AppVersion::global(cx);
|
||||
let auto_updater = cx.new(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client, cx);
|
||||
let updater = AutoUpdater::new(version, http_client);
|
||||
|
||||
let poll_for_updates = ReleaseChannel::try_global(cx)
|
||||
.map(|channel| channel.poll_for_updates())
|
||||
@@ -315,34 +321,12 @@ impl AutoUpdater {
|
||||
cx.default_global::<GlobalAutoUpdate>().0.clone()
|
||||
}
|
||||
|
||||
fn new(
|
||||
current_version: SemanticVersion,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
// On windows, executable files cannot be overwritten while they are
|
||||
// running, so we must wait to overwrite the application until quitting
|
||||
// or restarting. When quitting the app, we spawn the auto update helper
|
||||
// to finish the auto update process after Zed exits. When restarting
|
||||
// the app after an update, we use `set_restart_path` to run the auto
|
||||
// update helper instead of the app, so that it can overwrite the app
|
||||
// and then spawn the new binary.
|
||||
let quit_subscription = Some(cx.on_app_quit(|_, _| async move {
|
||||
#[cfg(target_os = "windows")]
|
||||
finalize_auto_update_on_quit();
|
||||
}));
|
||||
|
||||
cx.on_app_restart(|this, _| {
|
||||
this.quit_subscription.take();
|
||||
})
|
||||
.detach();
|
||||
|
||||
fn new(current_version: SemanticVersion, http_client: Arc<HttpClientWithUrl>) -> Self {
|
||||
Self {
|
||||
status: AutoUpdateStatus::Idle,
|
||||
current_version,
|
||||
http_client,
|
||||
pending_poll: None,
|
||||
quit_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,8 +536,6 @@ impl AutoUpdater {
|
||||
)
|
||||
})?;
|
||||
|
||||
Self::check_dependencies()?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Checking;
|
||||
cx.notify();
|
||||
@@ -600,15 +582,13 @@ impl AutoUpdater {
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
let new_binary_path = Self::install_release(installer_dir, target_path, &cx).await?;
|
||||
if let Some(new_binary_path) = new_binary_path {
|
||||
cx.update(|cx| cx.set_restart_path(new_binary_path))?;
|
||||
}
|
||||
let binary_path = Self::binary_path(installer_dir, target_path, &cx).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_should_show_update_notification(true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
this.status = AutoUpdateStatus::Updated {
|
||||
binary_path,
|
||||
version: newer_version,
|
||||
};
|
||||
cx.notify();
|
||||
@@ -659,15 +639,6 @@ impl AutoUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_dependencies() -> Result<()> {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
anyhow::ensure!(
|
||||
which::which("rsync").is_ok(),
|
||||
"Aborting. Could not find rsync which is required for auto-updates."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
|
||||
let filename = match OS {
|
||||
"macos" => anyhow::Ok("Zed.dmg"),
|
||||
@@ -676,14 +647,20 @@ impl AutoUpdater {
|
||||
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
|
||||
}?;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
anyhow::ensure!(
|
||||
which::which("rsync").is_ok(),
|
||||
"Aborting. Could not find rsync which is required for auto-updates."
|
||||
);
|
||||
|
||||
Ok(installer_dir.path().join(filename))
|
||||
}
|
||||
|
||||
async fn install_release(
|
||||
async fn binary_path(
|
||||
installer_dir: InstallerDir,
|
||||
target_path: PathBuf,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
) -> Result<PathBuf> {
|
||||
match OS {
|
||||
"macos" => install_release_macos(&installer_dir, target_path, cx).await,
|
||||
"linux" => install_release_linux(&installer_dir, target_path, cx).await,
|
||||
@@ -824,7 +801,7 @@ async fn install_release_linux(
|
||||
temp_dir: &InstallerDir,
|
||||
downloaded_tar_gz: PathBuf,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
) -> Result<PathBuf> {
|
||||
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
|
||||
let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
|
||||
let running_app_path = cx.update(|cx| cx.app_path())??;
|
||||
@@ -884,14 +861,14 @@ async fn install_release_linux(
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(Some(to.join(expected_suffix)))
|
||||
Ok(to.join(expected_suffix))
|
||||
}
|
||||
|
||||
async fn install_release_macos(
|
||||
temp_dir: &InstallerDir,
|
||||
downloaded_dmg: PathBuf,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
) -> Result<PathBuf> {
|
||||
let running_app_path = cx.update(|cx| cx.app_path())??;
|
||||
let running_app_filename = running_app_path
|
||||
.file_name()
|
||||
@@ -933,10 +910,10 @@ async fn install_release_macos(
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
Ok(running_app_path)
|
||||
}
|
||||
|
||||
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
|
||||
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<PathBuf> {
|
||||
let output = Command::new(downloaded_installer)
|
||||
.arg("/verysilent")
|
||||
.arg("/update=true")
|
||||
@@ -949,36 +926,29 @@ async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option
|
||||
"failed to start installer: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
// We return the path to the update helper program, because it will
|
||||
// perform the final steps of the update process, copying the new binary,
|
||||
// deleting the old one, and launching the new binary.
|
||||
let helper_path = std::env::current_exe()?
|
||||
.parent()
|
||||
.context("No parent dir for Zed.exe")?
|
||||
.join("tools\\auto_update_helper.exe");
|
||||
Ok(Some(helper_path))
|
||||
Ok(std::env::current_exe()?)
|
||||
}
|
||||
|
||||
pub fn finalize_auto_update_on_quit() {
|
||||
pub fn check_pending_installation() -> bool {
|
||||
let Some(installer_path) = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|p| p.join("updates")))
|
||||
else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
// The installer will create a flag file after it finishes updating
|
||||
let flag_file = installer_path.join("versions.txt");
|
||||
if flag_file.exists()
|
||||
&& let Some(helper) = installer_path
|
||||
if flag_file.exists() {
|
||||
if let Some(helper) = installer_path
|
||||
.parent()
|
||||
.map(|p| p.join("tools\\auto_update_helper.exe"))
|
||||
{
|
||||
let mut command = std::process::Command::new(helper);
|
||||
command.arg("--launch");
|
||||
command.arg("false");
|
||||
let _ = command.spawn();
|
||||
{
|
||||
let _ = std::process::Command::new(helper).spawn();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1032,6 +1002,7 @@ mod tests {
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
||||
};
|
||||
let fetched_version = SemanticVersion::new(1, 0, 1);
|
||||
@@ -1053,6 +1024,7 @@ mod tests {
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
||||
};
|
||||
let fetched_version = SemanticVersion::new(1, 0, 2);
|
||||
@@ -1118,6 +1090,7 @@ mod tests {
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||
};
|
||||
let fetched_sha = "b".to_string();
|
||||
@@ -1139,6 +1112,7 @@ mod tests {
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||
};
|
||||
let fetched_sha = "c".to_string();
|
||||
@@ -1186,6 +1160,7 @@ mod tests {
|
||||
let app_commit_sha = Ok(None);
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||
};
|
||||
let fetched_sha = "b".to_string();
|
||||
@@ -1208,6 +1183,7 @@ mod tests {
|
||||
let app_commit_sha = Ok(None);
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||
};
|
||||
let fetched_sha = "c".to_string();
|
||||
|
||||
@@ -37,11 +37,6 @@ mod windows_impl {
|
||||
pub(crate) const WM_JOB_UPDATED: u32 = WM_USER + 1;
|
||||
pub(crate) const WM_TERMINATE: u32 = WM_USER + 2;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Args {
|
||||
launch: Option<bool>,
|
||||
}
|
||||
|
||||
pub(crate) fn run() -> Result<()> {
|
||||
let helper_dir = std::env::current_exe()?
|
||||
.parent()
|
||||
@@ -56,9 +51,8 @@ mod windows_impl {
|
||||
log::info!("======= Starting Zed update =======");
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let hwnd = create_dialog_window(rx)?.0 as isize;
|
||||
let args = parse_args();
|
||||
std::thread::spawn(move || {
|
||||
let result = perform_update(app_dir.as_path(), Some(hwnd), args.launch.unwrap_or(true));
|
||||
let result = perform_update(app_dir.as_path(), Some(hwnd));
|
||||
tx.send(result).ok();
|
||||
unsafe { PostMessageW(Some(HWND(hwnd as _)), WM_TERMINATE, WPARAM(0), LPARAM(0)) }.ok();
|
||||
});
|
||||
@@ -83,41 +77,6 @@ mod windows_impl {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_args() -> Args {
|
||||
let mut result = Args { launch: None };
|
||||
if let Some(candidate) = std::env::args().nth(1) {
|
||||
parse_single_arg(&candidate, &mut result);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn parse_single_arg(arg: &str, result: &mut Args) {
|
||||
let Some((key, value)) = arg.strip_prefix("--").and_then(|arg| arg.split_once('=')) else {
|
||||
log::error!(
|
||||
"Invalid argument format: '{}'. Expected format: --key=value",
|
||||
arg
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
match key {
|
||||
"launch" => parse_launch_arg(value, &mut result.launch),
|
||||
_ => log::error!("Unknown argument: --{}", key),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_launch_arg(value: &str, arg: &mut Option<bool>) {
|
||||
match value {
|
||||
"true" => *arg = Some(true),
|
||||
"false" => *arg = Some(false),
|
||||
_ => log::error!(
|
||||
"Invalid value for --launch: '{}'. Expected 'true' or 'false'",
|
||||
value
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn show_error(mut content: String) {
|
||||
if content.len() > 600 {
|
||||
content.truncate(600);
|
||||
@@ -132,47 +91,4 @@ mod windows_impl {
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::windows_impl::{Args, parse_launch_arg, parse_single_arg};
|
||||
|
||||
#[test]
|
||||
fn test_parse_launch_arg() {
|
||||
let mut arg = None;
|
||||
parse_launch_arg("true", &mut arg);
|
||||
assert_eq!(arg, Some(true));
|
||||
|
||||
let mut arg = None;
|
||||
parse_launch_arg("false", &mut arg);
|
||||
assert_eq!(arg, Some(false));
|
||||
|
||||
let mut arg = None;
|
||||
parse_launch_arg("invalid", &mut arg);
|
||||
assert_eq!(arg, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_single_arg() {
|
||||
let mut args = Args { launch: None };
|
||||
parse_single_arg("--launch=true", &mut args);
|
||||
assert_eq!(args.launch, Some(true));
|
||||
|
||||
let mut args = Args { launch: None };
|
||||
parse_single_arg("--launch=false", &mut args);
|
||||
assert_eq!(args.launch, Some(false));
|
||||
|
||||
let mut args = Args { launch: None };
|
||||
parse_single_arg("--launch=invalid", &mut args);
|
||||
assert_eq!(args.launch, None);
|
||||
|
||||
let mut args = Args { launch: None };
|
||||
parse_single_arg("--launch", &mut args);
|
||||
assert_eq!(args.launch, None);
|
||||
|
||||
let mut args = Args { launch: None };
|
||||
parse_single_arg("--unknown", &mut args);
|
||||
assert_eq!(args.launch, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ pub(crate) fn create_dialog_window(receiver: Receiver<Result<()>>) -> Result<HWN
|
||||
let hwnd = CreateWindowExW(
|
||||
WS_EX_TOPMOST,
|
||||
class_name,
|
||||
windows::core::w!("Zed"),
|
||||
windows::core::w!("Zed Editor"),
|
||||
WS_VISIBLE | WS_POPUP | WS_CAPTION,
|
||||
rect.right / 2 - width / 2,
|
||||
rect.bottom / 2 - height / 2,
|
||||
@@ -171,7 +171,7 @@ unsafe extern "system" fn wnd_proc(
|
||||
&HSTRING::from(font_name),
|
||||
);
|
||||
let temp = SelectObject(hdc, font.into());
|
||||
let string = HSTRING::from("Updating Zed...");
|
||||
let string = HSTRING::from("Zed Editor is updating...");
|
||||
return_if_failed!(TextOutW(hdc, 20, 15, &string).ok());
|
||||
return_if_failed!(DeleteObject(temp).ok());
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ pub(crate) const JOBS: [Job; 2] = [
|
||||
},
|
||||
];
|
||||
|
||||
pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool) -> Result<()> {
|
||||
pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>) -> Result<()> {
|
||||
let hwnd = hwnd.map(|ptr| HWND(ptr as _));
|
||||
|
||||
for job in JOBS.iter() {
|
||||
@@ -145,11 +145,9 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool)
|
||||
}
|
||||
}
|
||||
}
|
||||
if launch {
|
||||
let _ = std::process::Command::new(app_dir.join("Zed.exe"))
|
||||
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
|
||||
.spawn();
|
||||
}
|
||||
let _ = std::process::Command::new(app_dir.join("Zed.exe"))
|
||||
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
|
||||
.spawn();
|
||||
log::info!("Update completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
@@ -161,11 +159,11 @@ mod test {
|
||||
#[test]
|
||||
fn test_perform_update() {
|
||||
let app_dir = std::path::Path::new("C:/");
|
||||
assert!(perform_update(app_dir, None, false).is_ok());
|
||||
assert!(perform_update(app_dir, None).is_ok());
|
||||
|
||||
// Simulate a timeout
|
||||
unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err") };
|
||||
let ret = perform_update(app_dir, None, false);
|
||||
let ret = perform_update(app_dir, None);
|
||||
assert!(ret.is_err_and(|e| e.to_string().as_str() == "Timed out"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -957,14 +957,17 @@ mod mac_os {
|
||||
) -> Result<()> {
|
||||
use anyhow::bail;
|
||||
|
||||
let app_path_prompt = format!(
|
||||
"POSIX path of (path to application \"{}\")",
|
||||
channel.display_name()
|
||||
);
|
||||
let app_path_output = Command::new("osascript")
|
||||
let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
|
||||
let app_id_output = Command::new("osascript")
|
||||
.arg("-e")
|
||||
.arg(&app_path_prompt)
|
||||
.arg(&app_id_prompt)
|
||||
.output()?;
|
||||
if !app_id_output.status.success() {
|
||||
bail!("Could not determine app id for {}", channel.display_name());
|
||||
}
|
||||
let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
|
||||
let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
|
||||
let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
|
||||
if !app_path_output.status.success() {
|
||||
bail!(
|
||||
"Could not determine app path for {}",
|
||||
|
||||
@@ -340,35 +340,22 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn log_edit_event(self: &Arc<Self>, environment: &'static str, is_via_ssh: bool) {
|
||||
static LAST_EVENT_TIME: Mutex<Option<Instant>> = Mutex::new(None);
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let period_data = state.event_coalescer.log_event(environment);
|
||||
drop(state);
|
||||
|
||||
if let Some(mut last_event) = LAST_EVENT_TIME.try_lock() {
|
||||
let current_time = std::time::Instant::now();
|
||||
let last_time = last_event.get_or_insert(current_time);
|
||||
if let Some((start, end, environment)) = period_data {
|
||||
let duration = end
|
||||
.saturating_duration_since(start)
|
||||
.min(Duration::from_secs(60 * 60 * 24))
|
||||
.as_millis() as i64;
|
||||
|
||||
if current_time.duration_since(*last_time) > Duration::from_secs(60 * 10) {
|
||||
*last_time = current_time;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some((start, end, environment)) = period_data {
|
||||
let duration = end
|
||||
.saturating_duration_since(start)
|
||||
.min(Duration::from_secs(60 * 60 * 24))
|
||||
.as_millis() as i64;
|
||||
|
||||
telemetry::event!(
|
||||
"Editor Edited",
|
||||
duration = duration,
|
||||
environment = environment,
|
||||
is_via_ssh = is_via_ssh
|
||||
);
|
||||
}
|
||||
telemetry::event!(
|
||||
"Editor Edited",
|
||||
duration = duration,
|
||||
environment = environment,
|
||||
is_via_ssh = is_via_ssh
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -987,7 +987,7 @@ impl Render for ChatPanel {
|
||||
)
|
||||
.child(
|
||||
div().pt_1().w_full().items_center().child(
|
||||
Button::new("toggle-collab", "Open", cx)
|
||||
Button::new("toggle-collab", "Open")
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action(
|
||||
&collab_panel::ToggleFocus,
|
||||
|
||||
@@ -2307,7 +2307,7 @@ impl CollabPanel {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("sign_in", "Sign in", cx)
|
||||
Button::new("sign_in", "Sign in")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
|
||||
@@ -177,7 +177,7 @@ impl Render for ChannelModal {
|
||||
))
|
||||
.children(
|
||||
Some(
|
||||
Button::new("copy-link", "Copy Link", cx)
|
||||
Button::new("copy-link", "Copy Link")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
if let Some(channel) = this
|
||||
|
||||
@@ -319,22 +319,20 @@ impl NotificationPanel {
|
||||
h_flex()
|
||||
.flex_grow()
|
||||
.justify_end()
|
||||
.child(Button::new("decline", "Decline", cx).on_click(
|
||||
{
|
||||
let notification = notification.clone();
|
||||
let entity = cx.entity().clone();
|
||||
move |_, _, cx| {
|
||||
entity.update(cx, |this, cx| {
|
||||
this.respond_to_notification(
|
||||
notification.clone(),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
.child(Button::new("accept", "Accept", cx).on_click({
|
||||
.child(Button::new("decline", "Decline").on_click({
|
||||
let notification = notification.clone();
|
||||
let entity = cx.entity().clone();
|
||||
move |_, _, cx| {
|
||||
entity.update(cx, |this, cx| {
|
||||
this.respond_to_notification(
|
||||
notification.clone(),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}))
|
||||
.child(Button::new("accept", "Accept").on_click({
|
||||
let notification = notification.clone();
|
||||
let entity = cx.entity().clone();
|
||||
move |_, _, cx| {
|
||||
@@ -633,7 +631,7 @@ impl Render for NotificationPanel {
|
||||
.gap_2()
|
||||
.p_4()
|
||||
.child(
|
||||
Button::new("connect_prompt_button", "Connect", cx)
|
||||
Button::new("connect_prompt_button", "Connect")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
|
||||
@@ -117,11 +117,11 @@ impl Render for IncomingCallNotification {
|
||||
div().size_full().font(ui_font).child(
|
||||
CollabNotification::new(
|
||||
self.state.call.calling_user.avatar_uri.clone(),
|
||||
Button::new("accept", "Accept", cx).on_click({
|
||||
Button::new("accept", "Accept").on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, _, cx| state.respond(true, cx)
|
||||
}),
|
||||
Button::new("decline", "Decline", cx).on_click({
|
||||
Button::new("decline", "Decline").on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, _, cx| state.respond(false, cx)
|
||||
}),
|
||||
|
||||
@@ -126,12 +126,10 @@ impl Render for ProjectSharedNotification {
|
||||
div().size_full().font(ui_font).child(
|
||||
CollabNotification::new(
|
||||
self.owner.avatar_uri.clone(),
|
||||
Button::new("open", "Open", cx).on_click(cx.listener(
|
||||
move |this, _event, _, cx| {
|
||||
this.join(cx);
|
||||
},
|
||||
)),
|
||||
Button::new("dismiss", "Dismiss", cx).on_click(cx.listener(
|
||||
Button::new("open", "Open").on_click(cx.listener(move |this, _event, _, cx| {
|
||||
this.join(cx);
|
||||
})),
|
||||
Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
move |this, _event, _, cx| {
|
||||
this.dismiss(cx);
|
||||
},
|
||||
|
||||
@@ -18,8 +18,8 @@ impl Render for CollabNotificationStory {
|
||||
window_container(400., 72.).child(
|
||||
CollabNotification::new(
|
||||
"https://avatars.githubusercontent.com/u/1486634?v=4",
|
||||
Button::new("accept", "Accept", cx),
|
||||
Button::new("decline", "Decline", cx),
|
||||
Button::new("accept", "Accept"),
|
||||
Button::new("decline", "Decline"),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -35,8 +35,8 @@ impl Render for CollabNotificationStory {
|
||||
window_container(400., 72.).child(
|
||||
CollabNotification::new(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
Button::new("open", "Open", cx),
|
||||
Button::new("dismiss", "Dismiss", cx),
|
||||
Button::new("open", "Open"),
|
||||
Button::new("dismiss", "Dismiss"),
|
||||
)
|
||||
.child(Label::new("iamnbutler"))
|
||||
.child(Label::new("is sharing a project in Zed:"))
|
||||
|
||||
@@ -264,7 +264,7 @@ impl CopilotCodeVerification {
|
||||
.size(ui::LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Button::new("connect-button", connect_button_label, cx)
|
||||
Button::new("connect-button", connect_button_label)
|
||||
.on_click({
|
||||
let verification_uri = data.verification_uri.clone();
|
||||
cx.listener(move |this, _, _window, cx| {
|
||||
@@ -276,7 +276,7 @@ impl CopilotCodeVerification {
|
||||
.style(ButtonStyle::Filled),
|
||||
)
|
||||
.child(
|
||||
Button::new("copilot-enable-cancel-button", "Cancel", cx)
|
||||
Button::new("copilot-enable-cancel-button", "Cancel")
|
||||
.full_width()
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
@@ -292,7 +292,7 @@ impl CopilotCodeVerification {
|
||||
"You can update your settings or sign out from the Copilot menu in the status bar.",
|
||||
))
|
||||
.child(
|
||||
Button::new("copilot-enabled-done-button", "Done", cx)
|
||||
Button::new("copilot-enabled-done-button", "Done")
|
||||
.full_width()
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||
)
|
||||
@@ -306,12 +306,12 @@ impl CopilotCodeVerification {
|
||||
"You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
|
||||
).color(Color::Warning))
|
||||
.child(
|
||||
Button::new("copilot-subscribe-button", "Subscribe on GitHub", cx)
|
||||
Button::new("copilot-subscribe-button", "Subscribe on GitHub")
|
||||
.full_width()
|
||||
.on_click(|_, _, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
|
||||
)
|
||||
.child(
|
||||
Button::new("copilot-subscribe-cancel-button", "Cancel", cx)
|
||||
Button::new("copilot-subscribe-cancel-button", "Cancel")
|
||||
.full_width()
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||
)
|
||||
|
||||
@@ -535,7 +535,6 @@ impl Render for DapLogToolbarItemView {
|
||||
))
|
||||
})
|
||||
.unwrap_or_else(|| "No adapter selected".into()),
|
||||
cx,
|
||||
))
|
||||
.menu(move |mut window, cx| {
|
||||
let log_view = log_view.clone();
|
||||
@@ -633,7 +632,7 @@ impl Render for DapLogToolbarItemView {
|
||||
.child(
|
||||
div()
|
||||
.child(
|
||||
Button::new("clear_log_button", "Clear", cx).on_click(cx.listener(
|
||||
Button::new("clear_log_button", "Clear").on_click(cx.listener(
|
||||
|this, _, window, cx| {
|
||||
if let Some(log_view) = this.log_view.as_ref() {
|
||||
log_view.update(cx, |log_view, cx| {
|
||||
|
||||
@@ -1715,7 +1715,7 @@ impl Render for DebugPanel {
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("spawn-new-session-empty-state", "New Session", cx)
|
||||
Button::new("spawn-new-session-empty-state", "New Session")
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -1725,7 +1725,7 @@ impl Render for DebugPanel {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("edit-debug-settings", "Edit debug.json", cx)
|
||||
Button::new("edit-debug-settings", "Edit debug.json")
|
||||
.icon(IconName::Code)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
@@ -1739,7 +1739,7 @@ impl Render for DebugPanel {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("open-debugger-docs", "Debugger Docs", cx)
|
||||
Button::new("open-debugger-docs", "Debugger Docs")
|
||||
.icon(IconName::Book)
|
||||
.color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -1751,7 +1751,6 @@ impl Render for DebugPanel {
|
||||
Button::new(
|
||||
"spawn-new-session-install-extensions",
|
||||
"Debugger Extensions",
|
||||
cx,
|
||||
)
|
||||
.icon(IconName::Blocks)
|
||||
.color(Color::Muted)
|
||||
|
||||
@@ -703,7 +703,7 @@ impl Render for NewProcessModal {
|
||||
container
|
||||
.child(
|
||||
h_flex().child(
|
||||
Button::new("edit-custom-debug", "Edit in debug.json", cx)
|
||||
Button::new("edit-custom-debug", "Edit in debug.json")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.save_debug_scenario(window, cx);
|
||||
}))
|
||||
@@ -719,7 +719,7 @@ impl Render for NewProcessModal {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start", cx)
|
||||
Button::new("debugger-spawn", "Start")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.start_new_session(window, cx)
|
||||
}))
|
||||
@@ -751,7 +751,7 @@ impl Render for NewProcessModal {
|
||||
.child(div().children(
|
||||
KeyBinding::for_action(&*secondary_action, window, cx).map(
|
||||
|keybind| {
|
||||
Button::new("edit-attach-task", "Edit in debug.json", cx)
|
||||
Button::new("edit-attach-task", "Edit in debug.json")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(keybind)
|
||||
.on_click(move |_, window, cx| {
|
||||
@@ -1389,7 +1389,7 @@ impl PickerDelegate for DebugDelegate {
|
||||
.children({
|
||||
let action = menu::SecondaryConfirm.boxed_clone();
|
||||
KeyBinding::for_action(&*action, window, cx).map(|keybind| {
|
||||
Button::new("edit-debug-task", "Edit in debug.json", cx)
|
||||
Button::new("edit-debug-task", "Edit in debug.json")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(keybind)
|
||||
.on_click(move |_, window, cx| {
|
||||
@@ -1401,7 +1401,7 @@ impl PickerDelegate for DebugDelegate {
|
||||
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
|
||||
let action = picker::ConfirmInput { secondary: false }.boxed_clone();
|
||||
this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
|
||||
Button::new("launch-custom", "Launch Custom", cx)
|
||||
Button::new("launch-custom", "Launch Custom")
|
||||
.key_binding(keybind)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(action.boxed_clone(), cx)
|
||||
@@ -1415,7 +1415,7 @@ impl PickerDelegate for DebugDelegate {
|
||||
let run_entry_label =
|
||||
if is_recent_selected { "Rerun" } else { "Spawn" };
|
||||
|
||||
Button::new("spawn", run_entry_label, cx)
|
||||
Button::new("spawn", run_entry_label)
|
||||
.key_binding(keybind)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(menu::Confirm.boxed_clone(), cx);
|
||||
|
||||
@@ -139,13 +139,13 @@ impl Render for DebuggerOnboardingModal {
|
||||
)),
|
||||
));
|
||||
|
||||
let open_panel_button = Button::new("open-panel", "Get Started with the Debugger", cx)
|
||||
let open_panel_button = Button::new("open-panel", "Get Started with the Debugger")
|
||||
.icon_size(IconSize::Indicator)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::open_panel));
|
||||
|
||||
let blog_post_button = Button::new("view-blog", "Check out the Blog Post", cx)
|
||||
let blog_post_button = Button::new("view-blog", "Check out the Blog Post")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
|
||||
@@ -128,12 +128,12 @@ impl Render for ProjectDiagnosticsEditor {
|
||||
self.summary.warning_count, plural_suffix
|
||||
);
|
||||
this.child(
|
||||
Button::new("diagnostics-show-warning-label", label, cx).on_click(
|
||||
cx.listener(|this, _, window, cx| {
|
||||
Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
|
||||
|this, _, window, cx| {
|
||||
this.toggle_warnings(&Default::default(), window, cx);
|
||||
cx.notify();
|
||||
}),
|
||||
),
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -74,7 +74,7 @@ impl Render for DiagnosticIndicator {
|
||||
let status = if let Some(diagnostic) = &self.current_diagnostic {
|
||||
let message = diagnostic.message.split('\n').next().unwrap().to_string();
|
||||
Some(
|
||||
Button::new("diagnostic_message", message, cx)
|
||||
Button::new("diagnostic_message", message)
|
||||
.label_size(LabelSize::Small)
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::for_action(
|
||||
|
||||
@@ -250,24 +250,6 @@ pub type RenderDiffHunkControlsFn = Arc<
|
||||
) -> AnyElement,
|
||||
>;
|
||||
|
||||
enum ReportEditorEvent {
|
||||
Saved { auto_saved: bool },
|
||||
EditorOpened,
|
||||
ZetaTosClicked,
|
||||
Closed,
|
||||
}
|
||||
|
||||
impl ReportEditorEvent {
|
||||
pub fn event_type(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Saved { .. } => "Editor Saved",
|
||||
Self::EditorOpened => "Editor Opened",
|
||||
Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked",
|
||||
Self::Closed => "Editor Closed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InlineValueCache {
|
||||
enabled: bool,
|
||||
inlays: Vec<InlayId>,
|
||||
@@ -2343,7 +2325,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
if editor.mode.is_full() {
|
||||
editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx);
|
||||
editor.report_editor_event("Editor Opened", None, cx);
|
||||
}
|
||||
|
||||
editor
|
||||
@@ -9142,7 +9124,7 @@ impl Editor {
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
cx.stop_propagation();
|
||||
this.report_editor_event(ReportEditorEvent::ZetaTosClicked, None, cx);
|
||||
this.report_editor_event("Edit Prediction Provider ToS Clicked", None, cx);
|
||||
window.dispatch_action(
|
||||
zed_actions::OpenZedPredictOnboarding.boxed_clone(),
|
||||
cx,
|
||||
@@ -20565,7 +20547,7 @@ impl Editor {
|
||||
|
||||
fn report_editor_event(
|
||||
&self,
|
||||
reported_event: ReportEditorEvent,
|
||||
event_type: &'static str,
|
||||
file_extension: Option<String>,
|
||||
cx: &App,
|
||||
) {
|
||||
@@ -20599,30 +20581,15 @@ impl Editor {
|
||||
.show_edit_predictions;
|
||||
|
||||
let project = project.read(cx);
|
||||
let event_type = reported_event.event_type();
|
||||
|
||||
if let ReportEditorEvent::Saved { auto_saved } = reported_event {
|
||||
telemetry::event!(
|
||||
event_type,
|
||||
type = if auto_saved {"autosave"} else {"manual"},
|
||||
file_extension,
|
||||
vim_mode,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
edit_predictions_provider,
|
||||
is_via_ssh = project.is_via_ssh(),
|
||||
);
|
||||
} else {
|
||||
telemetry::event!(
|
||||
event_type,
|
||||
file_extension,
|
||||
vim_mode,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
edit_predictions_provider,
|
||||
is_via_ssh = project.is_via_ssh(),
|
||||
);
|
||||
};
|
||||
telemetry::event!(
|
||||
event_type,
|
||||
file_extension,
|
||||
vim_mode,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
edit_predictions_provider,
|
||||
is_via_ssh = project.is_via_ssh(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
|
||||
@@ -23754,10 +23721,10 @@ impl Render for MissingEditPredictionKeybindingTooltip {
|
||||
.gap_1()
|
||||
.items_end()
|
||||
.w_full()
|
||||
.child(Button::new("see-key-binding", "See Keybinding", cx).size(ButtonSize::Compact).on_click(|_ev, window, cx| {
|
||||
.child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| {
|
||||
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx)
|
||||
}))
|
||||
.child(Button::new("learn-more", "Learn More", cx).size(ButtonSize::Compact).on_click(|_ev, _window, cx| {
|
||||
.child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| {
|
||||
cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding");
|
||||
})),
|
||||
)
|
||||
@@ -23804,7 +23771,7 @@ fn render_diff_hunk_controls(
|
||||
.block_mouse_except_scroll()
|
||||
.shadow_md()
|
||||
.child(if status.has_secondary_hunk() {
|
||||
Button::new(("stage", row as u64), "Stage", cx)
|
||||
Button::new(("stage", row as u64), "Stage")
|
||||
.alpha(if status.is_pending() { 0.66 } else { 1.0 })
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
@@ -23831,7 +23798,7 @@ fn render_diff_hunk_controls(
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Button::new(("unstage", row as u64), "Unstage", cx)
|
||||
Button::new(("unstage", row as u64), "Unstage")
|
||||
.alpha(if status.is_pending() { 0.66 } else { 1.0 })
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
@@ -23859,7 +23826,7 @@ fn render_diff_hunk_controls(
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Button::new(("restore", row as u64), "Restore", cx)
|
||||
Button::new(("restore", row as u64), "Restore")
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |window, cx| {
|
||||
|
||||
@@ -22456,7 +22456,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
cx.update(|_, cx| {
|
||||
workspace::reload(cx);
|
||||
workspace::reload(&workspace::Reload::default(), cx);
|
||||
});
|
||||
assert_language_servers_count(
|
||||
1,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget,
|
||||
MultiBuffer, MultiBufferSnapshot, NavigationData, ReportEditorEvent, SearchWithinRange,
|
||||
SelectionEffects, ToPoint as _,
|
||||
MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, SelectionEffects,
|
||||
ToPoint as _,
|
||||
display_map::HighlightKey,
|
||||
editor_settings::SeedQuerySetting,
|
||||
persistence::{DB, SerializedEditor},
|
||||
@@ -776,10 +776,6 @@ impl Item for Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_removed(&self, cx: &App) {
|
||||
self.report_editor_event(ReportEditorEvent::Closed, None, cx);
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let selection = self.selections.newest_anchor();
|
||||
self.push_to_nav_history(selection.head(), None, true, false, cx);
|
||||
@@ -819,9 +815,9 @@ impl Item for Editor {
|
||||
) -> Task<Result<()>> {
|
||||
// Add meta data tracking # of auto saves
|
||||
if options.autosave {
|
||||
self.report_editor_event(ReportEditorEvent::Saved { auto_saved: true }, None, cx);
|
||||
self.report_editor_event("Editor Autosaved", None, cx);
|
||||
} else {
|
||||
self.report_editor_event(ReportEditorEvent::Saved { auto_saved: false }, None, cx);
|
||||
self.report_editor_event("Editor Saved", None, cx);
|
||||
}
|
||||
|
||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||
@@ -900,11 +896,7 @@ impl Item for Editor {
|
||||
.path
|
||||
.extension()
|
||||
.map(|a| a.to_string_lossy().to_string());
|
||||
self.report_editor_event(
|
||||
ReportEditorEvent::Saved { auto_saved: false },
|
||||
file_extension,
|
||||
cx,
|
||||
);
|
||||
self.report_editor_event("Editor Saved", file_extension, cx);
|
||||
|
||||
project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
|
||||
}
|
||||
@@ -1005,16 +997,12 @@ impl Item for Editor {
|
||||
) {
|
||||
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
|
||||
if let Some(workspace) = &workspace.weak_handle().upgrade() {
|
||||
cx.subscribe(
|
||||
&workspace,
|
||||
|editor, _, event: &workspace::Event, _cx| match event {
|
||||
workspace::Event::ModalOpened => {
|
||||
editor.mouse_context_menu.take();
|
||||
editor.inline_blame_popover.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
cx.subscribe(&workspace, |editor, _, event: &workspace::Event, _cx| {
|
||||
if matches!(event, workspace::Event::ModalOpened) {
|
||||
editor.mouse_context_menu.take();
|
||||
editor.inline_blame_popover.take();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1118,17 +1118,15 @@ impl ExtensionStore {
|
||||
extensions_to_unload.len() - reload_count
|
||||
);
|
||||
|
||||
let extension_ids = extensions_to_load
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
Some((
|
||||
id.clone(),
|
||||
new_index.extensions.get(id)?.manifest.version.clone(),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
telemetry::event!("Extensions Loaded", id_and_versions = extension_ids);
|
||||
for extension_id in &extensions_to_load {
|
||||
if let Some(extension) = new_index.extensions.get(extension_id) {
|
||||
telemetry::event!(
|
||||
"Extension Loaded",
|
||||
extension_id,
|
||||
version = extension.manifest.version
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let themes_to_remove = old_index
|
||||
.themes
|
||||
|
||||
@@ -56,7 +56,7 @@ impl RenderOnce for FeatureUpsell {
|
||||
self.docs_url,
|
||||
|el, docs_url| {
|
||||
el.child(
|
||||
Button::new("open_docs", "View Documentation", cx)
|
||||
Button::new("open_docs", "View Documentation")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::End)
|
||||
|
||||
@@ -596,7 +596,6 @@ impl ExtensionsPage {
|
||||
Button::new(
|
||||
SharedString::from(format!("rebuild-{}", extension.id)),
|
||||
"Rebuild",
|
||||
cx,
|
||||
)
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Upgrading))
|
||||
@@ -610,7 +609,7 @@ impl ExtensionsPage {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new(SharedString::from(extension.id.clone()), "Uninstall", cx)
|
||||
Button::new(SharedString::from(extension.id.clone()), "Uninstall")
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Removing))
|
||||
.on_click({
|
||||
@@ -627,7 +626,6 @@ impl ExtensionsPage {
|
||||
Button::new(
|
||||
SharedString::from(format!("configure-{}", extension.id)),
|
||||
"Configure",
|
||||
cx,
|
||||
)
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Installing))
|
||||
@@ -949,7 +947,6 @@ impl ExtensionsPage {
|
||||
install_or_uninstall: Button::new(
|
||||
SharedString::from(extension.id.clone()),
|
||||
"Install",
|
||||
cx,
|
||||
),
|
||||
configure: None,
|
||||
upgrade: None,
|
||||
@@ -966,7 +963,6 @@ impl ExtensionsPage {
|
||||
install_or_uninstall: Button::new(
|
||||
SharedString::from(extension.id.clone()),
|
||||
"Install",
|
||||
cx,
|
||||
)
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
@@ -984,7 +980,6 @@ impl ExtensionsPage {
|
||||
install_or_uninstall: Button::new(
|
||||
SharedString::from(extension.id.clone()),
|
||||
"Install",
|
||||
cx,
|
||||
)
|
||||
.disabled(true),
|
||||
configure: None,
|
||||
@@ -994,27 +989,23 @@ impl ExtensionsPage {
|
||||
install_or_uninstall: Button::new(
|
||||
SharedString::from(extension.id.clone()),
|
||||
"Uninstall",
|
||||
cx,
|
||||
)
|
||||
.disabled(true),
|
||||
configure: is_configurable.then(|| {
|
||||
Button::new(
|
||||
SharedString::from(format!("configure-{}", extension.id)),
|
||||
"Configure",
|
||||
cx,
|
||||
)
|
||||
.disabled(true)
|
||||
}),
|
||||
upgrade: Some(
|
||||
Button::new(SharedString::from(extension.id.clone()), "Upgrade", cx)
|
||||
.disabled(true),
|
||||
Button::new(SharedString::from(extension.id.clone()), "Upgrade").disabled(true),
|
||||
),
|
||||
},
|
||||
ExtensionStatus::Installed(installed_version) => ExtensionCardButtons {
|
||||
install_or_uninstall: Button::new(
|
||||
SharedString::from(extension.id.clone()),
|
||||
"Uninstall",
|
||||
cx,
|
||||
)
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
@@ -1031,7 +1022,6 @@ impl ExtensionsPage {
|
||||
Button::new(
|
||||
SharedString::from(format!("configure-{}", extension.id)),
|
||||
"Configure",
|
||||
cx,
|
||||
)
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
@@ -1057,7 +1047,7 @@ impl ExtensionsPage {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Button::new(SharedString::from(extension.id.clone()), "Upgrade", cx)
|
||||
Button::new(SharedString::from(extension.id.clone()), "Upgrade")
|
||||
.when(!is_compatible, |upgrade_button| {
|
||||
upgrade_button.disabled(true).tooltip({
|
||||
let version = extension.manifest.version.clone();
|
||||
@@ -1095,14 +1085,12 @@ impl ExtensionsPage {
|
||||
install_or_uninstall: Button::new(
|
||||
SharedString::from(extension.id.clone()),
|
||||
"Uninstall",
|
||||
cx,
|
||||
)
|
||||
.disabled(true),
|
||||
configure: is_configurable.then(|| {
|
||||
Button::new(
|
||||
SharedString::from(format!("configure-{}", extension.id)),
|
||||
"Configure",
|
||||
cx,
|
||||
)
|
||||
.disabled(true)
|
||||
}),
|
||||
@@ -1406,7 +1394,7 @@ impl Render for ExtensionsPage {
|
||||
.justify_between()
|
||||
.child(Headline::new("Extensions").size(HeadlineSize::XLarge))
|
||||
.child(
|
||||
Button::new("install-dev-extension", "Install Dev Extension", cx)
|
||||
Button::new("install-dev-extension", "Install Dev Extension")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.on_click(|_event, window, cx| {
|
||||
@@ -1482,7 +1470,7 @@ impl Render for ExtensionsPage {
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
Button::new("filter-all-categories", "All", cx)
|
||||
Button::new("filter-all-categories", "All")
|
||||
.when(self.provides_filter.is_none(), |button| {
|
||||
button.style(ButtonStyle::Filled)
|
||||
})
|
||||
@@ -1505,7 +1493,7 @@ impl Render for ExtensionsPage {
|
||||
let button_id = SharedString::from(format!("filter-category-{}", label));
|
||||
|
||||
Some(
|
||||
Button::new(button_id, label, cx)
|
||||
Button::new(button_id, label)
|
||||
.style(if self.provides_filter == Some(provides) {
|
||||
ButtonStyle::Filled
|
||||
} else {
|
||||
|
||||
@@ -68,7 +68,7 @@ impl Render for FeedbackModal {
|
||||
)
|
||||
.child(Label::new("Thanks for using Zed! To share your experience with us, reach for the channel that's the most appropriate:"))
|
||||
.child(
|
||||
Button::new("file-a-bug-report", "File a Bug Report", cx)
|
||||
Button::new("file-a-bug-report", "File a Bug Report")
|
||||
.full_width()
|
||||
.icon(IconName::Debug)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -79,7 +79,7 @@ impl Render for FeedbackModal {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("request-a-feature", "Request a Feature", cx)
|
||||
Button::new("request-a-feature", "Request a Feature")
|
||||
.full_width()
|
||||
.icon(IconName::Sparkle)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -90,7 +90,7 @@ impl Render for FeedbackModal {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("send-us_an-email", "Send an Email", cx)
|
||||
Button::new("send-us_an-email", "Send an Email")
|
||||
.full_width()
|
||||
.icon(IconName::Envelope)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -101,7 +101,7 @@ impl Render for FeedbackModal {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("zed_repository", "GitHub Repository", cx)
|
||||
Button::new("zed_repository", "GitHub Repository")
|
||||
.full_width()
|
||||
.icon(IconName::Github)
|
||||
.icon_size(IconSize::XSmall)
|
||||
|
||||
@@ -1779,7 +1779,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("open-selection", "Open", cx)
|
||||
Button::new("open-selection", "Open")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
|
||||
@@ -73,7 +73,6 @@ async fn run_git_blame(
|
||||
.current_dir(working_directory)
|
||||
.arg("blame")
|
||||
.arg("--incremental")
|
||||
.arg("-w")
|
||||
.arg("--contents")
|
||||
.arg("-")
|
||||
.arg(path.as_os_str())
|
||||
|
||||
@@ -301,7 +301,6 @@ impl BlameRenderer for GitBlameRenderer {
|
||||
Button::new(
|
||||
"pull-request-button",
|
||||
format!("#{}", pr.number),
|
||||
cx,
|
||||
)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::PullRequest)
|
||||
@@ -319,7 +318,6 @@ impl BlameRenderer for GitBlameRenderer {
|
||||
Button::new(
|
||||
"commit-sha-button",
|
||||
short_commit_id.clone(),
|
||||
cx,
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Muted)
|
||||
|
||||
@@ -363,7 +363,7 @@ impl CommitModal {
|
||||
.map(|b| b.name().to_owned())
|
||||
.unwrap_or_else(|| "<no branch>".to_owned());
|
||||
|
||||
let branch_picker_button = panel_button(branch, cx)
|
||||
let branch_picker_button = panel_button(branch)
|
||||
.icon(IconName::GitBranch)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Placeholder)
|
||||
|
||||
@@ -283,7 +283,6 @@ impl Render for CommitTooltip {
|
||||
Button::new(
|
||||
"pull-request-button",
|
||||
format!("#{}", pr.number),
|
||||
cx,
|
||||
)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::PullRequest)
|
||||
@@ -301,7 +300,6 @@ impl Render for CommitTooltip {
|
||||
Button::new(
|
||||
"commit-sha-button",
|
||||
short_commit_id.clone(),
|
||||
cx,
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Muted)
|
||||
|
||||
@@ -394,7 +394,7 @@ fn render_conflict_buttons(
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
Button::new("head", "Use HEAD", cx)
|
||||
Button::new("head", "Use HEAD")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
@@ -414,7 +414,7 @@ fn render_conflict_buttons(
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("origin", "Use Origin", cx)
|
||||
Button::new("origin", "Use Origin")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
@@ -434,7 +434,7 @@ fn render_conflict_buttons(
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("both", "Use Both", cx)
|
||||
Button::new("both", "Use Both")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
|
||||
@@ -2105,7 +2105,7 @@ impl GitPanel {
|
||||
Ok(_) => cx.update(|window, cx| {
|
||||
window.prompt(
|
||||
PromptLevel::Info,
|
||||
&format!("Git Clone: {}", repo_name),
|
||||
"Git Clone",
|
||||
None,
|
||||
&["Add repo to project", "Open repo in new project"],
|
||||
cx,
|
||||
@@ -3327,7 +3327,7 @@ impl GitPanel {
|
||||
.px_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
panel_button(change_string, cx)
|
||||
panel_button(change_string)
|
||||
.color(Color::Muted)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Open Diff",
|
||||
@@ -3345,7 +3345,7 @@ impl GitPanel {
|
||||
.gap_1()
|
||||
.child(self.render_overflow_menu("overflow_menu"))
|
||||
.child(
|
||||
panel_filled_button(text, cx)
|
||||
panel_filled_button(text)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
tooltip,
|
||||
action.as_ref(),
|
||||
@@ -3604,7 +3604,7 @@ impl GitPanel {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
panel_button("Cancel", cx)
|
||||
panel_button("Cancel")
|
||||
.size(ButtonSize::Default)
|
||||
.on_click(cx.listener(|this, _, _, cx| this.set_amend_pending(false, cx))),
|
||||
)
|
||||
@@ -3703,7 +3703,7 @@ impl GitPanel {
|
||||
let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
|
||||
(worktree_count > 0 && self.active_repository.is_none()).then(|| {
|
||||
h_flex().w_full().justify_around().child(
|
||||
panel_filled_button("Initialize Repository", cx)
|
||||
panel_filled_button("Initialize Repository")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"git init",
|
||||
&git::Init,
|
||||
@@ -4841,7 +4841,7 @@ impl RenderOnce for PanelRepoFooter {
|
||||
util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len)
|
||||
};
|
||||
|
||||
let repo_selector_trigger = Button::new("repo-selector", truncated_repo_name, cx)
|
||||
let repo_selector_trigger = Button::new("repo-selector", truncated_repo_name)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.size(ButtonSize::None)
|
||||
.label_size(LabelSize::Small)
|
||||
@@ -4862,7 +4862,7 @@ impl RenderOnce for PanelRepoFooter {
|
||||
.anchor(Corner::BottomLeft)
|
||||
.into_any_element();
|
||||
|
||||
let branch_selector_button = Button::new("branch-selector", truncated_branch_name, cx)
|
||||
let branch_selector_button = Button::new("branch-selector", truncated_branch_name)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.size(ButtonSize::None)
|
||||
.label_size(LabelSize::Small)
|
||||
|
||||
@@ -181,6 +181,10 @@ pub fn init(cx: &mut App) {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
GitCloneModal::show(panel, window, cx)
|
||||
});
|
||||
|
||||
// panel.update(cx, |panel, cx| {
|
||||
// panel.git_clone(window, cx);
|
||||
// });
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| {
|
||||
open_modified_files(workspace, window, cx);
|
||||
|
||||
@@ -118,13 +118,13 @@ impl Render for GitOnboardingModal {
|
||||
)),
|
||||
));
|
||||
|
||||
let open_panel_button = Button::new("open-panel", "Get Started with the Git Panel", cx)
|
||||
let open_panel_button = Button::new("open-panel", "Get Started with the Git Panel")
|
||||
.icon_size(IconSize::Indicator)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::open_panel));
|
||||
|
||||
let blog_post_button = Button::new("view-blog", "Check out the Blog Post", cx)
|
||||
let blog_post_button = Button::new("view-blog", "Check out the Blog Post")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
|
||||
@@ -759,7 +759,7 @@ impl Render for ProjectDiff {
|
||||
})
|
||||
.child(
|
||||
h_flex().justify_around().mt_1().child(
|
||||
Button::new("project-diff-close-button", "Close", cx)
|
||||
Button::new("project-diff-close-button", "Close")
|
||||
// .style(ButtonStyle::Transparent)
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&CloseActiveItem::default(),
|
||||
@@ -936,7 +936,7 @@ impl Render for ProjectDiffToolbar {
|
||||
h_group_sm()
|
||||
.when(button_states.selection, |el| {
|
||||
el.child(
|
||||
Button::new("stage", "Toggle Staged", cx)
|
||||
Button::new("stage", "Toggle Staged")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Toggle Staged",
|
||||
&ToggleStaged,
|
||||
@@ -950,7 +950,7 @@ impl Render for ProjectDiffToolbar {
|
||||
})
|
||||
.when(!button_states.selection, |el| {
|
||||
el.child(
|
||||
Button::new("stage", "Stage", cx)
|
||||
Button::new("stage", "Stage")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Stage and go to next hunk",
|
||||
&StageAndNext,
|
||||
@@ -961,7 +961,7 @@ impl Render for ProjectDiffToolbar {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("unstage", "Unstage", cx)
|
||||
Button::new("unstage", "Unstage")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Unstage and go to next hunk",
|
||||
&UnstageAndNext,
|
||||
@@ -1011,7 +1011,7 @@ impl Render for ProjectDiffToolbar {
|
||||
button_states.unstage_all && !button_states.stage_all,
|
||||
|el| {
|
||||
el.child(
|
||||
Button::new("unstage-all", "Unstage All", cx)
|
||||
Button::new("unstage-all", "Unstage All")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Unstage all changes",
|
||||
&UnstageAll,
|
||||
@@ -1030,7 +1030,7 @@ impl Render for ProjectDiffToolbar {
|
||||
// todo make it so that changing to say "Unstaged"
|
||||
// doesn't change the position.
|
||||
div().child(
|
||||
Button::new("stage-all", "Stage All", cx)
|
||||
Button::new("stage-all", "Stage All")
|
||||
.disabled(!button_states.stage_all)
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Stage all changes",
|
||||
@@ -1045,7 +1045,7 @@ impl Render for ProjectDiffToolbar {
|
||||
},
|
||||
)
|
||||
.child(
|
||||
Button::new("commit", "Commit", cx)
|
||||
Button::new("commit", "Commit")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"Commit",
|
||||
&Commit,
|
||||
|
||||
@@ -219,7 +219,7 @@ impl Render for CursorPosition {
|
||||
let context = self.context.clone();
|
||||
|
||||
el.child(
|
||||
Button::new("go-to-line-column", text, cx)
|
||||
Button::new("go-to-line-column", text)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::{Rc, Weak},
|
||||
sync::{Arc, atomic::Ordering::SeqCst},
|
||||
task::{Poll, Waker},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -91,6 +92,36 @@ impl AppCell {
|
||||
}
|
||||
Ok(AppRefMut(self.app.try_borrow_mut()?))
|
||||
}
|
||||
|
||||
pub fn shutdown(self: &Rc<AppCell>) {
|
||||
let mut futures = Vec::new();
|
||||
|
||||
let mut cx = self.borrow_mut();
|
||||
|
||||
for observer in cx.quit_observers.remove(&()) {
|
||||
futures.push(observer(&mut cx));
|
||||
}
|
||||
|
||||
cx.windows.clear();
|
||||
cx.window_handles.clear();
|
||||
cx.flush_effects();
|
||||
let executor = cx.background_executor.clone();
|
||||
drop(cx);
|
||||
|
||||
let waker = Waker::noop();
|
||||
let mut future_cx = std::task::Context::from_waker(waker);
|
||||
let futures = futures::future::join_all(futures);
|
||||
futures::pin_mut!(futures);
|
||||
let mut start = std::time::Instant::now();
|
||||
while start.elapsed() < SHUTDOWN_TIMEOUT {
|
||||
match futures.as_mut().poll(&mut future_cx) {
|
||||
Poll::Pending => {
|
||||
executor.tick();
|
||||
}
|
||||
Poll::Ready(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -277,8 +308,6 @@ pub struct App {
|
||||
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
||||
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
||||
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
|
||||
pub(crate) restart_observers: SubscriberSet<(), Handler>,
|
||||
pub(crate) restart_path: Option<PathBuf>,
|
||||
pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>,
|
||||
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
|
||||
pub(crate) propagate_event: bool,
|
||||
@@ -351,8 +380,6 @@ impl App {
|
||||
keyboard_layout_observers: SubscriberSet::new(),
|
||||
global_observers: SubscriberSet::new(),
|
||||
quit_observers: SubscriberSet::new(),
|
||||
restart_observers: SubscriberSet::new(),
|
||||
restart_path: None,
|
||||
window_closed_observers: SubscriberSet::new(),
|
||||
layout_id_buffer: Default::default(),
|
||||
propagate_event: true,
|
||||
@@ -386,39 +413,13 @@ impl App {
|
||||
platform.on_quit(Box::new({
|
||||
let cx = app.clone();
|
||||
move || {
|
||||
cx.borrow_mut().shutdown();
|
||||
cx.shutdown();
|
||||
}
|
||||
}));
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
/// Quit the application gracefully. Handlers registered with [`Context::on_app_quit`]
|
||||
/// will be given 100ms to complete before exiting.
|
||||
pub fn shutdown(&mut self) {
|
||||
let mut futures = Vec::new();
|
||||
|
||||
for observer in self.quit_observers.remove(&()) {
|
||||
futures.push(observer(self));
|
||||
}
|
||||
|
||||
self.windows.clear();
|
||||
self.window_handles.clear();
|
||||
self.flush_effects();
|
||||
self.quitting = true;
|
||||
|
||||
let futures = futures::future::join_all(futures);
|
||||
if self
|
||||
.background_executor
|
||||
.block_with_timeout(SHUTDOWN_TIMEOUT, futures)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("timed out waiting on app_will_quit");
|
||||
}
|
||||
|
||||
self.quitting = false;
|
||||
}
|
||||
|
||||
/// Get the id of the current keyboard layout
|
||||
pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout {
|
||||
self.keyboard_layout.as_ref()
|
||||
@@ -836,16 +837,8 @@ impl App {
|
||||
}
|
||||
|
||||
/// Restarts the application.
|
||||
pub fn restart(&mut self) {
|
||||
self.restart_observers
|
||||
.clone()
|
||||
.retain(&(), |observer| observer(self));
|
||||
self.platform.restart(self.restart_path.take())
|
||||
}
|
||||
|
||||
/// Sets the path to use when restarting the application.
|
||||
pub fn set_restart_path(&mut self, path: PathBuf) {
|
||||
self.restart_path = Some(path);
|
||||
pub fn restart(&self, binary_path: Option<PathBuf>) {
|
||||
self.platform.restart(binary_path)
|
||||
}
|
||||
|
||||
/// Returns the HTTP client for the application.
|
||||
@@ -1478,21 +1471,6 @@ impl App {
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the application is about to restart.
|
||||
///
|
||||
/// These callbacks are called before any `on_app_quit` callbacks.
|
||||
pub fn on_app_restart(&self, mut on_restart: impl 'static + FnMut(&mut App)) -> Subscription {
|
||||
let (subscription, activate) = self.restart_observers.insert(
|
||||
(),
|
||||
Box::new(move |cx| {
|
||||
on_restart(cx);
|
||||
true
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when a window is closed
|
||||
/// The window is no longer accessible at the point this callback is invoked.
|
||||
pub fn on_window_closed(&self, mut on_closed: impl FnMut(&mut App) + 'static) -> Subscription {
|
||||
|
||||
@@ -164,20 +164,6 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the application is about to restart.
|
||||
pub fn on_app_restart(
|
||||
&self,
|
||||
mut on_restart: impl FnMut(&mut T, &mut App) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let handle = self.weak_entity();
|
||||
self.app.on_app_restart(move |cx| {
|
||||
handle.update(cx, |entity, cx| on_restart(entity, cx)).ok();
|
||||
})
|
||||
}
|
||||
|
||||
/// Arrange for the given function to be invoked whenever the application is quit.
|
||||
/// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits.
|
||||
pub fn on_app_quit<Fut>(
|
||||
@@ -189,15 +175,20 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
T: 'static,
|
||||
{
|
||||
let handle = self.weak_entity();
|
||||
self.app.on_app_quit(move |cx| {
|
||||
let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
|
||||
async move {
|
||||
if let Some(future) = future {
|
||||
future.await;
|
||||
let (subscription, activate) = self.app.quit_observers.insert(
|
||||
(),
|
||||
Box::new(move |cx| {
|
||||
let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
|
||||
async move {
|
||||
if let Some(future) = future {
|
||||
future.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
.boxed_local()
|
||||
})
|
||||
.boxed_local()
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Tell GPUI that this entity has changed and observers of it should be notified.
|
||||
|
||||
@@ -167,7 +167,7 @@ impl TestAppContext {
|
||||
/// public so the macro can call it.
|
||||
pub fn quit(&self) {
|
||||
self.on_quit.borrow_mut().drain(..).for_each(|f| f());
|
||||
self.app.borrow_mut().shutdown();
|
||||
self.app.shutdown();
|
||||
}
|
||||
|
||||
/// Register cleanup to run when the test ends.
|
||||
|
||||
@@ -384,10 +384,9 @@ impl BackgroundExecutor {
|
||||
self.dispatcher.as_test().unwrap().advance_clock(duration)
|
||||
}
|
||||
|
||||
/// in tests, run one task.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
/// docs
|
||||
pub fn tick(&self) -> bool {
|
||||
self.dispatcher.as_test().unwrap().tick(false)
|
||||
self.dispatcher.tick(true)
|
||||
}
|
||||
|
||||
/// in tests, run all tasks that are ready to run. If after doing so
|
||||
|
||||
@@ -545,6 +545,7 @@ pub trait PlatformDispatcher: Send + Sync {
|
||||
fn now(&self) -> Instant {
|
||||
Instant::now()
|
||||
}
|
||||
fn tick(&self, _: bool) -> bool;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_test(&self) -> Option<&TestDispatcher> {
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
|
||||
use crate::{PlatformDispatcher, TaskLabel};
|
||||
use async_task::Runnable;
|
||||
use block::{Block, ConcreteBlock, RcBlock};
|
||||
use core_foundation::{
|
||||
base::CFTypeRef,
|
||||
runloop::{
|
||||
CFRunLoopRef, CFRunLoopRunInMode, CFRunLoopWakeUp, kCFRunLoopCommonModes,
|
||||
kCFRunLoopDefaultMode,
|
||||
},
|
||||
};
|
||||
use objc::{
|
||||
class, msg_send,
|
||||
runtime::{BOOL, YES},
|
||||
@@ -11,7 +19,9 @@ use objc::{
|
||||
};
|
||||
use parking::{Parker, Unparker};
|
||||
use parking_lot::Mutex;
|
||||
use smol::io::BlockOn;
|
||||
use std::{
|
||||
cell::Cell,
|
||||
ffi::c_void,
|
||||
ptr::{NonNull, addr_of},
|
||||
sync::Arc,
|
||||
@@ -64,11 +74,21 @@ impl PlatformDispatcher for MacDispatcher {
|
||||
}
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: Runnable) {
|
||||
use core_foundation::runloop::CFRunLoopGetMain;
|
||||
|
||||
unsafe {
|
||||
dispatch_async_f(
|
||||
dispatch_get_main_queue(),
|
||||
runnable.into_raw().as_ptr() as *mut c_void,
|
||||
Some(trampoline),
|
||||
let mut runnable = Cell::new(Some(runnable));
|
||||
let main_run_loop = CFRunLoopGetMain();
|
||||
let block = ConcreteBlock::new(move || {
|
||||
if let Some(runnable) = runnable.take() {
|
||||
runnable.run();
|
||||
}
|
||||
})
|
||||
.copy();
|
||||
CFRunLoopPerformBlock(
|
||||
main_run_loop,
|
||||
kCFRunLoopDefaultMode as _,
|
||||
&*block as *const Block<_, _> as _,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -87,6 +107,13 @@ impl PlatformDispatcher for MacDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn tick(&self, background_only: bool) -> bool {
|
||||
unsafe {
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0., 0);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn park(&self, timeout: Option<Duration>) -> bool {
|
||||
if let Some(timeout) = timeout {
|
||||
self.parker.lock().park_timeout(timeout)
|
||||
@@ -105,3 +132,7 @@ extern "C" fn trampoline(runnable: *mut c_void) {
|
||||
let task = unsafe { Runnable::<()>::from_raw(NonNull::new_unchecked(runnable as *mut ())) };
|
||||
task.run();
|
||||
}
|
||||
|
||||
unsafe extern "C" {
|
||||
fn CFRunLoopPerformBlock(rl: CFRunLoopRef, mode: CFTypeRef, block: *const c_void);
|
||||
}
|
||||
|
||||
@@ -122,68 +122,6 @@ impl TestDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&self, background_only: bool) -> bool {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
while let Some((deadline, _)) = state.delayed.first() {
|
||||
if *deadline > state.time {
|
||||
break;
|
||||
}
|
||||
let (_, runnable) = state.delayed.remove(0);
|
||||
state.background.push(runnable);
|
||||
}
|
||||
|
||||
let foreground_len: usize = if background_only {
|
||||
0
|
||||
} else {
|
||||
state
|
||||
.foreground
|
||||
.values()
|
||||
.map(|runnables| runnables.len())
|
||||
.sum()
|
||||
};
|
||||
let background_len = state.background.len();
|
||||
|
||||
let runnable;
|
||||
let main_thread;
|
||||
if foreground_len == 0 && background_len == 0 {
|
||||
let deprioritized_background_len = state.deprioritized_background.len();
|
||||
if deprioritized_background_len == 0 {
|
||||
return false;
|
||||
}
|
||||
let ix = state.random.gen_range(0..deprioritized_background_len);
|
||||
main_thread = false;
|
||||
runnable = state.deprioritized_background.swap_remove(ix);
|
||||
} else {
|
||||
main_thread = state.random.gen_ratio(
|
||||
foreground_len as u32,
|
||||
(foreground_len + background_len) as u32,
|
||||
);
|
||||
if main_thread {
|
||||
let state = &mut *state;
|
||||
runnable = state
|
||||
.foreground
|
||||
.values_mut()
|
||||
.filter(|runnables| !runnables.is_empty())
|
||||
.choose(&mut state.random)
|
||||
.unwrap()
|
||||
.pop_front()
|
||||
.unwrap();
|
||||
} else {
|
||||
let ix = state.random.gen_range(0..background_len);
|
||||
runnable = state.background.swap_remove(ix);
|
||||
};
|
||||
};
|
||||
|
||||
let was_main_thread = state.is_main_thread;
|
||||
state.is_main_thread = main_thread;
|
||||
drop(state);
|
||||
runnable.run();
|
||||
self.state.lock().is_main_thread = was_main_thread;
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn deprioritize(&self, task_label: TaskLabel) {
|
||||
self.state
|
||||
.lock()
|
||||
@@ -267,6 +205,68 @@ impl PlatformDispatcher for TestDispatcher {
|
||||
state.start_time + state.time
|
||||
}
|
||||
|
||||
fn tick(&self, background_only: bool) -> bool {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
while let Some((deadline, _)) = state.delayed.first() {
|
||||
if *deadline > state.time {
|
||||
break;
|
||||
}
|
||||
let (_, runnable) = state.delayed.remove(0);
|
||||
state.background.push(runnable);
|
||||
}
|
||||
|
||||
let foreground_len: usize = if background_only {
|
||||
0
|
||||
} else {
|
||||
state
|
||||
.foreground
|
||||
.values()
|
||||
.map(|runnables| runnables.len())
|
||||
.sum()
|
||||
};
|
||||
let background_len = state.background.len();
|
||||
|
||||
let runnable;
|
||||
let main_thread;
|
||||
if foreground_len == 0 && background_len == 0 {
|
||||
let deprioritized_background_len = state.deprioritized_background.len();
|
||||
if deprioritized_background_len == 0 {
|
||||
return false;
|
||||
}
|
||||
let ix = state.random.gen_range(0..deprioritized_background_len);
|
||||
main_thread = false;
|
||||
runnable = state.deprioritized_background.swap_remove(ix);
|
||||
} else {
|
||||
main_thread = state.random.gen_ratio(
|
||||
foreground_len as u32,
|
||||
(foreground_len + background_len) as u32,
|
||||
);
|
||||
if main_thread {
|
||||
let state = &mut *state;
|
||||
runnable = state
|
||||
.foreground
|
||||
.values_mut()
|
||||
.filter(|runnables| !runnables.is_empty())
|
||||
.choose(&mut state.random)
|
||||
.unwrap()
|
||||
.pop_front()
|
||||
.unwrap();
|
||||
} else {
|
||||
let ix = state.random.gen_range(0..background_len);
|
||||
runnable = state.background.swap_remove(ix);
|
||||
};
|
||||
};
|
||||
|
||||
let was_main_thread = state.is_main_thread;
|
||||
state.is_main_thread = main_thread;
|
||||
drop(state);
|
||||
runnable.run();
|
||||
self.state.lock().is_main_thread = was_main_thread;
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
|
||||
@@ -10,7 +10,6 @@ mod keyboard;
|
||||
mod platform;
|
||||
mod system_settings;
|
||||
mod util;
|
||||
mod vsync;
|
||||
mod window;
|
||||
mod wrapper;
|
||||
|
||||
@@ -26,7 +25,6 @@ pub(crate) use keyboard::*;
|
||||
pub(crate) use platform::*;
|
||||
pub(crate) use system_settings::*;
|
||||
pub(crate) use util::*;
|
||||
pub(crate) use vsync::*;
|
||||
pub(crate) use window::*;
|
||||
pub(crate) use wrapper::*;
|
||||
|
||||
|
||||
@@ -4,15 +4,16 @@ use ::util::ResultExt;
|
||||
use anyhow::{Context, Result};
|
||||
use windows::{
|
||||
Win32::{
|
||||
Foundation::{HMODULE, HWND},
|
||||
Foundation::{FreeLibrary, HMODULE, HWND},
|
||||
Graphics::{
|
||||
Direct3D::*,
|
||||
Direct3D11::*,
|
||||
DirectComposition::*,
|
||||
Dxgi::{Common::*, *},
|
||||
},
|
||||
System::LibraryLoader::LoadLibraryA,
|
||||
},
|
||||
core::Interface,
|
||||
core::{Interface, PCSTR},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -207,7 +208,7 @@ impl DirectXRenderer {
|
||||
|
||||
fn present(&mut self) -> Result<()> {
|
||||
unsafe {
|
||||
let result = self.resources.swap_chain.Present(0, DXGI_PRESENT(0));
|
||||
let result = self.resources.swap_chain.Present(1, DXGI_PRESENT(0));
|
||||
// Presenting the swap chain can fail if the DirectX device was removed or reset.
|
||||
if result == DXGI_ERROR_DEVICE_REMOVED || result == DXGI_ERROR_DEVICE_RESET {
|
||||
let reason = self.devices.device.GetDeviceRemovedReason();
|
||||
@@ -1618,6 +1619,22 @@ pub(crate) mod shader_resources {
|
||||
}
|
||||
}
|
||||
|
||||
fn with_dll_library<R, F>(dll_name: PCSTR, f: F) -> Result<R>
|
||||
where
|
||||
F: FnOnce(HMODULE) -> Result<R>,
|
||||
{
|
||||
let library = unsafe {
|
||||
LoadLibraryA(dll_name).with_context(|| format!("Loading dll: {}", dll_name.display()))?
|
||||
};
|
||||
let result = f(library);
|
||||
unsafe {
|
||||
FreeLibrary(library)
|
||||
.with_context(|| format!("Freeing dll: {}", dll_name.display()))
|
||||
.log_err();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
mod nvidia {
|
||||
use std::{
|
||||
ffi::CStr,
|
||||
@@ -1627,7 +1644,7 @@ mod nvidia {
|
||||
use anyhow::Result;
|
||||
use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s};
|
||||
|
||||
use crate::with_dll_library;
|
||||
use crate::platform::windows::directx_renderer::with_dll_library;
|
||||
|
||||
// https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_lite_common.h#L180
|
||||
const NVAPI_SHORT_STRING_MAX: usize = 64;
|
||||
@@ -1694,7 +1711,7 @@ mod amd {
|
||||
use anyhow::Result;
|
||||
use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s};
|
||||
|
||||
use crate::with_dll_library;
|
||||
use crate::platform::windows::directx_renderer::with_dll_library;
|
||||
|
||||
// https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L145
|
||||
const AGS_CURRENT_VERSION: i32 = (6 << 22) | (3 << 12);
|
||||
|
||||
@@ -32,7 +32,7 @@ use crate::*;
|
||||
|
||||
pub(crate) struct WindowsPlatform {
|
||||
state: RefCell<WindowsPlatformState>,
|
||||
raw_window_handles: Arc<RwLock<SmallVec<[SafeHwnd; 4]>>>,
|
||||
raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
|
||||
// The below members will never change throughout the entire lifecycle of the app.
|
||||
icon: HICON,
|
||||
main_receiver: flume::Receiver<Runnable>,
|
||||
@@ -114,7 +114,7 @@ impl WindowsPlatform {
|
||||
};
|
||||
let icon = load_icon().unwrap_or_default();
|
||||
let state = RefCell::new(WindowsPlatformState::new());
|
||||
let raw_window_handles = Arc::new(RwLock::new(SmallVec::new()));
|
||||
let raw_window_handles = RwLock::new(SmallVec::new());
|
||||
let windows_version = WindowsVersion::new().context("Error retrieve windows version")?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -134,12 +134,22 @@ impl WindowsPlatform {
|
||||
})
|
||||
}
|
||||
|
||||
fn redraw_all(&self) {
|
||||
for handle in self.raw_window_handles.read().iter() {
|
||||
unsafe {
|
||||
RedrawWindow(Some(*handle), None, None, RDW_INVALIDATE | RDW_UPDATENOW)
|
||||
.ok()
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_from_hwnd(&self, hwnd: HWND) -> Option<Rc<WindowsWindowInner>> {
|
||||
self.raw_window_handles
|
||||
.read()
|
||||
.iter()
|
||||
.find(|entry| entry.as_raw() == hwnd)
|
||||
.and_then(|hwnd| window_from_hwnd(hwnd.as_raw()))
|
||||
.find(|entry| *entry == &hwnd)
|
||||
.and_then(|hwnd| window_from_hwnd(*hwnd))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -148,7 +158,7 @@ impl WindowsPlatform {
|
||||
.read()
|
||||
.iter()
|
||||
.for_each(|handle| unsafe {
|
||||
PostMessageW(Some(handle.as_raw()), message, wparam, lparam).log_err();
|
||||
PostMessageW(Some(*handle), message, wparam, lparam).log_err();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,7 +166,7 @@ impl WindowsPlatform {
|
||||
let mut lock = self.raw_window_handles.write();
|
||||
let index = lock
|
||||
.iter()
|
||||
.position(|handle| handle.as_raw() == target_window)
|
||||
.position(|handle| *handle == target_window)
|
||||
.unwrap();
|
||||
lock.remove(index);
|
||||
|
||||
@@ -216,19 +226,19 @@ impl WindowsPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns if the app should quit.
|
||||
fn handle_events(&self) {
|
||||
// Returns true if the app should quit.
|
||||
fn handle_events(&self) -> bool {
|
||||
let mut msg = MSG::default();
|
||||
unsafe {
|
||||
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
match msg.message {
|
||||
WM_QUIT => return,
|
||||
WM_QUIT => return true,
|
||||
WM_INPUTLANGCHANGE
|
||||
| WM_GPUI_CLOSE_ONE_WINDOW
|
||||
| WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD
|
||||
| WM_GPUI_DOCK_MENU_ACTION => {
|
||||
if self.handle_gpui_evnets(msg.message, msg.wParam, msg.lParam, &msg) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -237,6 +247,7 @@ impl WindowsPlatform {
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Returns true if the app should quit.
|
||||
@@ -304,28 +315,8 @@ impl WindowsPlatform {
|
||||
self.raw_window_handles
|
||||
.read()
|
||||
.iter()
|
||||
.find(|hwnd| hwnd.as_raw() == active_window_hwnd)
|
||||
.map(|hwnd| hwnd.as_raw())
|
||||
}
|
||||
|
||||
fn begin_vsync_thread(&self) {
|
||||
let all_windows = Arc::downgrade(&self.raw_window_handles);
|
||||
std::thread::spawn(move || {
|
||||
let vsync_provider = VSyncProvider::new();
|
||||
loop {
|
||||
vsync_provider.wait_for_vsync();
|
||||
let Some(all_windows) = all_windows.upgrade() else {
|
||||
break;
|
||||
};
|
||||
for hwnd in all_windows.read().iter() {
|
||||
unsafe {
|
||||
RedrawWindow(Some(hwnd.as_raw()), None, None, RDW_INVALIDATE)
|
||||
.ok()
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
.find(|&&hwnd| hwnd == active_window_hwnd)
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,8 +347,12 @@ impl Platform for WindowsPlatform {
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
|
||||
on_finish_launching();
|
||||
self.begin_vsync_thread();
|
||||
self.handle_events();
|
||||
loop {
|
||||
if self.handle_events() {
|
||||
break;
|
||||
}
|
||||
self.redraw_all();
|
||||
}
|
||||
|
||||
if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit {
|
||||
callback();
|
||||
@@ -370,9 +365,9 @@ impl Platform for WindowsPlatform {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn restart(&self, binary_path: Option<PathBuf>) {
|
||||
fn restart(&self, _: Option<PathBuf>) {
|
||||
let pid = std::process::id();
|
||||
let Some(app_path) = binary_path.or(self.app_path().log_err()) else {
|
||||
let Some(app_path) = self.app_path().log_err() else {
|
||||
return;
|
||||
};
|
||||
let script = format!(
|
||||
@@ -450,7 +445,7 @@ impl Platform for WindowsPlatform {
|
||||
) -> Result<Box<dyn PlatformWindow>> {
|
||||
let window = WindowsWindow::new(handle, options, self.generate_creation_info())?;
|
||||
let handle = window.get_raw_handle();
|
||||
self.raw_window_handles.write().push(handle.into());
|
||||
self.raw_window_handles.write().push(handle);
|
||||
|
||||
Ok(Box::new(window))
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use ::util::ResultExt;
|
||||
use anyhow::Context;
|
||||
use windows::{
|
||||
UI::{
|
||||
Color,
|
||||
ViewManagement::{UIColorType, UISettings},
|
||||
},
|
||||
Wdk::System::SystemServices::RtlGetVersion,
|
||||
Win32::{
|
||||
Foundation::*, Graphics::Dwm::*, System::LibraryLoader::LoadLibraryA,
|
||||
UI::WindowsAndMessaging::*,
|
||||
},
|
||||
core::{BOOL, HSTRING, PCSTR},
|
||||
Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*},
|
||||
core::{BOOL, HSTRING},
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
@@ -201,19 +197,3 @@ pub(crate) fn show_error(title: &str, content: String) {
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn with_dll_library<R, F>(dll_name: PCSTR, f: F) -> Result<R>
|
||||
where
|
||||
F: FnOnce(HMODULE) -> Result<R>,
|
||||
{
|
||||
let library = unsafe {
|
||||
LoadLibraryA(dll_name).with_context(|| format!("Loading dll: {}", dll_name.display()))?
|
||||
};
|
||||
let result = f(library);
|
||||
unsafe {
|
||||
FreeLibrary(library)
|
||||
.with_context(|| format!("Freeing dll: {}", dll_name.display()))
|
||||
.log_err();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
use std::{
|
||||
sync::LazyLock,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use util::ResultExt;
|
||||
use windows::{
|
||||
Win32::{
|
||||
Foundation::{HANDLE, HWND},
|
||||
Graphics::{
|
||||
DirectComposition::{
|
||||
COMPOSITION_FRAME_ID_COMPLETED, COMPOSITION_FRAME_ID_TYPE, COMPOSITION_FRAME_STATS,
|
||||
COMPOSITION_TARGET_ID,
|
||||
},
|
||||
Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo},
|
||||
},
|
||||
System::{
|
||||
LibraryLoader::{GetModuleHandleA, GetProcAddress},
|
||||
Performance::QueryPerformanceFrequency,
|
||||
Threading::INFINITE,
|
||||
},
|
||||
},
|
||||
core::{HRESULT, s},
|
||||
};
|
||||
|
||||
static QPC_TICKS_PER_SECOND: LazyLock<u64> = LazyLock::new(|| {
|
||||
let mut frequency = 0;
|
||||
// On systems that run Windows XP or later, the function will always succeed and
|
||||
// will thus never return zero.
|
||||
unsafe { QueryPerformanceFrequency(&mut frequency).unwrap() };
|
||||
frequency as u64
|
||||
});
|
||||
|
||||
const VSYNC_INTERVAL_THRESHOLD: Duration = Duration::from_millis(1);
|
||||
const DEFAULT_VSYNC_INTERVAL: Duration = Duration::from_micros(16_666); // ~60Hz
|
||||
|
||||
// Here we are using dynamic loading of DirectComposition functions,
|
||||
// or the app will refuse to start on windows systems that do not support DirectComposition.
|
||||
type DCompositionGetFrameId =
|
||||
unsafe extern "system" fn(frameidtype: COMPOSITION_FRAME_ID_TYPE, frameid: *mut u64) -> HRESULT;
|
||||
type DCompositionGetStatistics = unsafe extern "system" fn(
|
||||
frameid: u64,
|
||||
framestats: *mut COMPOSITION_FRAME_STATS,
|
||||
targetidcount: u32,
|
||||
targetids: *mut COMPOSITION_TARGET_ID,
|
||||
actualtargetidcount: *mut u32,
|
||||
) -> HRESULT;
|
||||
type DCompositionWaitForCompositorClock =
|
||||
unsafe extern "system" fn(count: u32, handles: *const HANDLE, timeoutinms: u32) -> u32;
|
||||
|
||||
pub(crate) struct VSyncProvider {
|
||||
interval: Duration,
|
||||
f: Box<dyn Fn() -> bool>,
|
||||
}
|
||||
|
||||
impl VSyncProvider {
|
||||
pub(crate) fn new() -> Self {
|
||||
if let Some((get_frame_id, get_statistics, wait_for_comp_clock)) =
|
||||
initialize_direct_composition()
|
||||
.context("Retrieving DirectComposition functions")
|
||||
.log_with_level(log::Level::Warn)
|
||||
{
|
||||
let interval = get_dwm_interval_from_direct_composition(get_frame_id, get_statistics)
|
||||
.context("Failed to get DWM interval from DirectComposition")
|
||||
.log_err()
|
||||
.unwrap_or(DEFAULT_VSYNC_INTERVAL);
|
||||
log::info!(
|
||||
"DirectComposition is supported for VSync, interval: {:?}",
|
||||
interval
|
||||
);
|
||||
let f = Box::new(move || unsafe {
|
||||
wait_for_comp_clock(0, std::ptr::null(), INFINITE) == 0
|
||||
});
|
||||
Self { interval, f }
|
||||
} else {
|
||||
let interval = get_dwm_interval()
|
||||
.context("Failed to get DWM interval")
|
||||
.log_err()
|
||||
.unwrap_or(DEFAULT_VSYNC_INTERVAL);
|
||||
log::info!(
|
||||
"DirectComposition is not supported for VSync, falling back to DWM, interval: {:?}",
|
||||
interval
|
||||
);
|
||||
let f = Box::new(|| unsafe { DwmFlush().is_ok() });
|
||||
Self { interval, f }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wait_for_vsync(&self) {
|
||||
let vsync_start = Instant::now();
|
||||
let wait_succeeded = (self.f)();
|
||||
let elapsed = vsync_start.elapsed();
|
||||
// DwmFlush and DCompositionWaitForCompositorClock returns very early
|
||||
// instead of waiting until vblank when the monitor goes to sleep or is
|
||||
// unplugged (nothing to present due to desktop occlusion). We use 1ms as
|
||||
// a threshhold for the duration of the wait functions and fallback to
|
||||
// Sleep() if it returns before that. This could happen during normal
|
||||
// operation for the first call after the vsync thread becomes non-idle,
|
||||
// but it shouldn't happen often.
|
||||
if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD {
|
||||
log::warn!("VSyncProvider::wait_for_vsync() took shorter than expected");
|
||||
std::thread::sleep(self.interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_direct_composition() -> Result<(
|
||||
DCompositionGetFrameId,
|
||||
DCompositionGetStatistics,
|
||||
DCompositionWaitForCompositorClock,
|
||||
)> {
|
||||
unsafe {
|
||||
// Load DLL at runtime since older Windows versions don't have dcomp.
|
||||
let hmodule = GetModuleHandleA(s!("dcomp.dll")).context("Loading dcomp.dll")?;
|
||||
let get_frame_id_addr = GetProcAddress(hmodule, s!("DCompositionGetFrameId"))
|
||||
.context("Function DCompositionGetFrameId not found")?;
|
||||
let get_statistics_addr = GetProcAddress(hmodule, s!("DCompositionGetStatistics"))
|
||||
.context("Function DCompositionGetStatistics not found")?;
|
||||
let wait_for_compositor_clock_addr =
|
||||
GetProcAddress(hmodule, s!("DCompositionWaitForCompositorClock"))
|
||||
.context("Function DCompositionWaitForCompositorClock not found")?;
|
||||
let get_frame_id: DCompositionGetFrameId = std::mem::transmute(get_frame_id_addr);
|
||||
let get_statistics: DCompositionGetStatistics = std::mem::transmute(get_statistics_addr);
|
||||
let wait_for_compositor_clock: DCompositionWaitForCompositorClock =
|
||||
std::mem::transmute(wait_for_compositor_clock_addr);
|
||||
Ok((get_frame_id, get_statistics, wait_for_compositor_clock))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_dwm_interval_from_direct_composition(
|
||||
get_frame_id: DCompositionGetFrameId,
|
||||
get_statistics: DCompositionGetStatistics,
|
||||
) -> Result<Duration> {
|
||||
let mut frame_id = 0;
|
||||
unsafe { get_frame_id(COMPOSITION_FRAME_ID_COMPLETED, &mut frame_id) }.ok()?;
|
||||
let mut stats = COMPOSITION_FRAME_STATS::default();
|
||||
unsafe {
|
||||
get_statistics(
|
||||
frame_id,
|
||||
&mut stats,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
}
|
||||
.ok()?;
|
||||
Ok(retrieve_duration(stats.framePeriod, *QPC_TICKS_PER_SECOND))
|
||||
}
|
||||
|
||||
fn get_dwm_interval() -> Result<Duration> {
|
||||
let mut timing_info = DWM_TIMING_INFO {
|
||||
cbSize: std::mem::size_of::<DWM_TIMING_INFO>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
unsafe { DwmGetCompositionTimingInfo(HWND::default(), &mut timing_info) }?;
|
||||
let interval = retrieve_duration(timing_info.qpcRefreshPeriod, *QPC_TICKS_PER_SECOND);
|
||||
// Check for interval values that are impossibly low. A 29 microsecond
|
||||
// interval was seen (from a qpcRefreshPeriod of 60).
|
||||
if interval < VSYNC_INTERVAL_THRESHOLD {
|
||||
Ok(retrieve_duration(
|
||||
timing_info.rateRefresh.uiDenominator as u64,
|
||||
timing_info.rateRefresh.uiNumerator as u64,
|
||||
))
|
||||
} else {
|
||||
Ok(interval)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn retrieve_duration(counts: u64, ticks_per_second: u64) -> Duration {
|
||||
let ticks_per_microsecond = ticks_per_second / 1_000_000;
|
||||
Duration::from_micros(counts / ticks_per_microsecond)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::HCURSOR};
|
||||
use windows::Win32::UI::WindowsAndMessaging::HCURSOR;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct SafeCursor {
|
||||
@@ -23,31 +23,3 @@ impl Deref for SafeCursor {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct SafeHwnd {
|
||||
raw: HWND,
|
||||
}
|
||||
|
||||
impl SafeHwnd {
|
||||
pub(crate) fn as_raw(&self) -> HWND {
|
||||
self.raw
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for SafeHwnd {}
|
||||
unsafe impl Sync for SafeHwnd {}
|
||||
|
||||
impl From<HWND> for SafeHwnd {
|
||||
fn from(value: HWND) -> Self {
|
||||
SafeHwnd { raw: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SafeHwnd {
|
||||
type Target = HWND;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,6 @@ fn generate_test_function(
|
||||
));
|
||||
cx_teardowns.extend(quote!(
|
||||
dispatcher.run_until_parked();
|
||||
#cx_varname.executor().forbid_parking();
|
||||
#cx_varname.quit();
|
||||
dispatcher.run_until_parked();
|
||||
));
|
||||
@@ -233,7 +232,7 @@ fn generate_test_function(
|
||||
cx_teardowns.extend(quote!(
|
||||
drop(#cx_varname_lock);
|
||||
dispatcher.run_until_parked();
|
||||
#cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); });
|
||||
#cx_varname.update(|cx| { cx.quit() });
|
||||
dispatcher.run_until_parked();
|
||||
));
|
||||
continue;
|
||||
@@ -248,7 +247,6 @@ fn generate_test_function(
|
||||
));
|
||||
cx_teardowns.extend(quote!(
|
||||
dispatcher.run_until_parked();
|
||||
#cx_varname.executor().forbid_parking();
|
||||
#cx_varname.quit();
|
||||
dispatcher.run_until_parked();
|
||||
));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user