Compare commits
23 Commits
fix-action
...
pretty-typ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53cf8a4e0e | ||
|
|
6a9ec10dac | ||
|
|
c3edc2cfc1 | ||
|
|
625a4b90a5 | ||
|
|
fbead09c30 | ||
|
|
0797f7b66e | ||
|
|
6f6c2915b2 | ||
|
|
0bd65829f7 | ||
|
|
90bf602ceb | ||
|
|
cd024b8870 | ||
|
|
af71e15ea0 | ||
|
|
d0e01dbd8f | ||
|
|
d65855c4a1 | ||
|
|
70351360d7 | ||
|
|
993e0f55ec | ||
|
|
496bf0ec43 | ||
|
|
c09f484ec4 | ||
|
|
a58a75c0f6 | ||
|
|
d1a6c5d494 | ||
|
|
10028aaae8 | ||
|
|
3b9bb521f4 | ||
|
|
7eb739d489 | ||
|
|
b4cbea50bb |
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"file_types": {
|
||||
"Dockerfile": ["Dockerfile*[!dockerignore]"],
|
||||
"JSONC": ["assets/**/*.json", "renovate.json"],
|
||||
"JSONC": ["**/assets/**/*.json", "renovate.json"],
|
||||
"Git Ignore": ["dockerignore"]
|
||||
},
|
||||
"hard_tabs": false,
|
||||
|
||||
6
Cargo.lock
generated
@@ -9,6 +9,7 @@ dependencies = [
|
||||
"agent_servers",
|
||||
"agentic-coding-protocol",
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
"async-pipe",
|
||||
"buffer_diff",
|
||||
"editor",
|
||||
@@ -263,9 +264,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agentic-coding-protocol"
|
||||
version = "0.0.6"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1ac0351749af7bf53c65042ef69fefb9351aa8b7efa0a813d6281377605c37d"
|
||||
checksum = "a75f520bcc049ebe40c8c99427aa61b48ad78a01bcc96a13b350b903dcfb9438"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -9133,6 +9134,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"indoc",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
|
||||
@@ -404,7 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agentic-coding-protocol = "0.0.6"
|
||||
agentic-coding-protocol = "0.0.7"
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
|
||||
@@ -320,7 +320,8 @@
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"up": "agent::PreviousHistoryMessage",
|
||||
"down": "agent::NextHistoryMessage"
|
||||
"down": "agent::NextHistoryMessage",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1112,7 +1113,10 @@
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-f": "search::FocusSearch"
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"alt-find": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-c": "keymap_editor::ToggleConflictFilter"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -371,7 +371,8 @@
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"up": "agent::PreviousHistoryMessage",
|
||||
"down": "agent::NextHistoryMessage"
|
||||
"down": "agent::NextHistoryMessage",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1211,7 +1212,8 @@
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-f": "search::FocusSearch"
|
||||
"cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"cmd-alt-c": "keymap_editor::ToggleConflictFilter"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -20,6 +20,7 @@ gemini = []
|
||||
agent_servers.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@@ -2,14 +2,19 @@ pub use acp::ToolCallId;
|
||||
use agent_servers::AgentServer;
|
||||
use agentic_coding_protocol::{self as acp, UserMessageChunk};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::BufferDiff;
|
||||
use editor::{MultiBuffer, PathKey};
|
||||
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
|
||||
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
|
||||
use itertools::Itertools;
|
||||
use language::{Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _};
|
||||
use language::{
|
||||
Anchor, Buffer, BufferSnapshot, Capability, LanguageRegistry, OffsetRangeExt as _, Point,
|
||||
text_diff,
|
||||
};
|
||||
use markdown::Markdown;
|
||||
use project::Project;
|
||||
use project::{AgentLocation, Project};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Formatter, Write};
|
||||
use std::{
|
||||
@@ -159,6 +164,26 @@ impl AgentThreadEntry {
|
||||
Self::ToolCall(too_call) => too_call.to_markdown(cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff(&self) -> Option<&Diff> {
|
||||
if let AgentThreadEntry::ToolCall(ToolCall {
|
||||
content: Some(ToolCallContent::Diff { diff }),
|
||||
..
|
||||
}) = self
|
||||
{
|
||||
Some(&diff)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> {
|
||||
if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self {
|
||||
Some(locations)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -168,6 +193,7 @@ pub struct ToolCall {
|
||||
pub icon: IconName,
|
||||
pub content: Option<ToolCallContent>,
|
||||
pub status: ToolCallStatus,
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
}
|
||||
|
||||
impl ToolCall {
|
||||
@@ -328,6 +354,8 @@ impl ToolCallContent {
|
||||
pub struct Diff {
|
||||
pub multibuffer: Entity<MultiBuffer>,
|
||||
pub path: PathBuf,
|
||||
pub new_buffer: Entity<Buffer>,
|
||||
pub old_buffer: Entity<Buffer>,
|
||||
_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
@@ -362,6 +390,7 @@ impl Diff {
|
||||
let task = cx.spawn({
|
||||
let multibuffer = multibuffer.clone();
|
||||
let path = path.clone();
|
||||
let new_buffer = new_buffer.clone();
|
||||
async move |cx| {
|
||||
diff_task.await?;
|
||||
|
||||
@@ -401,6 +430,8 @@ impl Diff {
|
||||
Self {
|
||||
multibuffer,
|
||||
path,
|
||||
new_buffer,
|
||||
old_buffer,
|
||||
_task: task,
|
||||
}
|
||||
}
|
||||
@@ -421,6 +452,8 @@ pub struct AcpThread {
|
||||
entries: Vec<AgentThreadEntry>,
|
||||
title: SharedString,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
|
||||
send_task: Option<Task<()>>,
|
||||
connection: Arc<acp::AgentConnection>,
|
||||
child_status: Option<Task<Result<()>>>,
|
||||
@@ -522,7 +555,11 @@ impl AcpThread {
|
||||
}
|
||||
});
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
Self {
|
||||
action_log,
|
||||
shared_buffers: Default::default(),
|
||||
entries: Default::default(),
|
||||
title: "ACP Thread".into(),
|
||||
project,
|
||||
@@ -534,6 +571,14 @@ impl AcpThread {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn action_log(&self) -> &Entity<ActionLog> {
|
||||
&self.action_log
|
||||
}
|
||||
|
||||
pub fn project(&self) -> &Entity<Project> {
|
||||
&self.project
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn fake(
|
||||
stdin: async_pipe::PipeWriter,
|
||||
@@ -558,7 +603,11 @@ impl AcpThread {
|
||||
}
|
||||
});
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
Self {
|
||||
action_log,
|
||||
shared_buffers: Default::default(),
|
||||
entries: Default::default(),
|
||||
title: "ACP Thread".into(),
|
||||
project,
|
||||
@@ -589,6 +638,26 @@ impl AcpThread {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_pending_edit_tool_calls(&self) -> bool {
|
||||
for entry in self.entries.iter().rev() {
|
||||
match entry {
|
||||
AgentThreadEntry::UserMessage(_) => return false,
|
||||
AgentThreadEntry::ToolCall(ToolCall {
|
||||
status:
|
||||
ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Running,
|
||||
..
|
||||
},
|
||||
content: Some(ToolCallContent::Diff { .. }),
|
||||
..
|
||||
}) => return true,
|
||||
AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
|
||||
self.entries.push(entry);
|
||||
cx.emit(AcpThreadEvent::NewEntry);
|
||||
@@ -644,65 +713,63 @@ impl AcpThread {
|
||||
|
||||
pub fn request_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
confirmation: acp::ToolCallConfirmation,
|
||||
tool_call: acp::RequestToolCallConfirmationParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolCallRequest {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let status = ToolCallStatus::WaitingForConfirmation {
|
||||
confirmation: ToolCallConfirmation::from_acp(
|
||||
confirmation,
|
||||
tool_call.confirmation,
|
||||
self.project.read(cx).languages().clone(),
|
||||
cx,
|
||||
),
|
||||
respond_tx: tx,
|
||||
};
|
||||
|
||||
let id = self.insert_tool_call(label, status, icon, content, cx);
|
||||
let id = self.insert_tool_call(tool_call.tool_call, status, cx);
|
||||
ToolCallRequest { id, outcome: rx }
|
||||
}
|
||||
|
||||
pub fn push_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
request: acp::PushToolCallParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> acp::ToolCallId {
|
||||
let status = ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Running,
|
||||
};
|
||||
|
||||
self.insert_tool_call(label, status, icon, content, cx)
|
||||
self.insert_tool_call(request, status, cx)
|
||||
}
|
||||
|
||||
fn insert_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
tool_call: acp::PushToolCallParams,
|
||||
status: ToolCallStatus,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> acp::ToolCallId {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let id = acp::ToolCallId(self.entries.len() as u64);
|
||||
|
||||
self.push_entry(
|
||||
AgentThreadEntry::ToolCall(ToolCall {
|
||||
id,
|
||||
label: cx.new(|cx| {
|
||||
Markdown::new(label.into(), Some(language_registry.clone()), None, cx)
|
||||
}),
|
||||
icon: acp_icon_to_ui_icon(icon),
|
||||
content: content
|
||||
.map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
|
||||
status,
|
||||
let call = ToolCall {
|
||||
id,
|
||||
label: cx.new(|cx| {
|
||||
Markdown::new(
|
||||
tool_call.label.into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
icon: acp_icon_to_ui_icon(tool_call.icon),
|
||||
content: tool_call
|
||||
.content
|
||||
.map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
|
||||
locations: tool_call.locations,
|
||||
status,
|
||||
};
|
||||
|
||||
self.push_entry(AgentThreadEntry::ToolCall(call), cx);
|
||||
|
||||
id
|
||||
}
|
||||
@@ -804,14 +871,16 @@ impl AcpThread {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn initialize(&self) -> impl use<> + Future<Output = Result<acp::InitializeResponse>> {
|
||||
pub fn initialize(
|
||||
&self,
|
||||
) -> impl use<> + Future<Output = Result<acp::InitializeResponse, acp::Error>> {
|
||||
let connection = self.connection.clone();
|
||||
async move { Ok(connection.request(acp::InitializeParams).await?) }
|
||||
async move { connection.request(acp::InitializeParams).await }
|
||||
}
|
||||
|
||||
pub fn authenticate(&self) -> impl use<> + Future<Output = Result<()>> {
|
||||
pub fn authenticate(&self) -> impl use<> + Future<Output = Result<(), acp::Error>> {
|
||||
let connection = self.connection.clone();
|
||||
async move { Ok(connection.request(acp::AuthenticateParams).await?) }
|
||||
async move { connection.request(acp::AuthenticateParams).await }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -819,7 +888,7 @@ impl AcpThread {
|
||||
&mut self,
|
||||
message: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) -> BoxFuture<'static, Result<()>> {
|
||||
) -> BoxFuture<'static, Result<(), acp::Error>> {
|
||||
self.send(
|
||||
acp::SendUserMessageParams {
|
||||
chunks: vec![acp::UserMessageChunk::Text {
|
||||
@@ -834,7 +903,7 @@ impl AcpThread {
|
||||
&mut self,
|
||||
message: acp::SendUserMessageParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> BoxFuture<'static, Result<()>> {
|
||||
) -> BoxFuture<'static, Result<(), acp::Error>> {
|
||||
let agent = self.connection.clone();
|
||||
self.push_entry(
|
||||
AgentThreadEntry::UserMessage(UserMessage::from_acp(
|
||||
@@ -865,7 +934,7 @@ impl AcpThread {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<(), acp::Error>> {
|
||||
let agent = self.connection.clone();
|
||||
|
||||
if self.send_task.take().is_some() {
|
||||
@@ -898,13 +967,123 @@ impl AcpThread {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_text_file(
|
||||
&self,
|
||||
request: acp::ReadTextFileParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<String>> {
|
||||
let project = self.project.clone();
|
||||
let action_log = self.action_log.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let load = project.update(cx, |project, cx| {
|
||||
let path = project
|
||||
.project_path_for_absolute_path(&request.path, cx)
|
||||
.context("invalid path")?;
|
||||
anyhow::Ok(project.open_buffer(path, cx))
|
||||
});
|
||||
let buffer = load??.await?;
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
project.update(cx, |project, cx| {
|
||||
let position = buffer
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
.anchor_before(Point::new(request.line.unwrap_or_default(), 0));
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
|
||||
this.update(cx, |this, _| {
|
||||
let text = snapshot.text();
|
||||
this.shared_buffers.insert(buffer.clone(), snapshot);
|
||||
text
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_text_file(
|
||||
&self,
|
||||
path: PathBuf,
|
||||
content: String,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let project = self.project.clone();
|
||||
let action_log = self.action_log.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let load = project.update(cx, |project, cx| {
|
||||
let path = project
|
||||
.project_path_for_absolute_path(&path, cx)
|
||||
.context("invalid path")?;
|
||||
anyhow::Ok(project.open_buffer(path, cx))
|
||||
});
|
||||
let buffer = load??.await?;
|
||||
let snapshot = this.update(cx, |this, cx| {
|
||||
this.shared_buffers
|
||||
.get(&buffer)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| buffer.read(cx).snapshot())
|
||||
})?;
|
||||
let edits = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let old_text = snapshot.text();
|
||||
text_diff(old_text.as_str(), &content)
|
||||
.into_iter()
|
||||
.map(|(range, replacement)| {
|
||||
(
|
||||
snapshot.anchor_after(range.start)
|
||||
..snapshot.anchor_before(range.end),
|
||||
replacement,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.await;
|
||||
cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: edits
|
||||
.last()
|
||||
.map(|(range, _)| range.end)
|
||||
.unwrap_or(Anchor::MIN),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, None, cx);
|
||||
});
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_edited(buffer.clone(), cx);
|
||||
});
|
||||
})?;
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub fn child_status(&mut self) -> Option<Task<Result<()>>> {
|
||||
self.child_status.take()
|
||||
}
|
||||
@@ -930,7 +1109,7 @@ impl acp::Client for AcpClientDelegate {
|
||||
async fn stream_assistant_message_chunk(
|
||||
&self,
|
||||
params: acp::StreamAssistantMessageChunkParams,
|
||||
) -> Result<()> {
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
@@ -947,45 +1126,37 @@ impl acp::Client for AcpClientDelegate {
|
||||
async fn request_tool_call_confirmation(
|
||||
&self,
|
||||
request: acp::RequestToolCallConfirmationParams,
|
||||
) -> Result<acp::RequestToolCallConfirmationResponse> {
|
||||
) -> Result<acp::RequestToolCallConfirmationResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let ToolCallRequest { id, outcome } = cx
|
||||
.update(|cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.request_tool_call(
|
||||
request.label,
|
||||
request.icon,
|
||||
request.content,
|
||||
request.confirmation,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.request_tool_call(request, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp::RequestToolCallConfirmationResponse {
|
||||
id,
|
||||
outcome: outcome.await?,
|
||||
outcome: outcome.await.map_err(acp::Error::into_internal_error)?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn push_tool_call(
|
||||
&self,
|
||||
request: acp::PushToolCallParams,
|
||||
) -> Result<acp::PushToolCallResponse> {
|
||||
) -> Result<acp::PushToolCallResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let id = cx
|
||||
.update(|cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.push_tool_call(request.label, request.icon, request.content, cx)
|
||||
})
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.push_tool_call(request, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp::PushToolCallResponse { id })
|
||||
}
|
||||
|
||||
async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<()> {
|
||||
async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
@@ -997,6 +1168,34 @@ impl acp::Client for AcpClientDelegate {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
request: acp::ReadTextFileParams,
|
||||
) -> Result<acp::ReadTextFileResponse, acp::Error> {
|
||||
let content = self
|
||||
.cx
|
||||
.update(|cx| {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.read_text_file(request, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
Ok(acp::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn write_text_file(&self, request: acp::WriteTextFileParams) -> Result<(), acp::Error> {
|
||||
self.cx
|
||||
.update(|cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.write_text_file(request.path, request.content, cx)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn acp_icon_to_ui_icon(icon: acp::Icon) -> IconName {
|
||||
@@ -1100,6 +1299,80 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edits_concurrently_to_user(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\n"}))
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
let (thread, fake_server) = fake_acp_thread(project.clone(), cx);
|
||||
let (worktree, pathbuf) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree.read(cx).id(), pathbuf), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (read_file_tx, read_file_rx) = oneshot::channel::<()>();
|
||||
let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx)));
|
||||
|
||||
fake_server.update(cx, |fake_server, _| {
|
||||
fake_server.on_user_message(move |_, server, mut cx| {
|
||||
let read_file_tx = read_file_tx.clone();
|
||||
async move {
|
||||
let content = server
|
||||
.update(&mut cx, |server, _| {
|
||||
server.send_to_zed(acp::ReadTextFileParams {
|
||||
path: path!("/tmp/foo").into(),
|
||||
line: None,
|
||||
limit: None,
|
||||
})
|
||||
})?
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(content.content, "one\ntwo\nthree\n");
|
||||
read_file_tx.take().unwrap().send(()).unwrap();
|
||||
server
|
||||
.update(&mut cx, |server, _| {
|
||||
server.send_to_zed(acp::WriteTextFileParams {
|
||||
path: path!("/tmp/foo").into(),
|
||||
content: "one\ntwo\nthree\nfour\nfive\n".to_string(),
|
||||
})
|
||||
})?
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw("Extend the count in /tmp/foo", cx)
|
||||
});
|
||||
read_file_rx.await.ok();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "zero\n".to_string())], None, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
"zero\none\ntwo\nthree\nfour\nfive\n"
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8(fs.read_file_sync(path!("/tmp/foo")).unwrap()).unwrap(),
|
||||
"zero\none\ntwo\nthree\nfour\nfive\n"
|
||||
);
|
||||
request.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -1124,6 +1397,7 @@ mod tests {
|
||||
label: "Fetch".to_string(),
|
||||
icon: acp::Icon::Globe,
|
||||
content: None,
|
||||
locations: vec![],
|
||||
})
|
||||
})?
|
||||
.await
|
||||
@@ -1553,7 +1827,7 @@ mod tests {
|
||||
acp::SendUserMessageParams,
|
||||
Entity<FakeAcpServer>,
|
||||
AsyncApp,
|
||||
) -> LocalBoxFuture<'static, Result<()>>,
|
||||
) -> LocalBoxFuture<'static, Result<(), acp::Error>>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
@@ -1565,21 +1839,24 @@ mod tests {
|
||||
}
|
||||
|
||||
impl acp::Agent for FakeAgent {
|
||||
async fn initialize(&self) -> Result<acp::InitializeResponse> {
|
||||
async fn initialize(&self) -> Result<acp::InitializeResponse, acp::Error> {
|
||||
Ok(acp::InitializeResponse {
|
||||
is_authenticated: true,
|
||||
})
|
||||
}
|
||||
|
||||
async fn authenticate(&self) -> Result<()> {
|
||||
async fn authenticate(&self) -> Result<(), acp::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cancel_send_message(&self) -> Result<()> {
|
||||
async fn cancel_send_message(&self) -> Result<(), acp::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_user_message(&self, request: acp::SendUserMessageParams) -> Result<()> {
|
||||
async fn send_user_message(
|
||||
&self,
|
||||
request: acp::SendUserMessageParams,
|
||||
) -> Result<(), acp::Error> {
|
||||
let mut cx = self.cx.clone();
|
||||
let handler = self
|
||||
.server
|
||||
@@ -1589,7 +1866,7 @@ mod tests {
|
||||
if let Some(handler) = handler {
|
||||
handler(request, self.server.clone(), self.cx.clone()).await
|
||||
} else {
|
||||
anyhow::bail!("No handler for on_user_message")
|
||||
Err(anyhow::anyhow!("No handler for on_user_message").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1624,7 +1901,7 @@ mod tests {
|
||||
handler: impl for<'a> Fn(acp::SendUserMessageParams, Entity<FakeAcpServer>, AsyncApp) -> F
|
||||
+ 'static,
|
||||
) where
|
||||
F: Future<Output = Result<()>> + 'static,
|
||||
F: Future<Output = Result<(), acp::Error>> + 'static,
|
||||
{
|
||||
self.on_user_message
|
||||
.replace(Rc::new(move |request, server, cx| {
|
||||
|
||||
@@ -2,4 +2,5 @@ mod completion_provider;
|
||||
mod message_history;
|
||||
mod thread_view;
|
||||
|
||||
pub use message_history::MessageHistory;
|
||||
pub use thread_view::AcpThreadView;
|
||||
|
||||
@@ -3,19 +3,25 @@ pub struct MessageHistory<T> {
|
||||
current: Option<usize>,
|
||||
}
|
||||
|
||||
impl<T> MessageHistory<T> {
|
||||
pub fn new() -> Self {
|
||||
impl<T> Default for MessageHistory<T> {
|
||||
fn default() -> Self {
|
||||
MessageHistory {
|
||||
items: Vec::new(),
|
||||
current: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MessageHistory<T> {
|
||||
pub fn push(&mut self, message: T) {
|
||||
self.current.take();
|
||||
self.items.push(message);
|
||||
}
|
||||
|
||||
pub fn reset_position(&mut self) {
|
||||
self.current.take();
|
||||
}
|
||||
|
||||
pub fn prev(&mut self) -> Option<&T> {
|
||||
if self.items.is_empty() {
|
||||
return None;
|
||||
@@ -46,7 +52,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_prev_next() {
|
||||
let mut history = MessageHistory::new();
|
||||
let mut history = MessageHistory::default();
|
||||
|
||||
// Test empty history
|
||||
assert_eq!(history.prev(), None);
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use agentic_coding_protocol::{self as acp};
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
|
||||
EditorStyle, MinimapVisibility, MultiBuffer,
|
||||
EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, Focusable,
|
||||
Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement, Subscription, TextStyle,
|
||||
TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, div, list, percentage,
|
||||
prelude::*, pulsating_between,
|
||||
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
|
||||
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
|
||||
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
||||
Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
|
||||
pulsating_between,
|
||||
};
|
||||
use gpui::{FocusHandle, Task};
|
||||
use language::language_settings::SoftWrap;
|
||||
use language::{Buffer, Language};
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use settings::Settings as _;
|
||||
use text::Anchor;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Disclosure, Tooltip, prelude::*};
|
||||
use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
|
||||
|
||||
use ::acp::{
|
||||
@@ -38,6 +43,8 @@ use ::acp::{
|
||||
|
||||
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
|
||||
use crate::acp::message_history::MessageHistory;
|
||||
use crate::agent_diff::AgentDiff;
|
||||
use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll};
|
||||
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
|
||||
@@ -47,13 +54,16 @@ pub struct AcpThreadView {
|
||||
thread_state: ThreadState,
|
||||
diff_editors: HashMap<EntityId, Entity<Editor>>,
|
||||
message_editor: Entity<Editor>,
|
||||
message_set_from_history: bool,
|
||||
_message_editor_subscription: Subscription,
|
||||
mention_set: Arc<Mutex<MentionSet>>,
|
||||
last_error: Option<Entity<Markdown>>,
|
||||
list_state: ListState,
|
||||
auth_task: Option<Task<()>>,
|
||||
expanded_tool_calls: HashSet<ToolCallId>,
|
||||
expanded_thinking_blocks: HashSet<(usize, usize)>,
|
||||
message_history: MessageHistory<acp::SendUserMessageParams>,
|
||||
edits_expanded: bool,
|
||||
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -62,7 +72,7 @@ enum ThreadState {
|
||||
},
|
||||
Ready {
|
||||
thread: Entity<AcpThread>,
|
||||
_subscription: Subscription,
|
||||
_subscription: [Subscription; 2],
|
||||
},
|
||||
LoadError(LoadError),
|
||||
Unauthenticated {
|
||||
@@ -74,6 +84,7 @@ impl AcpThreadView {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -118,6 +129,17 @@ impl AcpThreadView {
|
||||
editor
|
||||
});
|
||||
|
||||
let message_editor_subscription = cx.subscribe(&message_editor, |this, _, event, _| {
|
||||
if let editor::EditorEvent::BufferEdited = &event {
|
||||
if !this.message_set_from_history {
|
||||
this.message_history.borrow_mut().reset_position();
|
||||
}
|
||||
this.message_set_from_history = false;
|
||||
}
|
||||
});
|
||||
|
||||
let mention_set = mention_set.clone();
|
||||
|
||||
let list_state = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Bottom,
|
||||
@@ -136,10 +158,12 @@ impl AcpThreadView {
|
||||
);
|
||||
|
||||
Self {
|
||||
workspace,
|
||||
workspace: workspace.clone(),
|
||||
project: project.clone(),
|
||||
thread_state: Self::initial_state(project, window, cx),
|
||||
thread_state: Self::initial_state(workspace, project, window, cx),
|
||||
message_editor,
|
||||
message_set_from_history: false,
|
||||
_message_editor_subscription: message_editor_subscription,
|
||||
mention_set,
|
||||
diff_editors: Default::default(),
|
||||
list_state: list_state,
|
||||
@@ -147,11 +171,13 @@ impl AcpThreadView {
|
||||
auth_task: None,
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
expanded_thinking_blocks: HashSet::default(),
|
||||
message_history: MessageHistory::new(),
|
||||
edits_expanded: false,
|
||||
message_history,
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_state(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -219,15 +245,23 @@ impl AcpThreadView {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
let subscription =
|
||||
let thread_subscription =
|
||||
cx.subscribe_in(&thread, window, Self::handle_thread_event);
|
||||
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
let action_log_subscription =
|
||||
cx.observe(&action_log, |_, _, cx| cx.notify());
|
||||
|
||||
this.list_state
|
||||
.splice(0..0, thread.read(cx).entries().len());
|
||||
|
||||
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
||||
|
||||
this.thread_state = ThreadState::Ready {
|
||||
thread,
|
||||
_subscription: subscription,
|
||||
_subscription: [thread_subscription, action_log_subscription],
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -250,7 +284,7 @@ impl AcpThreadView {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn thread(&self) -> Option<&Entity<AcpThread>> {
|
||||
pub fn thread(&self) -> Option<&Entity<AcpThread>> {
|
||||
match &self.thread_state {
|
||||
ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
|
||||
Some(thread)
|
||||
@@ -281,7 +315,6 @@ impl AcpThreadView {
|
||||
|
||||
let mut ix = 0;
|
||||
let mut chunks: Vec<acp::UserMessageChunk> = Vec::new();
|
||||
|
||||
let project = self.project.clone();
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
@@ -342,7 +375,7 @@ impl AcpThreadView {
|
||||
editor.remove_creases(mention_set.lock().drain(), cx)
|
||||
});
|
||||
|
||||
self.message_history.push(message);
|
||||
self.message_history.borrow_mut().push(message);
|
||||
}
|
||||
|
||||
fn previous_history_message(
|
||||
@@ -351,11 +384,11 @@ impl AcpThreadView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
Self::set_draft_message(
|
||||
self.message_set_from_history = Self::set_draft_message(
|
||||
self.message_editor.clone(),
|
||||
self.mention_set.clone(),
|
||||
self.project.clone(),
|
||||
self.message_history.prev(),
|
||||
self.message_history.borrow_mut().prev(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -367,11 +400,11 @@ impl AcpThreadView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
Self::set_draft_message(
|
||||
self.message_set_from_history = Self::set_draft_message(
|
||||
self.message_editor.clone(),
|
||||
self.mention_set.clone(),
|
||||
self.project.clone(),
|
||||
self.message_history.next(),
|
||||
self.message_history.borrow_mut().next(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -384,15 +417,11 @@ impl AcpThreadView {
|
||||
message: Option<&acp::SendUserMessageParams>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> bool {
|
||||
cx.notify();
|
||||
|
||||
let Some(message) = message else {
|
||||
message_editor.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.remove_creases(mention_set.lock().drain(), cx)
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut text = String::new();
|
||||
@@ -452,6 +481,35 @@ impl AcpThreadView {
|
||||
mention_set.lock().insert(crease_id, project_path);
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(thread) = self.thread() {
|
||||
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn open_edited_buffer(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(thread) = self.thread() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(diff) =
|
||||
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
@@ -464,7 +522,8 @@ impl AcpThreadView {
|
||||
let count = self.list_state.item_count();
|
||||
match event {
|
||||
AcpThreadEvent::NewEntry => {
|
||||
self.sync_thread_entry_view(thread.read(cx).entries().len() - 1, window, cx);
|
||||
let index = thread.read(cx).entries().len() - 1;
|
||||
self.sync_thread_entry_view(index, window, cx);
|
||||
self.list_state.splice(count..count, 1);
|
||||
}
|
||||
AcpThreadEvent::EntryUpdated(index) => {
|
||||
@@ -537,15 +596,7 @@ impl AcpThreadView {
|
||||
|
||||
fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
|
||||
let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
|
||||
if let AgentThreadEntry::ToolCall(ToolCall {
|
||||
content: Some(ToolCallContent::Diff { diff }),
|
||||
..
|
||||
}) = &entry
|
||||
{
|
||||
Some(diff.multibuffer.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
entry.diff().map(|diff| diff.multibuffer.clone())
|
||||
}
|
||||
|
||||
fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -566,7 +617,8 @@ impl AcpThreadView {
|
||||
Markdown::new(format!("Error: {err}").into(), None, None, cx)
|
||||
}))
|
||||
} else {
|
||||
this.thread_state = Self::initial_state(project.clone(), window, cx)
|
||||
this.thread_state =
|
||||
Self::initial_state(this.workspace.clone(), project.clone(), window, cx)
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
@@ -873,10 +925,43 @@ impl AcpThreadView {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(self.render_markdown(
|
||||
tool_call.label.clone(),
|
||||
default_markdown_style(needs_confirmation, window, cx),
|
||||
)),
|
||||
.child(if tool_call.locations.len() == 1 {
|
||||
let name = tool_call.locations[0]
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
h_flex()
|
||||
.id(("open-tool-call-location", entry_ix))
|
||||
.child(name)
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.pr_1()
|
||||
.gap_0p5()
|
||||
.cursor_pointer()
|
||||
.rounded_sm()
|
||||
.opacity(0.8)
|
||||
.hover(|label| {
|
||||
label.opacity(1.).bg(cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_hover
|
||||
.opacity(0.5))
|
||||
})
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.open_tool_call_location(entry_ix, 0, window, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
self.render_markdown(
|
||||
tool_call.label.clone(),
|
||||
default_markdown_style(needs_confirmation, window, cx),
|
||||
)
|
||||
.into_any()
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -936,15 +1021,19 @@ impl AcpThreadView {
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
match content {
|
||||
ToolCallContent::Markdown { markdown } => self
|
||||
.render_markdown(markdown.clone(), default_markdown_style(false, window, cx))
|
||||
.into_any_element(),
|
||||
ToolCallContent::Markdown { markdown } => {
|
||||
div()
|
||||
.p_2()
|
||||
.child(self.render_markdown(
|
||||
markdown.clone(),
|
||||
default_markdown_style(false, window, cx),
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
ToolCallContent::Diff {
|
||||
diff: Diff {
|
||||
path, multibuffer, ..
|
||||
},
|
||||
diff: Diff { multibuffer, .. },
|
||||
..
|
||||
} => self.render_diff_editor(multibuffer, path),
|
||||
} => self.render_diff_editor(multibuffer),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1364,10 +1453,9 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>, path: &Path) -> AnyElement {
|
||||
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
|
||||
v_flex()
|
||||
.h_full()
|
||||
.child(path.to_string_lossy().to_string())
|
||||
.child(
|
||||
if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
|
||||
editor.clone().into_any_element()
|
||||
@@ -1529,6 +1617,357 @@ impl AcpThreadView {
|
||||
container.into_any()
|
||||
}
|
||||
|
||||
fn render_edits_bar(
|
||||
&self,
|
||||
thread_entity: &Entity<AcpThread>,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let thread = thread_entity.read(cx);
|
||||
let action_log = thread.action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
|
||||
if changed_buffers.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let active_color = cx.theme().colors().element_selected;
|
||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||
|
||||
let pending_edits = thread.has_pending_edit_tool_calls();
|
||||
let expanded = self.edits_expanded;
|
||||
|
||||
v_flex()
|
||||
.mt_1()
|
||||
.mx_2()
|
||||
.bg(bg_edit_files_disclosure)
|
||||
.border_1()
|
||||
.border_b_0()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_t_md()
|
||||
.shadow(vec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.15),
|
||||
offset: point(px(1.), px(-1.)),
|
||||
blur_radius: px(3.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(self.render_edits_bar_summary(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
expanded,
|
||||
pending_edits,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(expanded, |parent| {
|
||||
parent.child(self.render_edits_bar_files(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
pending_edits,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.into_any()
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_edits_bar_summary(
|
||||
&self,
|
||||
action_log: &Entity<ActionLog>,
|
||||
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
|
||||
expanded: bool,
|
||||
pending_edits: bool,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
h_flex()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.when(expanded, |this| {
|
||||
this.border_b_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id("edits-container")
|
||||
.cursor_pointer()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Disclosure::new("edits-disclosure", expanded))
|
||||
.map(|this| {
|
||||
if pending_edits {
|
||||
this.child(
|
||||
Label::new(format!(
|
||||
"Editing {} {}…",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"edit-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.3, 0.7)),
|
||||
|label, delta| label.alpha(delta),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
Label::new("Edits")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.edits_expanded = !this.edits_expanded;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("review-changes", IconName::ListTodo)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Review Changes",
|
||||
&OpenAgentDiff,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
|
||||
})),
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::Border))
|
||||
.child(
|
||||
Button::new("reject-all-changes", "Reject All")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.when(pending_edits, |this| {
|
||||
this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
|
||||
})
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&RejectAll,
|
||||
&focus_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click({
|
||||
let action_log = action_log.clone();
|
||||
cx.listener(move |_, _, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.reject_all_edits(cx).detach();
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("keep-all-changes", "Keep All")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.when(pending_edits, |this| {
|
||||
this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
|
||||
})
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click({
|
||||
let action_log = action_log.clone();
|
||||
cx.listener(move |_, _, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.keep_all_edits(cx);
|
||||
})
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_edits_bar_files(
|
||||
&self,
|
||||
action_log: &Entity<ActionLog>,
|
||||
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
|
||||
pending_edits: bool,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
|
||||
v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
|
||||
|(index, (buffer, _diff))| {
|
||||
let file = buffer.read(cx).file()?;
|
||||
let path = file.path();
|
||||
|
||||
let file_path = path.parent().and_then(|parent| {
|
||||
let parent_str = parent.to_string_lossy();
|
||||
|
||||
if parent_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let file_name = path.file_name().map(|name| {
|
||||
Label::new(name.to_string_lossy().to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx)
|
||||
});
|
||||
|
||||
let file_icon = FileIcons::get_icon(&path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
|
||||
.unwrap_or_else(|| {
|
||||
Icon::new(IconName::File)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
});
|
||||
|
||||
let overlay_gradient = linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(editor_bg_color, 1.),
|
||||
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
|
||||
);
|
||||
|
||||
let element = h_flex()
|
||||
.group("edited-code")
|
||||
.id(("file-container", index))
|
||||
.relative()
|
||||
.py_1()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.bg(editor_bg_color)
|
||||
.when(index < changed_buffers.len() - 1, |parent| {
|
||||
parent.border_color(cx.theme().colors().border).border_b_1()
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id(("file-name", index))
|
||||
.pr_8()
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(file_icon)
|
||||
.child(h_flex().gap_0p5().children(file_name).children(file_path))
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.open_edited_buffer(&buffer, window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.visible_on_hover("edited-code")
|
||||
.child(
|
||||
Button::new("review", "Review")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.open_edited_buffer(&buffer, window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::BorderVariant))
|
||||
.child(
|
||||
Button::new("reject-file", "Reject")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
let action_log = action_log.clone();
|
||||
move |_, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log
|
||||
.reject_edits_in_ranges(
|
||||
buffer.clone(),
|
||||
vec![Anchor::MIN..Anchor::MAX],
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("keep-file", "Keep")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
let action_log = action_log.clone();
|
||||
move |_, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.keep_edits_in_range(
|
||||
buffer.clone(),
|
||||
Anchor::MIN..Anchor::MAX,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("gradient-overlay")
|
||||
.absolute()
|
||||
.h_full()
|
||||
.w_12()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.right(px(152.))
|
||||
.bg(overlay_gradient),
|
||||
);
|
||||
|
||||
Some(element)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = TextSize::Small
|
||||
@@ -1559,6 +1998,76 @@ impl AcpThreadView {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
if self.thread().map_or(true, |thread| {
|
||||
thread.read(cx).status() == ThreadStatus::Idle
|
||||
}) {
|
||||
let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
|
||||
IconButton::new("send-message", IconName::Send)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(self.thread().is_none() || is_editor_empty)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.chat(&Chat, window, cx);
|
||||
}))
|
||||
.when(!is_editor_empty, |button| {
|
||||
button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button.tooltip(Tooltip::text("Type a message to submit"))
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
IconButton::new("stop-generation", IconName::StopFilled)
|
||||
.icon_color(Color::Error)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
|
||||
})
|
||||
.on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let following = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.is_being_followed(CollaboratorId::Agent)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
IconButton::new("follow-agent", IconName::Crosshair)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.toggle_state(following)
|
||||
.selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
|
||||
.tooltip(move |window, cx| {
|
||||
if following {
|
||||
Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
|
||||
} else {
|
||||
Tooltip::with_meta(
|
||||
"Follow Agent",
|
||||
Some(&Follow),
|
||||
"Track the agent's location as it reads and edits files.",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if following {
|
||||
workspace.unfollow(CollaboratorId::Agent, window, cx);
|
||||
} else {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
|
||||
let workspace = self.workspace.clone();
|
||||
MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
|
||||
@@ -1603,6 +2112,64 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
fn open_tool_call_location(
|
||||
&self,
|
||||
entry_ix: usize,
|
||||
location_ix: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
let location = self
|
||||
.thread()?
|
||||
.read(cx)
|
||||
.entries()
|
||||
.get(entry_ix)?
|
||||
.locations()?
|
||||
.get(location_ix)?;
|
||||
|
||||
let project_path = self
|
||||
.project
|
||||
.read(cx)
|
||||
.find_project_path(&location.path, cx)?;
|
||||
|
||||
let open_task = self
|
||||
.workspace
|
||||
.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?;
|
||||
|
||||
let Some(active_editor) = item.downcast::<Editor>() else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
|
||||
active_editor.update_in(cx, |editor, window, cx| {
|
||||
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([first_hunk_start..first_hunk_start]);
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn open_thread_as_markdown(
|
||||
&self,
|
||||
workspace: Entity<Workspace>,
|
||||
@@ -1673,10 +2240,6 @@ impl Focusable for AcpThreadView {
|
||||
|
||||
impl Render for AcpThreadView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let text = self.message_editor.read(cx).text(cx);
|
||||
let is_editor_empty = text.is_empty();
|
||||
let focus_handle = self.message_editor.focus_handle(cx);
|
||||
|
||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
@@ -1702,6 +2265,7 @@ impl Render for AcpThreadView {
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::previous_history_message))
|
||||
.on_action(cx.listener(Self::next_history_message))
|
||||
.on_action(cx.listener(Self::open_agent_diff))
|
||||
.child(match &self.thread_state {
|
||||
ThreadState::Unauthenticated { .. } => v_flex()
|
||||
.p_2()
|
||||
@@ -1755,6 +2319,7 @@ impl Render for AcpThreadView {
|
||||
.child(LoadingLabel::new("").size(LabelSize::Small))
|
||||
.into(),
|
||||
})
|
||||
.children(self.render_edits_bar(&thread, window, cx))
|
||||
} else {
|
||||
this.child(self.render_empty_state(false, cx))
|
||||
}
|
||||
@@ -1782,47 +2347,12 @@ impl Render for AcpThreadView {
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.render_message_editor(cx))
|
||||
.child({
|
||||
let thread = self.thread();
|
||||
|
||||
h_flex().justify_end().child(
|
||||
if thread.map_or(true, |thread| {
|
||||
thread.read(cx).status() == ThreadStatus::Idle
|
||||
}) {
|
||||
IconButton::new("send-message", IconName::Send)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(thread.is_none() || is_editor_empty)
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&Chat, window, cx);
|
||||
}
|
||||
})
|
||||
.when(!is_editor_empty, |button| {
|
||||
button.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Send", &Chat, window, cx)
|
||||
})
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button.tooltip(Tooltip::text("Type a message to submit"))
|
||||
})
|
||||
} else {
|
||||
IconButton::new("stop-generation", IconName::StopFilled)
|
||||
.icon_color(Color::Error)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Stop Generation",
|
||||
&editor::actions::Cancel,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
|
||||
},
|
||||
)
|
||||
}),
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(self.render_follow_toggle(cx))
|
||||
.child(self.render_send_button(cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
|
||||
use agent::{Thread, ThreadEvent};
|
||||
use acp::{AcpThread, AcpThreadEvent};
|
||||
use agent::{Thread, ThreadEvent, ThreadSummary};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
@@ -41,16 +43,108 @@ use zed_actions::assistant::ToggleFocus;
|
||||
pub struct AgentDiffPane {
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
thread: Entity<Thread>,
|
||||
thread: AgentDiffThread,
|
||||
focus_handle: FocusHandle,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
title: SharedString,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum AgentDiffThread {
|
||||
Native(Entity<Thread>),
|
||||
AcpThread(Entity<AcpThread>),
|
||||
}
|
||||
|
||||
impl AgentDiffThread {
|
||||
fn project(&self, cx: &App) -> Entity<Project> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).project().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
|
||||
}
|
||||
}
|
||||
fn action_log(&self, cx: &App) -> Entity<ActionLog> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).action_log().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn summary(&self, cx: &App) -> ThreadSummary {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).summary().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => ThreadSummary::Ready(thread.read(cx).title()),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_generating(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).is_generating(),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
thread.read(cx).status() == acp::ThreadStatus::Generating
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).has_pending_edit_tool_uses(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
|
||||
}
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> WeakAgentDiffThread {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => WeakAgentDiffThread::Native(thread.downgrade()),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
WeakAgentDiffThread::AcpThread(thread.downgrade())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<Thread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<Thread>) -> Self {
|
||||
AgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<AcpThread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<AcpThread>) -> Self {
|
||||
AgentDiffThread::AcpThread(entity)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum WeakAgentDiffThread {
|
||||
Native(WeakEntity<Thread>),
|
||||
AcpThread(WeakEntity<AcpThread>),
|
||||
}
|
||||
|
||||
impl WeakAgentDiffThread {
|
||||
pub fn upgrade(&self) -> Option<AgentDiffThread> {
|
||||
match self {
|
||||
WeakAgentDiffThread::Native(weak) => weak.upgrade().map(AgentDiffThread::Native),
|
||||
WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<Thread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<Thread>) -> Self {
|
||||
WeakAgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<AcpThread>) -> Self {
|
||||
WeakAgentDiffThread::AcpThread(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentDiffPane {
|
||||
pub fn deploy(
|
||||
thread: Entity<Thread>,
|
||||
thread: impl Into<AgentDiffThread>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -61,14 +155,16 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
pub fn deploy_in_workspace(
|
||||
thread: Entity<Thread>,
|
||||
thread: impl Into<AgentDiffThread>,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
let thread = thread.into();
|
||||
let existing_diff = workspace
|
||||
.items_of_type::<AgentDiffPane>(cx)
|
||||
.find(|diff| diff.read(cx).thread == thread);
|
||||
|
||||
if let Some(existing_diff) = existing_diff {
|
||||
workspace.activate_item(&existing_diff, true, true, window, cx);
|
||||
existing_diff
|
||||
@@ -81,7 +177,7 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
thread: Entity<Thread>,
|
||||
thread: AgentDiffThread,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -89,7 +185,7 @@ impl AgentDiffPane {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
|
||||
let project = thread.read(cx).project().clone();
|
||||
let project = thread.project(cx).clone();
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
||||
@@ -100,16 +196,27 @@ impl AgentDiffPane {
|
||||
editor
|
||||
});
|
||||
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
let action_log = thread.action_log(cx).clone();
|
||||
|
||||
let mut this = Self {
|
||||
_subscriptions: vec![
|
||||
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
|
||||
this.update_excerpts(window, cx)
|
||||
}),
|
||||
cx.subscribe(&thread, |this, _thread, event, cx| {
|
||||
this.handle_thread_event(event, cx)
|
||||
}),
|
||||
],
|
||||
_subscriptions: [
|
||||
Some(
|
||||
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
|
||||
this.update_excerpts(window, cx)
|
||||
}),
|
||||
),
|
||||
match &thread {
|
||||
AgentDiffThread::Native(thread) => {
|
||||
Some(cx.subscribe(&thread, |this, _thread, event, cx| {
|
||||
this.handle_thread_event(event, cx)
|
||||
}))
|
||||
}
|
||||
AgentDiffThread::AcpThread(_) => None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
title: SharedString::default(),
|
||||
multibuffer,
|
||||
editor,
|
||||
@@ -123,8 +230,7 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let thread = self.thread.read(cx);
|
||||
let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
|
||||
let changed_buffers = self.thread.action_log(cx).read(cx).changed_buffers(cx);
|
||||
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||
|
||||
for (buffer, diff_handle) in changed_buffers {
|
||||
@@ -211,7 +317,7 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn update_title(&mut self, cx: &mut Context<Self>) {
|
||||
let new_title = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
let new_title = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
if new_title != self.title {
|
||||
self.title = new_title;
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
@@ -275,14 +381,15 @@ impl AgentDiffPane {
|
||||
|
||||
fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.keep_all_edits(cx));
|
||||
.action_log(cx)
|
||||
.update(cx, |action_log, cx| action_log.keep_all_edits(cx))
|
||||
}
|
||||
}
|
||||
|
||||
fn keep_edits_in_selection(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
@@ -297,7 +404,7 @@ fn keep_edits_in_selection(
|
||||
fn reject_edits_in_selection(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
@@ -311,7 +418,7 @@ fn reject_edits_in_selection(
|
||||
fn keep_edits_in_ranges(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
ranges: Vec<Range<editor::Anchor>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
@@ -326,8 +433,8 @@ fn keep_edits_in_ranges(
|
||||
for hunk in &diff_hunks_in_ranges {
|
||||
let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
|
||||
thread.action_log(cx).update(cx, |action_log, cx| {
|
||||
action_log.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -336,7 +443,7 @@ fn keep_edits_in_ranges(
|
||||
fn reject_edits_in_ranges(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
ranges: Vec<Range<editor::Anchor>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
@@ -362,8 +469,9 @@ fn reject_edits_in_ranges(
|
||||
|
||||
for (buffer, ranges) in ranges_by_buffer {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.reject_edits_in_ranges(buffer, ranges, cx)
|
||||
.action_log(cx)
|
||||
.update(cx, |action_log, cx| {
|
||||
action_log.reject_edits_in_ranges(buffer, ranges, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -461,7 +569,7 @@ impl Item for AgentDiffPane {
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||
let summary = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
let summary = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
Label::new(format!("Review: {}", summary))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
@@ -641,7 +749,7 @@ impl Render for AgentDiffPane {
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_hunk_controls(thread: &Entity<Thread>) -> editor::RenderDiffHunkControlsFn {
|
||||
fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControlsFn {
|
||||
let thread = thread.clone();
|
||||
|
||||
Arc::new(
|
||||
@@ -676,7 +784,7 @@ fn render_diff_hunk_controls(
|
||||
hunk_range: Range<editor::Anchor>,
|
||||
is_created_file: bool,
|
||||
line_height: Pixels,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
editor: &Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -1112,11 +1220,8 @@ impl Render for AgentDiffToolbar {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let has_pending_edit_tool_use = agent_diff
|
||||
.read(cx)
|
||||
.thread
|
||||
.read(cx)
|
||||
.has_pending_edit_tool_uses();
|
||||
let has_pending_edit_tool_use =
|
||||
agent_diff.read(cx).thread.has_pending_edit_tool_uses(cx);
|
||||
|
||||
if has_pending_edit_tool_use {
|
||||
return div().px_2().child(spinner_icon).into_any();
|
||||
@@ -1187,8 +1292,8 @@ pub enum EditorState {
|
||||
}
|
||||
|
||||
struct WorkspaceThread {
|
||||
thread: WeakEntity<Thread>,
|
||||
_thread_subscriptions: [Subscription; 2],
|
||||
thread: WeakAgentDiffThread,
|
||||
_thread_subscriptions: (Subscription, Subscription),
|
||||
singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
|
||||
_settings_subscription: Subscription,
|
||||
_workspace_subscription: Option<Subscription>,
|
||||
@@ -1212,23 +1317,23 @@ impl AgentDiff {
|
||||
|
||||
pub fn set_active_thread(
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: impl Into<AgentDiffThread>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
Self::global(cx).update(cx, |this, cx| {
|
||||
this.register_active_thread_impl(workspace, thread, window, cx);
|
||||
this.register_active_thread_impl(workspace, thread.into(), window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn register_active_thread_impl(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
let action_log = thread.action_log(cx).clone();
|
||||
|
||||
let action_log_subscription = cx.observe_in(&action_log, window, {
|
||||
let workspace = workspace.clone();
|
||||
@@ -1237,17 +1342,25 @@ impl AgentDiff {
|
||||
}
|
||||
});
|
||||
|
||||
let thread_subscription = cx.subscribe_in(&thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, _thread, event, window, cx| {
|
||||
this.handle_thread_event(&workspace, event, window, cx)
|
||||
}
|
||||
});
|
||||
let thread_subscription = match &thread {
|
||||
AgentDiffThread::Native(thread) => cx.subscribe_in(&thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, _thread, event, window, cx| {
|
||||
this.handle_native_thread_event(&workspace, event, window, cx)
|
||||
}
|
||||
}),
|
||||
AgentDiffThread::AcpThread(thread) => cx.subscribe_in(&thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, thread, event, window, cx| {
|
||||
this.handle_acp_thread_event(&workspace, thread, event, window, cx)
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
if let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) {
|
||||
// replace thread and action log subscription, but keep editors
|
||||
workspace_thread.thread = thread.downgrade();
|
||||
workspace_thread._thread_subscriptions = [action_log_subscription, thread_subscription];
|
||||
workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription);
|
||||
self.update_reviewing_editors(&workspace, window, cx);
|
||||
return;
|
||||
}
|
||||
@@ -1272,7 +1385,7 @@ impl AgentDiff {
|
||||
workspace.clone(),
|
||||
WorkspaceThread {
|
||||
thread: thread.downgrade(),
|
||||
_thread_subscriptions: [action_log_subscription, thread_subscription],
|
||||
_thread_subscriptions: (action_log_subscription, thread_subscription),
|
||||
singleton_editors: HashMap::default(),
|
||||
_settings_subscription: settings_subscription,
|
||||
_workspace_subscription: workspace_subscription,
|
||||
@@ -1319,7 +1432,7 @@ impl AgentDiff {
|
||||
|
||||
fn register_review_action<T: Action>(
|
||||
workspace: &mut Workspace,
|
||||
review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState
|
||||
review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState
|
||||
+ 'static,
|
||||
this: &Entity<AgentDiff>,
|
||||
) {
|
||||
@@ -1338,7 +1451,7 @@ impl AgentDiff {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
fn handle_native_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
event: &ThreadEvent,
|
||||
@@ -1380,6 +1493,40 @@ impl AgentDiff {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_acp_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
thread: &Entity<AcpThread>,
|
||||
event: &AcpThreadEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
AcpThreadEvent::NewEntry => {
|
||||
if thread
|
||||
.read(cx)
|
||||
.entries()
|
||||
.last()
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
AcpThreadEvent::EntryUpdated(ix) => {
|
||||
if thread
|
||||
.read(cx)
|
||||
.entries()
|
||||
.get(*ix)
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_workspace_event(
|
||||
&mut self,
|
||||
workspace: &Entity<Workspace>,
|
||||
@@ -1485,7 +1632,7 @@ impl AgentDiff {
|
||||
return;
|
||||
};
|
||||
|
||||
let action_log = thread.read(cx).action_log();
|
||||
let action_log = thread.action_log(cx);
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
|
||||
let mut unaffected = self.reviewing_editors.clone();
|
||||
@@ -1510,7 +1657,7 @@ impl AgentDiff {
|
||||
multibuffer.add_diff(diff_handle.clone(), cx);
|
||||
});
|
||||
|
||||
let new_state = if thread.read(cx).is_generating() {
|
||||
let new_state = if thread.is_generating(cx) {
|
||||
EditorState::Generating
|
||||
} else {
|
||||
EditorState::Reviewing
|
||||
@@ -1606,7 +1753,7 @@ impl AgentDiff {
|
||||
|
||||
fn keep_all(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
@@ -1626,7 +1773,7 @@ impl AgentDiff {
|
||||
|
||||
fn reject_all(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
@@ -1646,7 +1793,7 @@ impl AgentDiff {
|
||||
|
||||
fn keep(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
@@ -1659,7 +1806,7 @@ impl AgentDiff {
|
||||
|
||||
fn reject(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
@@ -1682,7 +1829,7 @@ impl AgentDiff {
|
||||
fn review_in_active_editor(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState,
|
||||
review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
@@ -1703,7 +1850,7 @@ impl AgentDiff {
|
||||
|
||||
if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) {
|
||||
if let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
|
||||
let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx);
|
||||
let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
|
||||
|
||||
let mut keys = changed_buffers.keys().cycle();
|
||||
keys.find(|k| *k == &curr_buffer);
|
||||
@@ -1801,8 +1948,9 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
let thread =
|
||||
AgentDiffThread::Native(thread_store.update(cx, |store, cx| store.create_thread(cx)));
|
||||
let action_log = cx.read(|cx| thread.action_log(cx));
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
@@ -1988,8 +2136,9 @@ mod tests {
|
||||
});
|
||||
|
||||
// Set the active thread
|
||||
let thread = AgentDiffThread::Native(thread);
|
||||
cx.update(|window, cx| {
|
||||
AgentDiff::set_active_thread(&workspace.downgrade(), &thread, window, cx)
|
||||
AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
|
||||
});
|
||||
|
||||
let buffer1 = project
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
@@ -8,6 +9,7 @@ use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::NewAcpThread;
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
@@ -432,6 +434,8 @@ pub struct AgentPanel {
|
||||
configuration_subscription: Option<Subscription>,
|
||||
local_timezone: UtcOffset,
|
||||
active_view: ActiveView,
|
||||
acp_message_history:
|
||||
Rc<RefCell<crate::acp::MessageHistory<agentic_coding_protocol::SendUserMessageParams>>>,
|
||||
previous_view: Option<ActiveView>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
history: Entity<ThreadHistory>,
|
||||
@@ -624,7 +628,7 @@ impl AgentPanel {
|
||||
}
|
||||
};
|
||||
|
||||
AgentDiff::set_active_thread(&workspace, &thread, window, cx);
|
||||
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
||||
|
||||
let weak_panel = weak_self.clone();
|
||||
|
||||
@@ -698,6 +702,7 @@ impl AgentPanel {
|
||||
.unwrap(),
|
||||
inline_assist_context_store,
|
||||
previous_view: None,
|
||||
acp_message_history: Default::default(),
|
||||
history_store: history_store.clone(),
|
||||
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
|
||||
hovered_recent_history_item: None,
|
||||
@@ -845,7 +850,7 @@ impl AgentPanel {
|
||||
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
||||
self.set_active_view(thread_view, window, cx);
|
||||
|
||||
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
|
||||
}
|
||||
|
||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -887,14 +892,30 @@ impl AgentPanel {
|
||||
fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
let message_history = self.acp_message_history.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let thread_view = cx.new_window_entity(|window, cx| {
|
||||
crate::acp::AcpThreadView::new(workspace, project, window, cx)
|
||||
crate::acp::AcpThreadView::new(
|
||||
workspace.clone(),
|
||||
project,
|
||||
message_history,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx);
|
||||
this.set_active_view(
|
||||
ActiveView::AcpThread {
|
||||
thread_view: thread_view.clone(),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -1050,7 +1071,7 @@ impl AgentPanel {
|
||||
|
||||
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
||||
self.set_active_view(thread_view, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
|
||||
}
|
||||
|
||||
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -1181,7 +1202,12 @@ impl AgentPanel {
|
||||
let thread = thread.read(cx).thread().clone();
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
|
||||
AgentDiffPane::deploy_in_workspace(
|
||||
AgentDiffThread::Native(thread),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@@ -1417,6 +1443,8 @@ impl AgentPanel {
|
||||
self.active_view = new_view;
|
||||
}
|
||||
|
||||
self.acp_message_history.borrow_mut().reset_position();
|
||||
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::agent_model_selector::AgentModelSelector;
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
@@ -475,9 +476,12 @@ impl MessageEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Ok(diff) =
|
||||
AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
|
||||
{
|
||||
if let Ok(diff) = AgentDiffPane::deploy(
|
||||
AgentDiffThread::Native(self.thread.clone()),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
|
||||
diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
|
||||
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
|
||||
use std::{cmp, ops::Range, sync::Arc};
|
||||
use text::{Edit, Patch, Rope};
|
||||
use util::RangeExt;
|
||||
use util::{RangeExt, ResultExt as _};
|
||||
|
||||
/// Tracks actions performed by tools in a thread
|
||||
pub struct ActionLog {
|
||||
@@ -47,6 +47,10 @@ impl ActionLog {
|
||||
self.edited_since_project_diagnostics_check
|
||||
}
|
||||
|
||||
pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
|
||||
Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
|
||||
}
|
||||
|
||||
fn track_buffer_internal(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
@@ -715,6 +719,22 @@ impl ActionLog {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reject_all_edits(&mut self, cx: &mut Context<Self>) -> Task<()> {
|
||||
let futures = self.changed_buffers(cx).into_keys().map(|buffer| {
|
||||
let reject = self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx);
|
||||
|
||||
async move {
|
||||
reject.await.log_err();
|
||||
}
|
||||
});
|
||||
|
||||
let task = futures::future::join_all(futures);
|
||||
|
||||
cx.spawn(async move |_, _| {
|
||||
task.await;
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the set of buffers that contain edits that haven't been reviewed by the user.
|
||||
pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
|
||||
self.tracked_buffers
|
||||
|
||||
@@ -19,8 +19,8 @@ use crate::stripe_client::{
|
||||
StripeCustomerId, StripeCustomerUpdate, StripeCustomerUpdateAddress, StripeCustomerUpdateName,
|
||||
StripeMeter, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId,
|
||||
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems,
|
||||
UpdateSubscriptionParams,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection,
|
||||
UpdateSubscriptionItems, UpdateSubscriptionParams,
|
||||
};
|
||||
|
||||
pub struct StripeBilling {
|
||||
@@ -252,6 +252,7 @@ impl StripeBilling {
|
||||
name: Some(StripeCustomerUpdateName::Auto),
|
||||
shipping: None,
|
||||
});
|
||||
params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true });
|
||||
|
||||
let session = self.client.create_checkout_session(params).await?;
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
@@ -311,6 +312,7 @@ impl StripeBilling {
|
||||
name: Some(StripeCustomerUpdateName::Auto),
|
||||
shipping: None,
|
||||
});
|
||||
params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true });
|
||||
|
||||
let session = self.client.create_checkout_session(params).await?;
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
|
||||
@@ -190,6 +190,7 @@ pub struct StripeCreateCheckoutSessionParams<'a> {
|
||||
pub success_url: Option<&'a str>,
|
||||
pub billing_address_collection: Option<StripeBillingAddressCollection>,
|
||||
pub customer_update: Option<StripeCustomerUpdate>,
|
||||
pub tax_id_collection: Option<StripeTaxIdCollection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
@@ -218,6 +219,11 @@ pub struct StripeCreateCheckoutSessionSubscriptionData {
|
||||
pub trial_settings: Option<StripeSubscriptionTrialSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct StripeTaxIdCollection {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StripeCheckoutSession {
|
||||
pub url: Option<String>,
|
||||
|
||||
@@ -14,8 +14,8 @@ use crate::stripe_client::{
|
||||
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
|
||||
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
|
||||
StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription,
|
||||
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, UpdateCustomerParams,
|
||||
UpdateSubscriptionParams,
|
||||
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeTaxIdCollection,
|
||||
UpdateCustomerParams, UpdateSubscriptionParams,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -38,6 +38,7 @@ pub struct StripeCreateCheckoutSessionCall {
|
||||
pub success_url: Option<String>,
|
||||
pub billing_address_collection: Option<StripeBillingAddressCollection>,
|
||||
pub customer_update: Option<StripeCustomerUpdate>,
|
||||
pub tax_id_collection: Option<StripeTaxIdCollection>,
|
||||
}
|
||||
|
||||
pub struct FakeStripeClient {
|
||||
@@ -236,6 +237,7 @@ impl StripeClient for FakeStripeClient {
|
||||
success_url: params.success_url.map(|url| url.to_string()),
|
||||
billing_address_collection: params.billing_address_collection,
|
||||
customer_update: params.customer_update,
|
||||
tax_id_collection: params.tax_id_collection,
|
||||
});
|
||||
|
||||
Ok(StripeCheckoutSession {
|
||||
|
||||
@@ -27,8 +27,8 @@ use crate::stripe_client::{
|
||||
StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription,
|
||||
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId,
|
||||
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateCustomerParams,
|
||||
UpdateSubscriptionParams,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection,
|
||||
UpdateCustomerParams, UpdateSubscriptionParams,
|
||||
};
|
||||
|
||||
pub struct RealStripeClient {
|
||||
@@ -448,6 +448,7 @@ impl<'a> TryFrom<StripeCreateCheckoutSessionParams<'a>> for CreateCheckoutSessio
|
||||
success_url: value.success_url,
|
||||
billing_address_collection: value.billing_address_collection.map(Into::into),
|
||||
customer_update: value.customer_update.map(Into::into),
|
||||
tax_id_collection: value.tax_id_collection.map(Into::into),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -590,3 +591,11 @@ impl From<StripeCustomerUpdate> for stripe::CreateCheckoutSessionCustomerUpdate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StripeTaxIdCollection> for stripe::CreateCheckoutSessionTaxIdCollection {
|
||||
fn from(value: StripeTaxIdCollection) -> Self {
|
||||
stripe::CreateCheckoutSessionTaxIdCollection {
|
||||
enabled: value.enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,6 +547,7 @@ async fn handle_envs(
|
||||
}
|
||||
};
|
||||
|
||||
let mut env_vars = HashMap::default();
|
||||
for path in env_files {
|
||||
let Some(path) = path
|
||||
.and_then(|s| PathBuf::from_str(s).ok())
|
||||
@@ -556,13 +557,33 @@ async fn handle_envs(
|
||||
};
|
||||
|
||||
if let Ok(file) = fs.open_sync(&path).await {
|
||||
envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
|
||||
let file_envs: HashMap<String, String> = dotenvy::from_read_iter(file)
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
envs.extend(file_envs.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||||
env_vars.extend(file_envs);
|
||||
} else {
|
||||
warn!("While starting Go debug session: failed to read env file {path:?}");
|
||||
};
|
||||
}
|
||||
|
||||
let mut env_obj: serde_json::Map<String, Value> = serde_json::Map::new();
|
||||
|
||||
for (k, v) in env_vars {
|
||||
env_obj.insert(k, Value::String(v));
|
||||
}
|
||||
|
||||
if let Some(existing_env) = config.get("env").and_then(|v| v.as_object()) {
|
||||
for (k, v) in existing_env {
|
||||
env_obj.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !env_obj.is_empty() {
|
||||
config.insert("env".to_string(), Value::Object(env_obj));
|
||||
}
|
||||
|
||||
// remove envFile now that it's been handled
|
||||
config.remove("entry");
|
||||
config.remove("envFile");
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -32,12 +32,19 @@ use workspace::{
|
||||
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum View {
|
||||
AdapterLogs,
|
||||
RpcMessages,
|
||||
InitializationSequence,
|
||||
}
|
||||
|
||||
struct DapLogView {
|
||||
editor: Entity<Editor>,
|
||||
focus_handle: FocusHandle,
|
||||
log_store: Entity<LogStore>,
|
||||
editor_subscriptions: Vec<Subscription>,
|
||||
current_view: Option<(SessionId, LogKind)>,
|
||||
current_view: Option<(SessionId, View)>,
|
||||
project: Entity<Project>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -77,6 +84,7 @@ struct DebugAdapterState {
|
||||
id: SessionId,
|
||||
log_messages: VecDeque<SharedString>,
|
||||
rpc_messages: RpcMessages,
|
||||
session_label: SharedString,
|
||||
adapter_name: DebugAdapterName,
|
||||
has_adapter_logs: bool,
|
||||
is_terminated: bool,
|
||||
@@ -121,12 +129,18 @@ impl MessageKind {
|
||||
}
|
||||
|
||||
impl DebugAdapterState {
|
||||
fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self {
|
||||
fn new(
|
||||
id: SessionId,
|
||||
adapter_name: DebugAdapterName,
|
||||
session_label: SharedString,
|
||||
has_adapter_logs: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
log_messages: VecDeque::new(),
|
||||
rpc_messages: RpcMessages::new(),
|
||||
adapter_name,
|
||||
session_label,
|
||||
has_adapter_logs,
|
||||
is_terminated: false,
|
||||
}
|
||||
@@ -371,18 +385,21 @@ impl LogStore {
|
||||
return None;
|
||||
};
|
||||
|
||||
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
|
||||
(
|
||||
session.adapter(),
|
||||
session
|
||||
.adapter_client()
|
||||
.map_or(false, |client| client.has_adapter_logs()),
|
||||
)
|
||||
});
|
||||
let (adapter_name, session_label, has_adapter_logs) =
|
||||
session.read_with(cx, |session, _| {
|
||||
(
|
||||
session.adapter(),
|
||||
session.label(),
|
||||
session
|
||||
.adapter_client()
|
||||
.map_or(false, |client| client.has_adapter_logs()),
|
||||
)
|
||||
});
|
||||
|
||||
state.insert(DebugAdapterState::new(
|
||||
id.session_id,
|
||||
adapter_name,
|
||||
session_label,
|
||||
has_adapter_logs,
|
||||
));
|
||||
|
||||
@@ -506,12 +523,13 @@ impl Render for DapLogToolbarItemView {
|
||||
current_client
|
||||
.map(|sub_item| {
|
||||
Cow::Owned(format!(
|
||||
"{} ({}) - {}",
|
||||
"{} - {} - {}",
|
||||
sub_item.adapter_name,
|
||||
sub_item.session_id.0,
|
||||
sub_item.session_label,
|
||||
match sub_item.selected_entry {
|
||||
LogKind::Adapter => ADAPTER_LOGS,
|
||||
LogKind::Rpc => RPC_MESSAGES,
|
||||
View::AdapterLogs => ADAPTER_LOGS,
|
||||
View::RpcMessages => RPC_MESSAGES,
|
||||
View::InitializationSequence => INITIALIZATION_SEQUENCE,
|
||||
}
|
||||
))
|
||||
})
|
||||
@@ -529,8 +547,8 @@ impl Render for DapLogToolbarItemView {
|
||||
.pl_2()
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{}. {}",
|
||||
row.session_id.0, row.adapter_name,
|
||||
"{} - {}",
|
||||
row.adapter_name, row.session_label
|
||||
))
|
||||
.color(workspace::ui::Color::Muted),
|
||||
)
|
||||
@@ -669,9 +687,16 @@ impl DapLogView {
|
||||
|
||||
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
|
||||
Event::NewLogEntry { id, entry, kind } => {
|
||||
if log_view.current_view == Some((id.session_id, *kind))
|
||||
&& log_view.project == *id.project
|
||||
{
|
||||
let is_current_view = match (log_view.current_view, *kind) {
|
||||
(Some((i, View::AdapterLogs)), LogKind::Adapter)
|
||||
| (Some((i, View::RpcMessages)), LogKind::Rpc)
|
||||
if i == id.session_id =>
|
||||
{
|
||||
log_view.project == *id.project
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if is_current_view {
|
||||
log_view.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(false);
|
||||
let last_point = editor.buffer().read(cx).len(cx);
|
||||
@@ -768,10 +793,11 @@ impl DapLogView {
|
||||
.map(|state| DapMenuItem {
|
||||
session_id: state.id,
|
||||
adapter_name: state.adapter_name.clone(),
|
||||
session_label: state.session_label.clone(),
|
||||
has_adapter_logs: state.has_adapter_logs,
|
||||
selected_entry: self
|
||||
.current_view
|
||||
.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
.map_or(View::AdapterLogs, |(_, kind)| kind),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
@@ -789,7 +815,7 @@ impl DapLogView {
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((id.session_id, LogKind::Rpc));
|
||||
self.current_view = Some((id.session_id, View::RpcMessages));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
@@ -830,7 +856,7 @@ impl DapLogView {
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(message_log) = message_log {
|
||||
self.current_view = Some((id.session_id, LogKind::Adapter));
|
||||
self.current_view = Some((id.session_id, View::AdapterLogs));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
|
||||
editor
|
||||
.read(cx)
|
||||
@@ -859,7 +885,7 @@ impl DapLogView {
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((id.session_id, LogKind::Rpc));
|
||||
self.current_view = Some((id.session_id, View::InitializationSequence));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
@@ -899,11 +925,12 @@ fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub(crate) struct DapMenuItem {
|
||||
pub session_id: SessionId,
|
||||
pub adapter_name: DebugAdapterName,
|
||||
pub has_adapter_logs: bool,
|
||||
pub selected_entry: LogKind,
|
||||
struct DapMenuItem {
|
||||
session_id: SessionId,
|
||||
session_label: SharedString,
|
||||
adapter_name: DebugAdapterName,
|
||||
has_adapter_logs: bool,
|
||||
selected_entry: View,
|
||||
}
|
||||
|
||||
const ADAPTER_LOGS: &str = "Adapter Logs";
|
||||
|
||||
@@ -11,7 +11,7 @@ use project::worktree_store::WorktreeStore;
|
||||
use rpc::proto;
|
||||
use running::RunningState;
|
||||
use std::{cell::OnceCell, sync::OnceLock};
|
||||
use ui::{Indicator, Tooltip, prelude::*};
|
||||
use ui::{Indicator, prelude::*};
|
||||
use util::truncate_and_trailoff;
|
||||
use workspace::{
|
||||
CollaboratorId, FollowableItem, ViewId, Workspace,
|
||||
@@ -158,7 +158,6 @@ impl DebugSession {
|
||||
|
||||
h_flex()
|
||||
.id("session-label")
|
||||
.tooltip(Tooltip::text(format!("Session {}", self.session_id(cx).0,)))
|
||||
.ml(depth * px(16.0))
|
||||
.gap_2()
|
||||
.when_some(icon, |this, indicator| this.child(indicator))
|
||||
|
||||
@@ -2241,3 +2241,34 @@ func main() {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_trim_multi_line_inline_value(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
let variables = [("y", "hello\n world")];
|
||||
|
||||
let before = r#"
|
||||
fn main() {
|
||||
let y = "hello\n world";
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let after = r#"
|
||||
fn main() {
|
||||
let y: hello… = "hello\n world";
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
test_inline_values_util(
|
||||
&variables,
|
||||
&[],
|
||||
&before,
|
||||
&after,
|
||||
None,
|
||||
rust_lang(),
|
||||
executor,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use editor::{
|
||||
hover_popover::diagnostics_markdown_style,
|
||||
};
|
||||
use gpui::{AppContext, Entity, Focusable, WeakEntity};
|
||||
use language::{BufferId, Diagnostic, DiagnosticEntry};
|
||||
use language::{BufferId, Diagnostic, DiagnosticEntry, LanguageRegistry};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use markdown::{Markdown, MarkdownElement};
|
||||
use settings::Settings;
|
||||
@@ -27,6 +27,7 @@ impl DiagnosticRenderer {
|
||||
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
||||
buffer_id: BufferId,
|
||||
diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Vec<DiagnosticBlock> {
|
||||
let Some(primary_ix) = diagnostic_group
|
||||
@@ -79,7 +80,9 @@ impl DiagnosticRenderer {
|
||||
initial_range: primary.range.clone(),
|
||||
severity: primary.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
markdown: cx.new(|cx| {
|
||||
Markdown::new(markdown.into(), Some(languages.clone()), None, cx)
|
||||
}),
|
||||
});
|
||||
} else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
|
||||
let markdown = Self::markdown(&entry.diagnostic);
|
||||
@@ -88,7 +91,9 @@ impl DiagnosticRenderer {
|
||||
initial_range: entry.range.clone(),
|
||||
severity: entry.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
markdown: cx.new(|cx| {
|
||||
Markdown::new(markdown.into(), Some(languages.clone()), None, cx)
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
let mut markdown = Self::markdown(&entry.diagnostic);
|
||||
@@ -100,7 +105,9 @@ impl DiagnosticRenderer {
|
||||
initial_range: entry.range.clone(),
|
||||
severity: entry.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
markdown: cx.new(|cx| {
|
||||
Markdown::new(markdown.into(), Some(languages.clone()), None, cx)
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -127,9 +134,11 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||
buffer_id: BufferId,
|
||||
snapshot: EditorSnapshot,
|
||||
editor: WeakEntity<Editor>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Vec<BlockProperties<Anchor>> {
|
||||
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
|
||||
let blocks =
|
||||
Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, languages, cx);
|
||||
blocks
|
||||
.into_iter()
|
||||
.map(|block| {
|
||||
@@ -155,9 +164,11 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
||||
range: Range<Point>,
|
||||
buffer_id: BufferId,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<Markdown>> {
|
||||
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
|
||||
let blocks =
|
||||
Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, languages, cx);
|
||||
blocks.into_iter().find_map(|block| {
|
||||
if block.initial_range == range {
|
||||
Some(block.markdown)
|
||||
|
||||
@@ -508,6 +508,15 @@ impl ProjectDiagnosticsEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let languages = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.project
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.languages()
|
||||
.clone();
|
||||
let was_empty = self.multibuffer.read(cx).is_empty();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
@@ -559,6 +568,7 @@ impl ProjectDiagnosticsEditor {
|
||||
group,
|
||||
buffer_snapshot.remote_id(),
|
||||
Some(this.clone()),
|
||||
languages.clone(),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -111,8 +111,9 @@ use itertools::Itertools;
|
||||
use language::{
|
||||
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
|
||||
CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode,
|
||||
EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
|
||||
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
|
||||
EditPreview, HighlightedText, IndentKind, IndentSize, Language, LanguageRegistry,
|
||||
OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
|
||||
WordsQuery,
|
||||
language_settings::{
|
||||
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
|
||||
all_language_settings, language_settings,
|
||||
@@ -402,6 +403,7 @@ pub trait DiagnosticRenderer {
|
||||
buffer_id: BufferId,
|
||||
snapshot: EditorSnapshot,
|
||||
editor: WeakEntity<Editor>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Vec<BlockProperties<Anchor>>;
|
||||
|
||||
@@ -410,6 +412,7 @@ pub trait DiagnosticRenderer {
|
||||
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
||||
range: Range<Point>,
|
||||
buffer_id: BufferId,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<markdown::Markdown>>;
|
||||
|
||||
@@ -2322,7 +2325,10 @@ impl Editor {
|
||||
editor.update_lsp_data(false, None, window, cx);
|
||||
}
|
||||
|
||||
editor.report_editor_event("Editor Opened", None, cx);
|
||||
if editor.mode.is_full() {
|
||||
editor.report_editor_event("Editor Opened", None, cx);
|
||||
}
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
@@ -16571,13 +16577,20 @@ impl Editor {
|
||||
let Some(renderer) = GlobalDiagnosticRenderer::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let languages = self.project.as_ref().unwrap().read(cx).languages().clone();
|
||||
|
||||
let diagnostic_group = buffer
|
||||
.diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let blocks =
|
||||
renderer.render_group(diagnostic_group, buffer_id, snapshot, cx.weak_entity(), cx);
|
||||
let blocks = renderer.render_group(
|
||||
diagnostic_group,
|
||||
buffer_id,
|
||||
snapshot,
|
||||
cx.weak_entity(),
|
||||
languages,
|
||||
cx,
|
||||
);
|
||||
|
||||
let blocks = self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.insert_blocks(blocks, cx).into_iter().collect()
|
||||
@@ -19655,8 +19668,9 @@ impl Editor {
|
||||
Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
|
||||
hint.text(),
|
||||
);
|
||||
|
||||
new_inlays.push(inlay);
|
||||
if !inlay.text.chars().contains(&'\n') {
|
||||
new_inlays.push(inlay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8035,23 +8035,25 @@ impl Element for EditorElement {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Autoscrolling for both axes
|
||||
let mut autoscroll_request = None;
|
||||
let mut autoscroll_containing_element = false;
|
||||
let mut autoscroll_horizontally = false;
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
autoscroll_request = editor.autoscroll_request();
|
||||
autoscroll_containing_element =
|
||||
let (
|
||||
autoscroll_request,
|
||||
autoscroll_containing_element,
|
||||
needs_horizontal_autoscroll,
|
||||
) = self.editor.update(cx, |editor, cx| {
|
||||
let autoscroll_request = editor.autoscroll_request();
|
||||
let autoscroll_containing_element =
|
||||
autoscroll_request.is_some() || editor.has_pending_selection();
|
||||
// TODO: Is this horizontal or vertical?!
|
||||
autoscroll_horizontally = editor.autoscroll_vertically(
|
||||
bounds,
|
||||
line_height,
|
||||
max_scroll_top,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
|
||||
let (needs_horizontal_autoscroll, was_scrolled) = editor
|
||||
.autoscroll_vertically(bounds, line_height, max_scroll_top, window, cx);
|
||||
if was_scrolled.0 {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
}
|
||||
(
|
||||
autoscroll_request,
|
||||
autoscroll_containing_element,
|
||||
needs_horizontal_autoscroll,
|
||||
)
|
||||
});
|
||||
|
||||
let mut scroll_position = snapshot.scroll_position();
|
||||
@@ -8460,10 +8462,12 @@ impl Element for EditorElement {
|
||||
);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
|
||||
if editor.scroll_manager.clamp_scroll_left(scroll_max.x) {
|
||||
scroll_position.x = scroll_position.x.min(scroll_max.x);
|
||||
}
|
||||
|
||||
let autoscrolled = if autoscroll_horizontally {
|
||||
editor.autoscroll_horizontally(
|
||||
if needs_horizontal_autoscroll.0
|
||||
&& let Some(new_scroll_position) = editor.autoscroll_horizontally(
|
||||
start_row,
|
||||
editor_content_width,
|
||||
scroll_width,
|
||||
@@ -8472,13 +8476,8 @@ impl Element for EditorElement {
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if clamped || autoscrolled {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
scroll_position = snapshot.scroll_position();
|
||||
{
|
||||
scroll_position = new_scroll_position;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8593,7 +8592,9 @@ impl Element for EditorElement {
|
||||
}
|
||||
} else {
|
||||
log::error!(
|
||||
"bug: line_ix {} is out of bounds - row_infos.len(): {}, line_layouts.len(): {}, crease_trailers.len(): {}",
|
||||
"bug: line_ix {} is out of bounds - row_infos.len(): {}, \
|
||||
line_layouts.len(): {}, \
|
||||
crease_trailers.len(): {}",
|
||||
line_ix,
|
||||
row_infos.len(),
|
||||
line_layouts.len(),
|
||||
@@ -8839,7 +8840,7 @@ impl Element for EditorElement {
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
None
|
||||
None,
|
||||
);
|
||||
let space_invisible = window.text_system().shape_line(
|
||||
"•".into(),
|
||||
@@ -8852,7 +8853,7 @@ impl Element for EditorElement {
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
None
|
||||
None,
|
||||
);
|
||||
|
||||
let mode = snapshot.mode.clone();
|
||||
|
||||
@@ -275,6 +275,13 @@ fn show_hover(
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let languages = editor
|
||||
.project
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.languages()
|
||||
.clone();
|
||||
|
||||
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
|
||||
let all_diagnostics_active = editor.active_diagnostics == ActiveDiagnostic::All;
|
||||
@@ -340,7 +347,7 @@ fn show_hover(
|
||||
renderer
|
||||
.as_ref()
|
||||
.and_then(|renderer| {
|
||||
renderer.render_hover(group, point_range, buffer_id, cx)
|
||||
renderer.render_hover(group, point_range, buffer_id, languages, cx)
|
||||
})
|
||||
.context("no rendered diagnostic")
|
||||
})??;
|
||||
|
||||
@@ -813,7 +813,13 @@ impl Item for Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.report_editor_event("Editor Saved", None, cx);
|
||||
// Add meta data tracking # of auto saves
|
||||
if options.autosave {
|
||||
self.report_editor_event("Editor Autosaved", None, cx);
|
||||
} else {
|
||||
self.report_editor_event("Editor Saved", None, cx);
|
||||
}
|
||||
|
||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||
let buffers = buffers
|
||||
.into_iter()
|
||||
|
||||
@@ -27,6 +27,8 @@ use workspace::{ItemId, WorkspaceId};
|
||||
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
pub struct WasScrolled(pub(crate) bool);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ScrollbarAutoHide(pub bool);
|
||||
|
||||
@@ -215,87 +217,56 @@ impl ScrollManager {
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let (new_anchor, top_row) = if scroll_position.y <= 0. && scroll_position.x <= 0. {
|
||||
(
|
||||
ScrollAnchor {
|
||||
anchor: Anchor::min(),
|
||||
offset: scroll_position.max(&gpui::Point::default()),
|
||||
},
|
||||
0,
|
||||
)
|
||||
} else if scroll_position.y <= 0. {
|
||||
let buffer_point = map
|
||||
.clip_point(
|
||||
DisplayPoint::new(DisplayRow(0), scroll_position.x as u32),
|
||||
Bias::Left,
|
||||
)
|
||||
.to_point(map);
|
||||
let anchor = map.buffer_snapshot.anchor_at(buffer_point, Bias::Right);
|
||||
|
||||
(
|
||||
ScrollAnchor {
|
||||
anchor: anchor,
|
||||
offset: scroll_position.max(&gpui::Point::default()),
|
||||
},
|
||||
0,
|
||||
)
|
||||
} else {
|
||||
let scroll_top = scroll_position.y;
|
||||
let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
|
||||
ScrollBeyondLastLine::OnePage => scroll_top,
|
||||
ScrollBeyondLastLine::Off => {
|
||||
if let Some(height_in_lines) = self.visible_line_count {
|
||||
let max_row = map.max_point().row().0 as f32;
|
||||
scroll_top.min(max_row - height_in_lines + 1.).max(0.)
|
||||
} else {
|
||||
scroll_top
|
||||
}
|
||||
) -> WasScrolled {
|
||||
let scroll_top = scroll_position.y.max(0.);
|
||||
let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
|
||||
ScrollBeyondLastLine::OnePage => scroll_top,
|
||||
ScrollBeyondLastLine::Off => {
|
||||
if let Some(height_in_lines) = self.visible_line_count {
|
||||
let max_row = map.max_point().row().0 as f32;
|
||||
scroll_top.min(max_row - height_in_lines + 1.).max(0.)
|
||||
} else {
|
||||
scroll_top
|
||||
}
|
||||
ScrollBeyondLastLine::VerticalScrollMargin => {
|
||||
if let Some(height_in_lines) = self.visible_line_count {
|
||||
let max_row = map.max_point().row().0 as f32;
|
||||
scroll_top
|
||||
.min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
|
||||
.max(0.)
|
||||
} else {
|
||||
scroll_top
|
||||
}
|
||||
}
|
||||
ScrollBeyondLastLine::VerticalScrollMargin => {
|
||||
if let Some(height_in_lines) = self.visible_line_count {
|
||||
let max_row = map.max_point().row().0 as f32;
|
||||
scroll_top
|
||||
.min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
|
||||
.max(0.)
|
||||
} else {
|
||||
scroll_top
|
||||
}
|
||||
};
|
||||
|
||||
let scroll_top_row = DisplayRow(scroll_top as u32);
|
||||
let scroll_top_buffer_point = map
|
||||
.clip_point(
|
||||
DisplayPoint::new(scroll_top_row, scroll_position.x as u32),
|
||||
Bias::Left,
|
||||
)
|
||||
.to_point(map);
|
||||
let top_anchor = map
|
||||
.buffer_snapshot
|
||||
.anchor_at(scroll_top_buffer_point, Bias::Right);
|
||||
|
||||
(
|
||||
ScrollAnchor {
|
||||
anchor: top_anchor,
|
||||
offset: point(
|
||||
scroll_position.x.max(0.),
|
||||
scroll_top - top_anchor.to_display_point(map).row().as_f32(),
|
||||
),
|
||||
},
|
||||
scroll_top_buffer_point.row,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let scroll_top_row = DisplayRow(scroll_top as u32);
|
||||
let scroll_top_buffer_point = map
|
||||
.clip_point(
|
||||
DisplayPoint::new(scroll_top_row, scroll_position.x as u32),
|
||||
Bias::Left,
|
||||
)
|
||||
.to_point(map);
|
||||
let top_anchor = map
|
||||
.buffer_snapshot
|
||||
.anchor_at(scroll_top_buffer_point, Bias::Right);
|
||||
|
||||
self.set_anchor(
|
||||
new_anchor,
|
||||
top_row,
|
||||
ScrollAnchor {
|
||||
anchor: top_anchor,
|
||||
offset: point(
|
||||
scroll_position.x.max(0.),
|
||||
scroll_top - top_anchor.to_display_point(map).row().as_f32(),
|
||||
),
|
||||
},
|
||||
scroll_top_buffer_point.row,
|
||||
local,
|
||||
autoscroll,
|
||||
workspace_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
fn set_anchor(
|
||||
@@ -307,7 +278,7 @@ impl ScrollManager {
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
) -> WasScrolled {
|
||||
let adjusted_anchor = if self.forbid_vertical_scroll {
|
||||
ScrollAnchor {
|
||||
offset: gpui::Point::new(anchor.offset.x, self.anchor.offset.y),
|
||||
@@ -317,10 +288,14 @@ impl ScrollManager {
|
||||
anchor
|
||||
};
|
||||
|
||||
self.autoscroll_request.take();
|
||||
if self.anchor == adjusted_anchor {
|
||||
return WasScrolled(false);
|
||||
}
|
||||
|
||||
self.anchor = adjusted_anchor;
|
||||
cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
|
||||
self.show_scrollbars(window, cx);
|
||||
self.autoscroll_request.take();
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
let item_id = cx.entity().entity_id().as_u64() as ItemId;
|
||||
|
||||
@@ -342,6 +317,8 @@ impl ScrollManager {
|
||||
.detach()
|
||||
}
|
||||
cx.notify();
|
||||
|
||||
WasScrolled(true)
|
||||
}
|
||||
|
||||
pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
@@ -552,13 +529,13 @@ impl Editor {
|
||||
scroll_position: gpui::Point<f32>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> WasScrolled {
|
||||
let mut position = scroll_position;
|
||||
if self.scroll_manager.forbid_vertical_scroll {
|
||||
let current_position = self.scroll_position(cx);
|
||||
position.y = current_position.y;
|
||||
}
|
||||
self.set_scroll_position_internal(position, true, false, window, cx);
|
||||
self.set_scroll_position_internal(position, true, false, window, cx)
|
||||
}
|
||||
|
||||
/// Scrolls so that `row` is at the top of the editor view.
|
||||
@@ -590,7 +567,7 @@ impl Editor {
|
||||
autoscroll: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> WasScrolled {
|
||||
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
self.set_scroll_position_taking_display_map(
|
||||
scroll_position,
|
||||
@@ -599,7 +576,7 @@ impl Editor {
|
||||
map,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
fn set_scroll_position_taking_display_map(
|
||||
@@ -610,7 +587,7 @@ impl Editor {
|
||||
display_map: DisplaySnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> WasScrolled {
|
||||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
|
||||
|
||||
@@ -624,7 +601,7 @@ impl Editor {
|
||||
scroll_position
|
||||
};
|
||||
|
||||
self.scroll_manager.set_scroll_position(
|
||||
let editor_was_scrolled = self.scroll_manager.set_scroll_position(
|
||||
adjusted_position,
|
||||
&display_map,
|
||||
local,
|
||||
@@ -636,6 +613,7 @@ impl Editor {
|
||||
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
self.refresh_colors(false, None, window, cx);
|
||||
editor_was_scrolled
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<f32> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects,
|
||||
display_map::ToDisplayPoint,
|
||||
display_map::ToDisplayPoint, scroll::WasScrolled,
|
||||
};
|
||||
use gpui::{Bounds, Context, Pixels, Window, px};
|
||||
use language::Point;
|
||||
@@ -99,19 +99,21 @@ impl AutoscrollStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool);
|
||||
|
||||
impl Editor {
|
||||
pub fn autoscroll_request(&self) -> Option<Autoscroll> {
|
||||
self.scroll_manager.autoscroll_request()
|
||||
}
|
||||
|
||||
pub fn autoscroll_vertically(
|
||||
pub(crate) fn autoscroll_vertically(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
line_height: Pixels,
|
||||
max_scroll_top: f32,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
) -> (NeedsHorizontalAutoscroll, WasScrolled) {
|
||||
let viewport_height = bounds.size.height;
|
||||
let visible_lines = viewport_height / line_height;
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
@@ -129,12 +131,14 @@ impl Editor {
|
||||
scroll_position.y = max_scroll_top;
|
||||
}
|
||||
|
||||
if original_y != scroll_position.y {
|
||||
self.set_scroll_position(scroll_position, window, cx);
|
||||
}
|
||||
let editor_was_scrolled = if original_y != scroll_position.y {
|
||||
self.set_scroll_position(scroll_position, window, cx)
|
||||
} else {
|
||||
WasScrolled(false)
|
||||
};
|
||||
|
||||
let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
|
||||
return false;
|
||||
return (NeedsHorizontalAutoscroll(false), editor_was_scrolled);
|
||||
};
|
||||
|
||||
let mut target_top;
|
||||
@@ -212,7 +216,7 @@ impl Editor {
|
||||
target_bottom = target_top + 1.;
|
||||
}
|
||||
|
||||
match strategy {
|
||||
let was_autoscrolled = match strategy {
|
||||
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
|
||||
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
|
||||
let target_top = (target_top - margin).max(0.0);
|
||||
@@ -225,39 +229,42 @@ impl Editor {
|
||||
|
||||
if needs_scroll_up && !needs_scroll_down {
|
||||
scroll_position.y = target_top;
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
}
|
||||
if !needs_scroll_up && needs_scroll_down {
|
||||
} else if !needs_scroll_up && needs_scroll_down {
|
||||
scroll_position.y = target_bottom - visible_lines;
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
}
|
||||
|
||||
if needs_scroll_up ^ needs_scroll_down {
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
} else {
|
||||
WasScrolled(false)
|
||||
}
|
||||
}
|
||||
AutoscrollStrategy::Center => {
|
||||
scroll_position.y = (target_top - margin).max(0.0);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
AutoscrollStrategy::Focused => {
|
||||
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
|
||||
scroll_position.y = (target_top - margin).max(0.0);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
AutoscrollStrategy::Top => {
|
||||
scroll_position.y = (target_top).max(0.0);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
AutoscrollStrategy::Bottom => {
|
||||
scroll_position.y = (target_bottom - visible_lines).max(0.0);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
AutoscrollStrategy::TopRelative(lines) => {
|
||||
scroll_position.y = target_top - lines as f32;
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
AutoscrollStrategy::BottomRelative(lines) => {
|
||||
scroll_position.y = target_bottom + lines as f32;
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.scroll_manager.last_autoscroll = Some((
|
||||
self.scroll_manager.anchor.offset,
|
||||
@@ -266,7 +273,8 @@ impl Editor {
|
||||
strategy,
|
||||
));
|
||||
|
||||
true
|
||||
let was_scrolled = WasScrolled(editor_was_scrolled.0 || was_autoscrolled.0);
|
||||
(NeedsHorizontalAutoscroll(true), was_scrolled)
|
||||
}
|
||||
|
||||
pub(crate) fn autoscroll_horizontally(
|
||||
@@ -278,7 +286,7 @@ impl Editor {
|
||||
layouts: &[LineWithInvisibles],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
) -> Option<gpui::Point<f32>> {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
|
||||
@@ -319,22 +327,26 @@ impl Editor {
|
||||
target_right = target_right.min(scroll_width);
|
||||
|
||||
if target_right - target_left > viewport_width {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
|
||||
let scroll_left = self.scroll_manager.anchor.offset.x * em_advance;
|
||||
let scroll_right = scroll_left + viewport_width;
|
||||
|
||||
if target_left < scroll_left {
|
||||
let was_scrolled = if target_left < scroll_left {
|
||||
scroll_position.x = target_left / em_advance;
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
|
||||
true
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx)
|
||||
} else if target_right > scroll_right {
|
||||
scroll_position.x = (target_right - viewport_width) / em_advance;
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
|
||||
true
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx)
|
||||
} else {
|
||||
false
|
||||
WasScrolled(false)
|
||||
};
|
||||
|
||||
if was_scrolled.0 {
|
||||
Some(scroll_position)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,7 @@ use gpui::{
|
||||
use language::{Anchor, Buffer, BufferId};
|
||||
use project::{ConflictRegion, ConflictSet, ConflictSetUpdate, ProjectItem as _};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use ui::{
|
||||
ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
|
||||
StyledTypography as _, Window, div, h_flex, rems,
|
||||
};
|
||||
use ui::{ActiveTheme, Element as _, Styled, Window, prelude::*};
|
||||
use util::{ResultExt as _, debug_panic, maybe};
|
||||
|
||||
pub(crate) struct ConflictAddon {
|
||||
@@ -391,20 +388,15 @@ fn render_conflict_buttons(
|
||||
cx: &mut BlockContext,
|
||||
) -> AnyElement {
|
||||
h_flex()
|
||||
.h(cx.line_height)
|
||||
.items_end()
|
||||
.ml(cx.margins.gutter.width)
|
||||
.id(cx.block_id)
|
||||
.gap_0p5()
|
||||
.h(cx.line_height)
|
||||
.ml(cx.margins.gutter.width)
|
||||
.items_end()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
div()
|
||||
.id("ours")
|
||||
.px_1()
|
||||
.child("Take Ours")
|
||||
.rounded_t(rems(0.2))
|
||||
.text_ui_sm(cx)
|
||||
.hover(|this| this.bg(cx.theme().colors().element_background))
|
||||
.cursor_pointer()
|
||||
Button::new("head", "Use HEAD")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let conflict = conflict.clone();
|
||||
@@ -423,14 +415,8 @@ fn render_conflict_buttons(
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("theirs")
|
||||
.px_1()
|
||||
.child("Take Theirs")
|
||||
.rounded_t(rems(0.2))
|
||||
.text_ui_sm(cx)
|
||||
.hover(|this| this.bg(cx.theme().colors().element_background))
|
||||
.cursor_pointer()
|
||||
Button::new("origin", "Use Origin")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let conflict = conflict.clone();
|
||||
@@ -449,14 +435,8 @@ fn render_conflict_buttons(
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("both")
|
||||
.px_1()
|
||||
.child("Take Both")
|
||||
.rounded_t(rems(0.2))
|
||||
.text_ui_sm(cx)
|
||||
.hover(|this| this.bg(cx.theme().colors().element_background))
|
||||
.cursor_pointer()
|
||||
Button::new("both", "Use Both")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let conflict = conflict.clone();
|
||||
|
||||
@@ -949,12 +949,10 @@ impl App {
|
||||
.write()
|
||||
.retain(|handle_id, count| {
|
||||
if count.load(SeqCst) == 0 {
|
||||
println!("Dropping {handle_id}");
|
||||
for window_handle in self.windows() {
|
||||
window_handle
|
||||
.update(self, |_, window, _| {
|
||||
if window.focus == Some(handle_id) {
|
||||
println!("released focus handle blur");
|
||||
window.blur();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -570,7 +570,6 @@ impl DispatchTree {
|
||||
}
|
||||
|
||||
pub fn focus_path(&self, focus_id: FocusId) -> SmallVec<[FocusId; 8]> {
|
||||
println!("focus path requested for focus id: {:?}", focus_id);
|
||||
let mut focus_path: SmallVec<[FocusId; 8]> = SmallVec::new();
|
||||
let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied();
|
||||
while let Some(node_id) = current_node_id {
|
||||
|
||||
@@ -227,7 +227,7 @@ pub(crate) type FocusMap = RwLock<SlotMap<FocusId, AtomicUsize>>;
|
||||
impl FocusId {
|
||||
/// Obtains whether the element associated with this handle is currently focused.
|
||||
pub fn is_focused(&self, window: &Window) -> bool {
|
||||
dbg!(window.focus) == Some(*self)
|
||||
window.focus == Some(*self)
|
||||
}
|
||||
|
||||
/// Obtains whether the element associated with this handle contains the focused
|
||||
@@ -705,7 +705,6 @@ impl Frame {
|
||||
self.window_control_hitboxes.clear();
|
||||
self.deferred_draws.clear();
|
||||
self.focus = None;
|
||||
println!("clearing focus 1");
|
||||
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
{
|
||||
@@ -752,8 +751,6 @@ impl Frame {
|
||||
}
|
||||
|
||||
pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> {
|
||||
dbg!("focus path");
|
||||
dbg!(self.focus.is_some());
|
||||
self.focus
|
||||
.map(|focus_id| self.dispatch_tree.focus_path(focus_id))
|
||||
.unwrap_or_default()
|
||||
@@ -1267,16 +1264,10 @@ impl Window {
|
||||
|
||||
/// Move focus to the element associated with the given [`FocusHandle`].
|
||||
pub fn focus(&mut self, handle: &FocusHandle) {
|
||||
println!(
|
||||
"Setting focus to {:?} on platform {:?}",
|
||||
handle.id,
|
||||
std::env::consts::OS
|
||||
);
|
||||
|
||||
if !self.focus_enabled || self.focus == Some(handle.id) {
|
||||
return;
|
||||
}
|
||||
println!("actually setting focus");
|
||||
|
||||
self.focus = Some(handle.id);
|
||||
self.clear_pending_keystrokes();
|
||||
self.refresh();
|
||||
@@ -1289,13 +1280,11 @@ impl Window {
|
||||
}
|
||||
|
||||
self.focus = None;
|
||||
println!("clearing focus 2");
|
||||
self.refresh();
|
||||
}
|
||||
|
||||
/// Blur the window and don't allow anything in it to be focused again.
|
||||
pub fn disable_focus(&mut self) {
|
||||
println!("disable_focus");
|
||||
self.blur();
|
||||
self.focus_enabled = false;
|
||||
}
|
||||
@@ -1344,13 +1333,12 @@ impl Window {
|
||||
/// Dispatch the given action on the currently focused element.
|
||||
pub fn dispatch_action(&mut self, action: Box<dyn Action>, cx: &mut App) {
|
||||
let focus_id = self.focused(cx).map(|handle| handle.id);
|
||||
dbg!(&focus_id);
|
||||
|
||||
let window = self.handle;
|
||||
cx.defer(move |cx| {
|
||||
window
|
||||
.update(cx, |_, window, cx| {
|
||||
let node_id = window.focus_node_id_in_rendered_frame(focus_id);
|
||||
dbg!(&node_id);
|
||||
window.dispatch_action_on_node(node_id, action.as_ref(), cx);
|
||||
})
|
||||
.log_err();
|
||||
@@ -1809,16 +1797,8 @@ impl Window {
|
||||
self.invalidator.set_phase(DrawPhase::Focus);
|
||||
let previous_focus_path = self.rendered_frame.focus_path();
|
||||
let previous_window_active = self.rendered_frame.window_active;
|
||||
println!(
|
||||
"dbg! Window::draw - pre-swap: rendered_frame.focus = {:?}, next_frame.focus = {:?}",
|
||||
self.rendered_frame.focus, self.next_frame.focus
|
||||
);
|
||||
mem::swap(&mut self.rendered_frame, &mut self.next_frame);
|
||||
self.next_frame.clear();
|
||||
println!(
|
||||
"dbg! Window::draw - post-swap: rendered_frame.focus = {:?}, next_frame.focus = {:?}",
|
||||
self.rendered_frame.focus, self.next_frame.focus
|
||||
);
|
||||
let current_focus_path = self.rendered_frame.focus_path();
|
||||
let current_window_active = self.rendered_frame.window_active;
|
||||
|
||||
@@ -2135,7 +2115,6 @@ impl Window {
|
||||
);
|
||||
|
||||
if reused_subtree.contains_focus() {
|
||||
println!("setting focus for next frame");
|
||||
self.next_frame.focus = self.focus;
|
||||
}
|
||||
|
||||
@@ -3134,9 +3113,7 @@ impl Window {
|
||||
/// This method should only be called as part of the prepaint phase of element drawing.
|
||||
pub fn set_focus_handle(&mut self, focus_handle: &FocusHandle, _: &App) {
|
||||
self.invalidator.debug_assert_prepaint();
|
||||
println!("set_focus_handle called");
|
||||
if focus_handle.is_focused(self) {
|
||||
println!("setting focus for next frame");
|
||||
self.next_frame.focus = Some(focus_handle.id);
|
||||
}
|
||||
self.next_frame.dispatch_tree.set_focus_id(focus_handle.id);
|
||||
@@ -3780,10 +3757,7 @@ impl Window {
|
||||
.dispatch_tree
|
||||
.focusable_node_id(focus_id)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
println!("root node id");
|
||||
self.rendered_frame.dispatch_tree.root_node_id()
|
||||
})
|
||||
.unwrap_or_else(|| self.rendered_frame.dispatch_tree.root_node_id())
|
||||
}
|
||||
|
||||
fn dispatch_action_on_node(
|
||||
|
||||
@@ -44,6 +44,7 @@ dap.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
|
||||
@@ -262,6 +262,7 @@ impl LspAdapter for RustLspAdapter {
|
||||
_: LanguageServerId,
|
||||
_: Option<&'_ Buffer>,
|
||||
) {
|
||||
// https://zed.dev/cla
|
||||
static REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ use futures::future::join_all;
|
||||
use gpui::{App, AppContext, AsyncApp, Task};
|
||||
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
|
||||
use language::{
|
||||
ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
|
||||
Buffer, ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter,
|
||||
LspAdapterDelegate,
|
||||
};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerId, LanguageServerName};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::{Fs, lsp_store::language_server_settings};
|
||||
use serde_json::{Value, json};
|
||||
@@ -605,6 +606,7 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// >>> https://zed.dev/cla <<<
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Box<dyn 'static + Send + Any>,
|
||||
@@ -748,6 +750,15 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
("TSX".into(), "typescriptreact".into()),
|
||||
])
|
||||
}
|
||||
|
||||
fn process_diagnostics(
|
||||
&self,
|
||||
d: &mut lsp::PublishDiagnosticsParams,
|
||||
_: LanguageServerId,
|
||||
_: Option<&'_ Buffer>,
|
||||
) {
|
||||
dbg!("called with ", d);
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_ts_server_binary(
|
||||
|
||||
@@ -280,6 +280,185 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
("TSX".into(), "typescriptreact".into()),
|
||||
])
|
||||
}
|
||||
|
||||
fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
|
||||
use regex::{Captures, Regex};
|
||||
dbg!(&message);
|
||||
|
||||
// Helper functions for formatting
|
||||
let format_type_block = |prefix: &str, content: &str| -> String {
|
||||
if prefix.is_empty() {
|
||||
if content.len() > 50 || content.contains('\n') || content.contains('`') {
|
||||
format!("\n```typescript\ntype a ={}\n```\n", dbg!(content))
|
||||
} else {
|
||||
format!("`{}`", dbg!(content))
|
||||
}
|
||||
} else {
|
||||
if content.len() > 50 || content.contains('\n') || content.contains('`') {
|
||||
format!(
|
||||
"{}\n```typescript\ntype a ={}\n```\n",
|
||||
prefix,
|
||||
dbg!(content)
|
||||
)
|
||||
} else {
|
||||
format!("{} `{}`", prefix, dbg!(content))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let format_typescript_block =
|
||||
|content: &str| -> String { format!("\n\n```typescript\n{}\n```\n", dbg!(content)) };
|
||||
|
||||
let format_simple_type_block = |content: &str| -> String { format!("`{}`", dbg!(content)) };
|
||||
|
||||
let unstyle_code_block = |content: &str| -> String { format!("`{}`", dbg!(content)) };
|
||||
|
||||
let mut result = message.to_string();
|
||||
|
||||
// Format 'key' with "value"
|
||||
let re = Regex::new(r#"(\w+)(\s+)'(.+?)'(\s+)with(\s+)"(.+?)""#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format!(
|
||||
"{}{}`{}`{} with `\"{}\"`",
|
||||
&caps[1], &caps[2], &caps[3], &caps[4], &caps[6]
|
||||
)
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format "key"
|
||||
let re = Regex::new(r#"(\s)'"(.*?)"'(\s|:|.|$)"#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format!("{}`\"{}\"`{}", &caps[1], &caps[2], &caps[3])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format declare module snippet
|
||||
let re = Regex::new(r#"['"](declare module )['"](.*)['""];['"']"#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format_typescript_block(&format!("{} \"{}\"", &caps[1], &caps[2]))
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format missing props error
|
||||
let re = Regex::new(r#"(is missing the following properties from type\s?)'(.*)': ([^:]+)"#)
|
||||
.unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
let props: Vec<&str> = caps[3].split(", ").filter(|s| !s.is_empty()).collect();
|
||||
let props_html = props
|
||||
.iter()
|
||||
.map(|prop| format!("<li>{}</li>", prop))
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
format!("{}`{}`: <ul>{}</ul>", &caps[1], &caps[2], props_html)
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format type pairs
|
||||
let re = Regex::new(r#"(?i)(types) ['"](.*?)['"] and ['"](.*?)['"][.]?"#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format!("{} `{}` and `{}`", &caps[1], &caps[2], &caps[3])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format type annotation options
|
||||
let re = Regex::new(r#"(?i)type annotation must be ['"](.*?)['"] or ['"](.*?)['"][.]?"#)
|
||||
.unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format!("type annotation must be `{}` or `{}`", &caps[1], &caps[2])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format overload
|
||||
let re = Regex::new(r#"(?i)(Overload \d of \d), ['"](.*?)['"], "#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format!("{}, `{}`, ", &caps[1], &caps[2])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format simple strings
|
||||
let re = Regex::new(r#"^['"]"[^"]*"['"]$"#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| format_typescript_block(&caps[0]))
|
||||
.to_string();
|
||||
|
||||
// Replace module 'x' by module "x" for ts error #2307
|
||||
let re = Regex::new(r#"(?i)(module )'([^"]*?)'"#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format!("{}\"{}\"", &caps[1], &caps[2])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format string types
|
||||
let re = Regex::new(r#"(?i)(module|file|file name|imported via) ['""](.*?)['""]"#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format_type_block(&caps[1], &format!("\"{}\"", &caps[2]))
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format types
|
||||
dbg!(&result);
|
||||
let re = Regex::new(r#"(?i)(type|type alias|interface|module|file|file name|class|method's|subtype of constraint) ['"](.*?)['"]"#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
dbg!(&caps);
|
||||
format_type_block(&caps[1], &caps[2])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format reversed types
|
||||
let re = Regex::new(r#"(?i)(.*)['"]([^>]*)['"] (type|interface|return type|file|module|is (not )?assignable)"#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format!("{}`{}` {}", &caps[1], &caps[2], &caps[3])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format simple types that didn't captured before
|
||||
let re = Regex::new(
|
||||
r#"['"]((void|null|undefined|any|boolean|string|number|bigint|symbol)(\[\])?)['"']"#,
|
||||
)
|
||||
.unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format_simple_type_block(&caps[1])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format some typescript keywords
|
||||
let re = Regex::new(r#"['"](import|export|require|in|continue|break|let|false|true|const|new|throw|await|for await|[0-9]+)( ?.*?)['"]"#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format_typescript_block(&format!("{}{}", &caps[1], &caps[2]))
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format return values
|
||||
let re = Regex::new(r#"(?i)(return|operator) ['"](.*?)['"']"#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format!("{} {}", &caps[1], format_typescript_block(&caps[2]))
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Format regular code blocks
|
||||
let re = Regex::new(r#"(\W|^)'([^'"]*?)'(\W|$)"#).unwrap();
|
||||
result = re
|
||||
.replace_all(&result, |caps: &Captures| {
|
||||
format!("{}{}{}", &caps[1], unstyle_code_block(&caps[2]), &caps[3])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_ts_server_binary(
|
||||
@@ -301,3 +480,25 @@ async fn get_cached_ts_server_binary(
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use indoc::indoc;
|
||||
|
||||
#[test]
|
||||
fn test_diagnostic_message_to_markdown() {
|
||||
let message = "Property 'user' is missing in type '{ person: { username: string; email: string; }; }' but required in type '{ user: { name: string; email: `${string}@${string}.${string}`; age: number; }; }'.";
|
||||
let expected = indoc! { "
|
||||
Property `user` is missing in type `{ person: { username: string; email: string; }; }` but required in type
|
||||
|
||||
```typescript
|
||||
{ user: { name: string; email: `${string}@${string}.${string}`; age: number; }; }
|
||||
```
|
||||
"};
|
||||
let result = VtslsLspAdapter::new(NodeRuntime::unavailable())
|
||||
.diagnostic_message_to_markdown(message)
|
||||
.unwrap();
|
||||
pretty_assertions::assert_eq!(result, expected.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1534,12 +1534,26 @@ impl MarkdownElementBuilder {
|
||||
rendered_index: self.pending_line.text.len(),
|
||||
source_index: source_range.start,
|
||||
});
|
||||
self.pending_line.text.push_str(text);
|
||||
if text.starts_with("type a =") {
|
||||
self.pending_line.text.push_str(&text["type a =".len()..]);
|
||||
} else {
|
||||
self.pending_line.text.push_str(text);
|
||||
}
|
||||
self.current_source_index = source_range.end;
|
||||
|
||||
if let Some(Some(language)) = self.code_block_stack.last() {
|
||||
dbg!(&language);
|
||||
let mut offset = 0;
|
||||
for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
|
||||
for (mut range, highlight_id) in
|
||||
language.highlight_text(&Rope::from(text), 0..text.len())
|
||||
{
|
||||
if text.starts_with("type a =") {
|
||||
if range.start < "type a =".len() || range.end < "type a =".len() {
|
||||
continue;
|
||||
}
|
||||
range.start -= "type a =".len();
|
||||
range.end -= "type a =".len();
|
||||
};
|
||||
if range.start > offset {
|
||||
self.pending_line
|
||||
.runs
|
||||
|
||||
@@ -560,6 +560,11 @@ impl DapStore {
|
||||
fn format_value(mut value: String) -> String {
|
||||
const LIMIT: usize = 100;
|
||||
|
||||
if let Some(index) = value.find("\n") {
|
||||
value.truncate(index);
|
||||
value.push_str("…");
|
||||
}
|
||||
|
||||
if value.len() > LIMIT {
|
||||
let mut index = LIMIT;
|
||||
// If index isn't a char boundary truncate will cause a panic
|
||||
@@ -567,7 +572,7 @@ impl DapStore {
|
||||
index -= 1;
|
||||
}
|
||||
value.truncate(index);
|
||||
value.push_str("...");
|
||||
value.push_str("…");
|
||||
}
|
||||
|
||||
format!(": {}", value)
|
||||
|
||||
@@ -288,7 +288,6 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
println!("this must run");
|
||||
workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
|
||||
});
|
||||
|
||||
|
||||
@@ -783,8 +783,12 @@ impl KeymapFile {
|
||||
target: &KeybindUpdateTarget<'a>,
|
||||
target_action_value: &Value,
|
||||
) -> Option<(usize, &'b str)> {
|
||||
let target_context_parsed =
|
||||
KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
|
||||
for (index, section) in keymap.sections().enumerate() {
|
||||
if section.context != target.context.unwrap_or("") {
|
||||
let section_context_parsed =
|
||||
KeyBindingContextPredicate::parse(§ion.context).ok();
|
||||
if section_context_parsed != target_context_parsed {
|
||||
continue;
|
||||
}
|
||||
if section.use_key_equivalents != target.use_key_equivalents {
|
||||
@@ -835,6 +839,7 @@ pub enum KeybindUpdateOperation<'a> {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KeybindUpdateTarget<'a> {
|
||||
pub context: Option<&'a str>,
|
||||
pub keystrokes: &'a [Keystroke],
|
||||
|
||||
@@ -10,9 +10,9 @@ use feature_flags::FeatureFlagViewExt;
|
||||
use fs::Fs;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy,
|
||||
StyledText, Subscription, WeakEntity, actions, div,
|
||||
Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, Global, KeyContext, Keystroke, ModifiersChangedEvent, MouseButton,
|
||||
Point, ScrollStrategy, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div,
|
||||
};
|
||||
use language::{Language, LanguageConfig, ToOffset as _};
|
||||
use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
|
||||
@@ -21,7 +21,7 @@ use util::ResultExt;
|
||||
|
||||
use ui::{
|
||||
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, ParentElement as _, Render,
|
||||
SharedString, Styled as _, Tooltip, Window, prelude::*, right_click_menu,
|
||||
SharedString, Styled as _, Tooltip, Window, prelude::*,
|
||||
};
|
||||
use workspace::{
|
||||
Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
|
||||
@@ -57,7 +57,11 @@ actions!(
|
||||
/// Copies the action name to clipboard.
|
||||
CopyAction,
|
||||
/// Copies the context predicate to clipboard.
|
||||
CopyContext
|
||||
CopyContext,
|
||||
/// Toggles Conflict Filtering
|
||||
ToggleConflictFilter,
|
||||
/// Toggle Keystroke search
|
||||
ToggleKeystrokeSearch,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -67,20 +71,30 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
cx.on_action(|_: &OpenKeymapEditor, cx| {
|
||||
workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
|
||||
let existing = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| item.downcast::<KeymapEditor>());
|
||||
workspace
|
||||
.with_local_workspace(window, cx, |workspace, window, cx| {
|
||||
let existing = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| item.downcast::<KeymapEditor>());
|
||||
|
||||
if let Some(existing) = existing {
|
||||
workspace.activate_item(&existing, true, true, window, cx);
|
||||
} else {
|
||||
let keymap_editor =
|
||||
cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
|
||||
workspace.add_item_to_active_pane(Box::new(keymap_editor), None, true, window, cx);
|
||||
}
|
||||
});
|
||||
if let Some(existing) = existing {
|
||||
workspace.activate_item(&existing, true, true, window, cx);
|
||||
} else {
|
||||
let keymap_editor =
|
||||
cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(keymap_editor),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
});
|
||||
|
||||
cx.observe_new(|_workspace: &mut Workspace, window, cx| {
|
||||
@@ -143,6 +157,22 @@ impl KeymapEventChannel {
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
enum SearchMode {
|
||||
#[default]
|
||||
Normal,
|
||||
KeyStroke,
|
||||
}
|
||||
|
||||
impl SearchMode {
|
||||
fn invert(&self) -> Self {
|
||||
match self {
|
||||
SearchMode::Normal => SearchMode::KeyStroke,
|
||||
SearchMode::KeyStroke => SearchMode::Normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
enum FilterState {
|
||||
#[default]
|
||||
All,
|
||||
@@ -167,13 +197,19 @@ struct ConflictState {
|
||||
}
|
||||
|
||||
impl ConflictState {
|
||||
fn new(key_bindings: &Vec<ProcessedKeybinding>) -> Self {
|
||||
fn new(key_bindings: &[ProcessedKeybinding]) -> Self {
|
||||
let mut action_keybind_mapping: HashMap<_, Vec<usize>> = HashMap::default();
|
||||
|
||||
key_bindings
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, binding)| !binding.keystroke_text.is_empty())
|
||||
.filter(|(_, binding)| {
|
||||
!binding.keystroke_text.is_empty()
|
||||
&& binding
|
||||
.source
|
||||
.as_ref()
|
||||
.is_some_and(|source| matches!(source.0, KeybindSource::User))
|
||||
})
|
||||
.for_each(|(index, binding)| {
|
||||
action_keybind_mapping
|
||||
.entry(binding.get_action_mapping())
|
||||
@@ -221,12 +257,15 @@ struct KeymapEditor {
|
||||
keybindings: Vec<ProcessedKeybinding>,
|
||||
keybinding_conflict_state: ConflictState,
|
||||
filter_state: FilterState,
|
||||
search_mode: SearchMode,
|
||||
// corresponds 1 to 1 with keybindings
|
||||
string_match_candidates: Arc<Vec<StringMatchCandidate>>,
|
||||
matches: Vec<StringMatch>,
|
||||
table_interaction_state: Entity<TableInteractionState>,
|
||||
filter_editor: Entity<Editor>,
|
||||
keystroke_editor: Entity<KeystrokeInput>,
|
||||
selected_index: Option<usize>,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for KeymapEditor {}
|
||||
@@ -239,12 +278,16 @@ impl Focusable for KeymapEditor {
|
||||
|
||||
impl KeymapEditor {
|
||||
fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let _keymap_subscription =
|
||||
cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
|
||||
let table_interaction_state = TableInteractionState::new(window, cx);
|
||||
|
||||
let keystroke_editor = cx.new(|cx| {
|
||||
let mut keystroke_editor = KeystrokeInput::new(None, window, cx);
|
||||
keystroke_editor.highlight_on_focus = false;
|
||||
keystroke_editor
|
||||
});
|
||||
|
||||
let filter_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Filter action names…", cx);
|
||||
@@ -260,18 +303,30 @@ impl KeymapEditor {
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(&keystroke_editor, |this, _, _, cx| {
|
||||
if matches!(this.search_mode, SearchMode::Normal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.update_matches(cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut this = Self {
|
||||
workspace,
|
||||
keybindings: vec![],
|
||||
keybinding_conflict_state: ConflictState::default(),
|
||||
filter_state: FilterState::default(),
|
||||
search_mode: SearchMode::default(),
|
||||
string_match_candidates: Arc::new(vec![]),
|
||||
matches: vec![],
|
||||
focus_handle: focus_handle.clone(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
_keymap_subscription,
|
||||
table_interaction_state,
|
||||
filter_editor,
|
||||
keystroke_editor,
|
||||
selected_index: None,
|
||||
context_menu: None,
|
||||
};
|
||||
|
||||
this.update_keybindings(cx);
|
||||
@@ -279,30 +334,47 @@ impl KeymapEditor {
|
||||
this
|
||||
}
|
||||
|
||||
fn current_query(&self, cx: &mut Context<Self>) -> String {
|
||||
fn current_action_query(&self, cx: &App) -> String {
|
||||
self.filter_editor.read(cx).text(cx)
|
||||
}
|
||||
|
||||
fn update_matches(&self, cx: &mut Context<Self>) {
|
||||
let query = self.current_query(cx);
|
||||
fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
|
||||
match self.search_mode {
|
||||
SearchMode::KeyStroke => self
|
||||
.keystroke_editor
|
||||
.read(cx)
|
||||
.keystrokes()
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
SearchMode::Normal => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
cx.spawn(async move |this, cx| Self::process_query(this, query, cx).await)
|
||||
.detach();
|
||||
fn update_matches(&self, cx: &mut Context<Self>) {
|
||||
let action_query = self.current_action_query(cx);
|
||||
let keystroke_query = self.current_keystroke_query(cx);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
Self::process_query(this, action_query, keystroke_query, cx).await
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
async fn process_query(
|
||||
this: WeakEntity<Self>,
|
||||
query: String,
|
||||
action_query: String,
|
||||
keystroke_query: Vec<Keystroke>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<()> {
|
||||
let query = command_palette::normalize_action_query(&query);
|
||||
let action_query = command_palette::normalize_action_query(&action_query);
|
||||
let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
|
||||
(this.string_match_candidates.clone(), this.keybindings.len())
|
||||
})?;
|
||||
let executor = cx.background_executor().clone();
|
||||
let mut matches = fuzzy::match_strings(
|
||||
&string_match_candidates,
|
||||
&query,
|
||||
&action_query,
|
||||
true,
|
||||
true,
|
||||
keybind_count,
|
||||
@@ -321,7 +393,26 @@ impl KeymapEditor {
|
||||
FilterState::All => {}
|
||||
}
|
||||
|
||||
if query.is_empty() {
|
||||
match this.search_mode {
|
||||
SearchMode::KeyStroke => {
|
||||
matches.retain(|item| {
|
||||
this.keybindings[item.candidate_id]
|
||||
.ui_key_binding
|
||||
.as_ref()
|
||||
.is_some_and(|binding| {
|
||||
keystroke_query.iter().all(|key| {
|
||||
binding.keystrokes.iter().any(|keystroke| {
|
||||
keystroke.key == key.key
|
||||
&& keystroke.modifiers == key.modifiers
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
SearchMode::Normal => {}
|
||||
}
|
||||
|
||||
if action_query.is_empty() {
|
||||
// apply default sort
|
||||
// sorts by source precedence, and alphabetically by action name within each source
|
||||
matches.sort_by_key(|match_item| {
|
||||
@@ -432,7 +523,7 @@ impl KeymapEditor {
|
||||
let json_language = load_json_language(workspace.clone(), cx).await;
|
||||
let rust_language = load_rust_language(workspace.clone(), cx).await;
|
||||
|
||||
let query = this.update(cx, |this, cx| {
|
||||
let (action_query, keystroke_query) = this.update(cx, |this, cx| {
|
||||
let (key_bindings, string_match_candidates) =
|
||||
Self::process_bindings(json_language, rust_language, cx);
|
||||
|
||||
@@ -455,10 +546,13 @@ impl KeymapEditor {
|
||||
string: candidate.string.clone(),
|
||||
})
|
||||
.collect();
|
||||
this.current_query(cx)
|
||||
(
|
||||
this.current_action_query(cx),
|
||||
this.current_keystroke_query(cx),
|
||||
)
|
||||
})?;
|
||||
// calls cx.notify
|
||||
Self::process_query(this, query, cx).await
|
||||
Self::process_query(this, action_query, keystroke_query, cx).await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -509,6 +603,73 @@ impl KeymapEditor {
|
||||
.and_then(|keybind_index| self.keybindings.get(keybind_index))
|
||||
}
|
||||
|
||||
fn select_index(&mut self, index: usize, cx: &mut Context<Self>) {
|
||||
if self.selected_index != Some(index) {
|
||||
self.selected_index = Some(index);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn create_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_menu = self.selected_binding().map(|selected_binding| {
|
||||
let selected_binding_has_no_context = selected_binding
|
||||
.context
|
||||
.as_ref()
|
||||
.and_then(KeybindContextString::local)
|
||||
.is_none();
|
||||
|
||||
let selected_binding_is_unbound = selected_binding.ui_key_binding.is_none();
|
||||
|
||||
let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
menu.action_disabled_when(
|
||||
selected_binding_is_unbound,
|
||||
"Edit",
|
||||
Box::new(EditBinding),
|
||||
)
|
||||
.action("Create", Box::new(CreateBinding))
|
||||
.action_disabled_when(
|
||||
selected_binding_is_unbound,
|
||||
"Delete",
|
||||
Box::new(DeleteBinding),
|
||||
)
|
||||
.action("Copy action", Box::new(CopyAction))
|
||||
.action_disabled_when(
|
||||
selected_binding_has_no_context,
|
||||
"Copy Context",
|
||||
Box::new(CopyContext),
|
||||
)
|
||||
});
|
||||
|
||||
let context_menu_handle = context_menu.focus_handle(cx);
|
||||
window.defer(cx, move |window, _cx| window.focus(&context_menu_handle));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
|this, _, _: &DismissEvent, window, cx| {
|
||||
this.dismiss_context_menu(window, cx);
|
||||
},
|
||||
);
|
||||
(context_menu, position, subscription)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn dismiss_context_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.context_menu.take();
|
||||
window.focus(&self.focus_handle);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn context_menu_deployed(&self) -> bool {
|
||||
self.context_menu.is_some()
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(selected) = self.selected_index {
|
||||
let selected = selected + 1;
|
||||
@@ -664,6 +825,33 @@ impl KeymapEditor {
|
||||
};
|
||||
cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
|
||||
}
|
||||
|
||||
fn toggle_conflict_filter(
|
||||
&mut self,
|
||||
_: &ToggleConflictFilter,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.filter_state = self.filter_state.invert();
|
||||
self.update_matches(cx);
|
||||
}
|
||||
|
||||
fn toggle_keystroke_search(
|
||||
&mut self,
|
||||
_: &ToggleKeystrokeSearch,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.search_mode = self.search_mode.invert();
|
||||
self.update_matches(cx);
|
||||
|
||||
match self.search_mode {
|
||||
SearchMode::KeyStroke => {
|
||||
window.focus(&self.keystroke_editor.focus_handle(cx));
|
||||
}
|
||||
SearchMode::Normal => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -763,41 +951,97 @@ impl Render for KeymapEditor {
|
||||
.on_action(cx.listener(Self::delete_binding))
|
||||
.on_action(cx.listener(Self::copy_action_to_clipboard))
|
||||
.on_action(cx.listener(Self::copy_context_to_clipboard))
|
||||
.on_action(cx.listener(Self::toggle_conflict_filter))
|
||||
.on_action(cx.listener(Self::toggle_keystroke_search))
|
||||
.size_full()
|
||||
.p_2()
|
||||
.gap_1()
|
||||
.bg(theme.colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_1()
|
||||
.key_context({
|
||||
let mut context = KeyContext::new_with_defaults();
|
||||
context.add("BufferSearchBar");
|
||||
context
|
||||
})
|
||||
.h_8()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.border_1()
|
||||
.border_color(theme.colors().border)
|
||||
.rounded_lg()
|
||||
.child(self.filter_editor.clone())
|
||||
.when(self.keybinding_conflict_state.any_conflicts(), |this| {
|
||||
this.child(
|
||||
IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
|
||||
.tooltip(Tooltip::text(match self.filter_state {
|
||||
FilterState::All => "Show conflicts",
|
||||
FilterState::Conflicts => "Hide conflicts",
|
||||
}))
|
||||
.selected_icon_color(Color::Error)
|
||||
.toggle_state(matches!(self.filter_state, FilterState::Conflicts))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.filter_state = this.filter_state.invert();
|
||||
this.update_matches(cx);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.h_8()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.border_1()
|
||||
.border_color(theme.colors().border)
|
||||
.rounded_lg()
|
||||
.child(self.filter_editor.clone()),
|
||||
)
|
||||
.child(
|
||||
// TODO: Ask Mikyala if there's a way to get have items be aligned by horizontally
|
||||
// without embedding a h_flex in another h_flex
|
||||
h_flex()
|
||||
.when(self.keybinding_conflict_state.any_conflicts(), |this| {
|
||||
this.child(
|
||||
IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
|
||||
.tooltip({
|
||||
let filter_state = self.filter_state;
|
||||
|
||||
move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
match filter_state {
|
||||
FilterState::All => "Show conflicts",
|
||||
FilterState::Conflicts => "Hide conflicts",
|
||||
},
|
||||
&ToggleConflictFilter,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.selected_icon_color(Color::Error)
|
||||
.toggle_state(matches!(
|
||||
self.filter_state,
|
||||
FilterState::Conflicts
|
||||
))
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
ToggleConflictFilter.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
IconButton::new("KeymapEditorToggleFiltersIcon", IconName::Filter)
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Toggle Keystroke Search",
|
||||
&ToggleKeystrokeSearch,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.toggle_state(matches!(self.search_mode, SearchMode::KeyStroke))
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
ToggleKeystrokeSearch.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.when(matches!(self.search_mode, SearchMode::KeyStroke), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.child(self.keystroke_editor.clone())
|
||||
.border_1()
|
||||
.border_color(theme.colors().border)
|
||||
.rounded_lg(),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Table::new()
|
||||
.interactable(&self.table_interaction_state)
|
||||
@@ -808,6 +1052,7 @@ impl Render for KeymapEditor {
|
||||
"keymap-editor-table",
|
||||
row_count,
|
||||
cx.processor(move |this, range: Range<usize>, _window, cx| {
|
||||
let context_menu_deployed = this.context_menu_deployed();
|
||||
range
|
||||
.filter_map(|index| {
|
||||
let candidate_id = this.matches.get(index)?.candidate_id;
|
||||
@@ -816,21 +1061,23 @@ impl Render for KeymapEditor {
|
||||
let action = div()
|
||||
.child(binding.action_name.clone())
|
||||
.id(("keymap action", index))
|
||||
.tooltip({
|
||||
let action_name = binding.action_name.clone();
|
||||
let action_docs = binding.action_docs;
|
||||
move |_, cx| {
|
||||
let action_tooltip = Tooltip::new(
|
||||
command_palette::humanize_action_name(
|
||||
&action_name,
|
||||
),
|
||||
);
|
||||
let action_tooltip = match action_docs {
|
||||
Some(docs) => action_tooltip.meta(docs),
|
||||
None => action_tooltip,
|
||||
};
|
||||
cx.new(|_| action_tooltip).into()
|
||||
}
|
||||
.when(!context_menu_deployed, |this| {
|
||||
this.tooltip({
|
||||
let action_name = binding.action_name.clone();
|
||||
let action_docs = binding.action_docs;
|
||||
move |_, cx| {
|
||||
let action_tooltip = Tooltip::new(
|
||||
command_palette::humanize_action_name(
|
||||
&action_name,
|
||||
),
|
||||
);
|
||||
let action_tooltip = match action_docs {
|
||||
Some(docs) => action_tooltip.meta(docs),
|
||||
None => action_tooltip,
|
||||
};
|
||||
cx.new(|_| action_tooltip).into()
|
||||
}
|
||||
})
|
||||
})
|
||||
.into_any_element();
|
||||
let keystrokes = binding.ui_key_binding.clone().map_or(
|
||||
@@ -848,12 +1095,24 @@ impl Render for KeymapEditor {
|
||||
}
|
||||
}
|
||||
};
|
||||
let context = binding
|
||||
.context
|
||||
.clone()
|
||||
.map_or(gpui::Empty.into_any_element(), |context| {
|
||||
context.into_any_element()
|
||||
});
|
||||
let context = binding.context.clone().map_or(
|
||||
gpui::Empty.into_any_element(),
|
||||
|context| {
|
||||
let is_local = context.local().is_some();
|
||||
|
||||
div()
|
||||
.id(("keymap context", index))
|
||||
.child(context.clone())
|
||||
.when(is_local && !context_menu_deployed, |this| {
|
||||
this.tooltip(Tooltip::element({
|
||||
move |_, _| {
|
||||
context.clone().into_any_element()
|
||||
}
|
||||
}))
|
||||
})
|
||||
.into_any_element()
|
||||
},
|
||||
);
|
||||
let source = binding
|
||||
.source
|
||||
.clone()
|
||||
@@ -876,9 +1135,30 @@ impl Render for KeymapEditor {
|
||||
|
||||
let row = row
|
||||
.id(("keymap-table-row", row_index))
|
||||
.on_any_mouse_down(cx.listener(
|
||||
move |this,
|
||||
mouse_down_event: &gpui::MouseDownEvent,
|
||||
window,
|
||||
cx| {
|
||||
match mouse_down_event.button {
|
||||
MouseButton::Left => {
|
||||
this.select_index(row_index, cx);
|
||||
}
|
||||
|
||||
MouseButton::Right => {
|
||||
this.select_index(row_index, cx);
|
||||
this.create_context_menu(
|
||||
mouse_down_event.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
))
|
||||
.on_click(cx.listener(
|
||||
move |this, event: &ClickEvent, window, cx| {
|
||||
this.selected_index = Some(row_index);
|
||||
if event.up.click_count == 2 {
|
||||
this.open_edit_keybinding_modal(false, window, cx);
|
||||
}
|
||||
@@ -892,18 +1172,23 @@ impl Render for KeymapEditor {
|
||||
row.border_color(cx.theme().colors().panel_focused_border)
|
||||
});
|
||||
|
||||
right_click_menu(("keymap-table-row-menu", row_index))
|
||||
.trigger(move |_, _, _| row)
|
||||
.menu({
|
||||
let this = cx.weak_entity();
|
||||
move |window, cx| {
|
||||
build_keybind_context_menu(&this, row_index, window, cx)
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
row.into_any_element()
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|this, _, _, cx| {
|
||||
this.context_menu.take();
|
||||
cx.notify();
|
||||
}))
|
||||
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
||||
deferred(
|
||||
anchored()
|
||||
.position(*position)
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1018,7 +1303,16 @@ impl KeybindingEditorModal {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let keybind_editor = cx.new(|cx| KeystrokeInput::new(window, cx));
|
||||
let keybind_editor = cx.new(|cx| {
|
||||
KeystrokeInput::new(
|
||||
editing_keybind
|
||||
.ui_key_binding
|
||||
.as_ref()
|
||||
.map(|keybinding| keybinding.keystrokes.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let context_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
@@ -1522,13 +1816,19 @@ async fn remove_keybinding(
|
||||
|
||||
struct KeystrokeInput {
|
||||
keystrokes: Vec<Keystroke>,
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
highlight_on_focus: bool,
|
||||
focus_handle: FocusHandle,
|
||||
intercept_subscription: Option<Subscription>,
|
||||
_focus_subscriptions: [Subscription; 2],
|
||||
}
|
||||
|
||||
impl KeystrokeInput {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
fn new(
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let _focus_subscriptions = [
|
||||
cx.on_focus_in(&focus_handle, window, Self::on_focus_in),
|
||||
@@ -1536,6 +1836,8 @@ impl KeystrokeInput {
|
||||
];
|
||||
Self {
|
||||
keystrokes: Vec::new(),
|
||||
placeholder_keystrokes,
|
||||
highlight_on_focus: true,
|
||||
focus_handle,
|
||||
intercept_subscription: None,
|
||||
_focus_subscriptions,
|
||||
@@ -1553,6 +1855,7 @@ impl KeystrokeInput {
|
||||
{
|
||||
if !event.modifiers.modified() {
|
||||
self.keystrokes.pop();
|
||||
cx.emit(());
|
||||
} else {
|
||||
last.modifiers = event.modifiers;
|
||||
}
|
||||
@@ -1562,6 +1865,7 @@ impl KeystrokeInput {
|
||||
key: "".to_string(),
|
||||
key_char: None,
|
||||
});
|
||||
cx.emit(());
|
||||
}
|
||||
cx.stop_propagation();
|
||||
cx.notify();
|
||||
@@ -1575,6 +1879,7 @@ impl KeystrokeInput {
|
||||
} else if Some(keystroke) != self.keystrokes.last() {
|
||||
self.keystrokes.push(keystroke.clone());
|
||||
}
|
||||
cx.emit(());
|
||||
cx.stop_propagation();
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1589,6 +1894,7 @@ impl KeystrokeInput {
|
||||
&& !last.key.is_empty()
|
||||
&& last.modifiers == event.keystroke.modifiers
|
||||
{
|
||||
cx.emit(());
|
||||
self.keystrokes.push(Keystroke {
|
||||
modifiers: event.keystroke.modifiers,
|
||||
key: "".to_string(),
|
||||
@@ -1618,6 +1924,11 @@ impl KeystrokeInput {
|
||||
}
|
||||
|
||||
fn keystrokes(&self) -> &[Keystroke] {
|
||||
if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
||||
&& self.keystrokes.is_empty()
|
||||
{
|
||||
return placeholders;
|
||||
}
|
||||
if self
|
||||
.keystrokes
|
||||
.last()
|
||||
@@ -1627,8 +1938,29 @@ impl KeystrokeInput {
|
||||
}
|
||||
return &self.keystrokes;
|
||||
}
|
||||
|
||||
fn render_keystrokes(&self) -> impl Iterator<Item = Div> {
|
||||
let (keystrokes, color) = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
||||
&& self.keystrokes.is_empty()
|
||||
{
|
||||
(placeholders, Color::Placeholder)
|
||||
} else {
|
||||
(&self.keystrokes, Color::Default)
|
||||
};
|
||||
keystrokes.iter().map(move |keystroke| {
|
||||
h_flex().children(ui::render_keystroke(
|
||||
keystroke,
|
||||
Some(color),
|
||||
Some(rems(0.875).into()),
|
||||
ui::PlatformStyle::platform(),
|
||||
false,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for KeystrokeInput {}
|
||||
|
||||
impl Focusable for KeystrokeInput {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
@@ -1645,9 +1977,11 @@ impl Render for KeystrokeInput {
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
|
||||
.on_key_up(cx.listener(Self::on_key_up))
|
||||
.focus(|mut style| {
|
||||
style.border_color = Some(colors.border_focused);
|
||||
style
|
||||
.when(self.highlight_on_focus, |this| {
|
||||
this.focus(|mut style| {
|
||||
style.border_color = Some(colors.border_focused);
|
||||
style
|
||||
})
|
||||
})
|
||||
.py_2()
|
||||
.px_3()
|
||||
@@ -1668,15 +2002,7 @@ impl Render for KeystrokeInput {
|
||||
.justify_center()
|
||||
.flex_wrap()
|
||||
.gap(ui::DynamicSpacing::Base04.rems(cx))
|
||||
.children(self.keystrokes.iter().map(|keystroke| {
|
||||
h_flex().children(ui::render_keystroke(
|
||||
keystroke,
|
||||
None,
|
||||
Some(rems(0.875).into()),
|
||||
ui::PlatformStyle::platform(),
|
||||
false,
|
||||
))
|
||||
})),
|
||||
.children(self.render_keystrokes()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -1688,6 +2014,7 @@ impl Render for KeystrokeInput {
|
||||
.when(!is_focused, |this| this.icon_color(Color::Muted))
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.keystrokes.pop();
|
||||
cx.emit(());
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
@@ -1697,6 +2024,7 @@ impl Render for KeystrokeInput {
|
||||
.when(!is_focused, |this| this.icon_color(Color::Muted))
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.keystrokes.clear();
|
||||
cx.emit(());
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
@@ -1704,53 +2032,6 @@ impl Render for KeystrokeInput {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_keybind_context_menu(
|
||||
this: &WeakEntity<KeymapEditor>,
|
||||
item_idx: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |menu, _window, cx| {
|
||||
let selected_binding = this
|
||||
.update(cx, |this, _cx| {
|
||||
this.selected_index = Some(item_idx);
|
||||
this.selected_binding().cloned()
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let Some(selected_binding) = selected_binding else {
|
||||
return menu;
|
||||
};
|
||||
|
||||
let selected_binding_has_no_context = selected_binding
|
||||
.context
|
||||
.as_ref()
|
||||
.and_then(KeybindContextString::local)
|
||||
.is_none();
|
||||
|
||||
let selected_binding_is_unbound_action = selected_binding.ui_key_binding.is_none();
|
||||
|
||||
menu.action_disabled_when(
|
||||
selected_binding_is_unbound_action,
|
||||
"Edit",
|
||||
Box::new(EditBinding),
|
||||
)
|
||||
.action("Create", Box::new(CreateBinding))
|
||||
.action_disabled_when(
|
||||
selected_binding_is_unbound_action,
|
||||
"Delete",
|
||||
Box::new(DeleteBinding),
|
||||
)
|
||||
.action("Copy action", Box::new(CopyAction))
|
||||
.action_disabled_when(
|
||||
selected_binding_has_no_context,
|
||||
"Copy Context",
|
||||
Box::new(CopyContext),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_contexts_from_assets() -> Vec<SharedString> {
|
||||
let mut keymap_assets = vec![
|
||||
util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
|
||||
|
||||
@@ -127,7 +127,7 @@ impl BatchedTextRun {
|
||||
cx: &mut App,
|
||||
) {
|
||||
let pos = Point::new(
|
||||
(origin.x + self.start_point.column as f32 * dimensions.cell_width).floor(),
|
||||
origin.x + self.start_point.column as f32 * dimensions.cell_width,
|
||||
origin.y + self.start_point.line as f32 * dimensions.line_height,
|
||||
);
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ impl OnboardingBanner {
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut Context<Self>) {
|
||||
telemetry::event!("Banner Dismissed", source = self.source);
|
||||
persist_dismissed(&self.source, cx);
|
||||
self.dismissed = true;
|
||||
cx.notify();
|
||||
@@ -144,7 +143,10 @@ impl Render for OnboardingBanner {
|
||||
div().border_l_1().border_color(border_color).child(
|
||||
IconButton::new("close", IconName::Close)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
telemetry::event!("Banner Dismissed", source = this.source);
|
||||
this.dismiss(cx)
|
||||
}))
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Close Announcement Banner",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
@@ -7,15 +9,36 @@ use crate::{Color, KeyBinding, Label, LabelSize, StyledExt, h_flex, v_flex};
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct Tooltip {
|
||||
title: SharedString,
|
||||
title: Title,
|
||||
meta: Option<SharedString>,
|
||||
key_binding: Option<KeyBinding>,
|
||||
}
|
||||
|
||||
#[derive(Clone, IntoElement)]
|
||||
enum Title {
|
||||
Str(SharedString),
|
||||
Callback(Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>),
|
||||
}
|
||||
|
||||
impl From<SharedString> for Title {
|
||||
fn from(value: SharedString) -> Self {
|
||||
Title::Str(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Title {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement {
|
||||
match self {
|
||||
Title::Str(title) => title.into_any_element(),
|
||||
Title::Callback(element) => element(window, cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tooltip {
|
||||
pub fn simple(title: impl Into<SharedString>, cx: &mut App) -> AnyView {
|
||||
cx.new(|_| Self {
|
||||
title: title.into(),
|
||||
title: Title::Str(title.into()),
|
||||
meta: None,
|
||||
key_binding: None,
|
||||
})
|
||||
@@ -26,7 +49,7 @@ impl Tooltip {
|
||||
let title = title.into();
|
||||
move |_, cx| {
|
||||
cx.new(|_| Self {
|
||||
title: title.clone(),
|
||||
title: title.clone().into(),
|
||||
meta: None,
|
||||
key_binding: None,
|
||||
})
|
||||
@@ -34,15 +57,15 @@ impl Tooltip {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_action_title<Title: Into<SharedString>>(
|
||||
title: Title,
|
||||
pub fn for_action_title<T: Into<SharedString>>(
|
||||
title: T,
|
||||
action: &dyn Action,
|
||||
) -> impl Fn(&mut Window, &mut App) -> AnyView + use<Title> {
|
||||
) -> impl Fn(&mut Window, &mut App) -> AnyView + use<T> {
|
||||
let title = title.into();
|
||||
let action = action.boxed_clone();
|
||||
move |window, cx| {
|
||||
cx.new(|cx| Self {
|
||||
title: title.clone(),
|
||||
title: Title::Str(title.clone()),
|
||||
meta: None,
|
||||
key_binding: KeyBinding::for_action(action.as_ref(), window, cx),
|
||||
})
|
||||
@@ -60,7 +83,7 @@ impl Tooltip {
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
cx.new(|cx| Self {
|
||||
title: title.clone(),
|
||||
title: Title::Str(title.clone()),
|
||||
meta: None,
|
||||
key_binding: KeyBinding::for_action_in(action.as_ref(), &focus_handle, window, cx),
|
||||
})
|
||||
@@ -75,7 +98,7 @@ impl Tooltip {
|
||||
cx: &mut App,
|
||||
) -> AnyView {
|
||||
cx.new(|cx| Self {
|
||||
title: title.into(),
|
||||
title: Title::Str(title.into()),
|
||||
meta: None,
|
||||
key_binding: KeyBinding::for_action(action, window, cx),
|
||||
})
|
||||
@@ -90,7 +113,7 @@ impl Tooltip {
|
||||
cx: &mut App,
|
||||
) -> AnyView {
|
||||
cx.new(|cx| Self {
|
||||
title: title.into(),
|
||||
title: title.into().into(),
|
||||
meta: None,
|
||||
key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx),
|
||||
})
|
||||
@@ -105,7 +128,7 @@ impl Tooltip {
|
||||
cx: &mut App,
|
||||
) -> AnyView {
|
||||
cx.new(|cx| Self {
|
||||
title: title.into(),
|
||||
title: title.into().into(),
|
||||
meta: Some(meta.into()),
|
||||
key_binding: action.and_then(|action| KeyBinding::for_action(action, window, cx)),
|
||||
})
|
||||
@@ -121,7 +144,7 @@ impl Tooltip {
|
||||
cx: &mut App,
|
||||
) -> AnyView {
|
||||
cx.new(|cx| Self {
|
||||
title: title.into(),
|
||||
title: title.into().into(),
|
||||
meta: Some(meta.into()),
|
||||
key_binding: action
|
||||
.and_then(|action| KeyBinding::for_action_in(action, focus_handle, window, cx)),
|
||||
@@ -131,12 +154,35 @@ impl Tooltip {
|
||||
|
||||
pub fn new(title: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
title: title.into().into(),
|
||||
meta: None,
|
||||
key_binding: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_element(title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static) -> Self {
|
||||
Self {
|
||||
title: Title::Callback(Rc::new(title)),
|
||||
meta: None,
|
||||
key_binding: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn element(
|
||||
title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
|
||||
) -> impl Fn(&mut Window, &mut App) -> AnyView {
|
||||
let title = Title::Callback(Rc::new(title));
|
||||
move |_, cx| {
|
||||
let title = title.clone();
|
||||
cx.new(|_| Self {
|
||||
title: title,
|
||||
meta: None,
|
||||
key_binding: None,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
|
||||
self.meta = Some(meta.into());
|
||||
self
|
||||
|
||||
@@ -909,7 +909,6 @@ impl Render for PanelButtons {
|
||||
.on_click({
|
||||
let action = action.boxed_clone();
|
||||
move |_, window, cx| {
|
||||
println!("panel button click");
|
||||
window.dispatch_action(action.boxed_clone(), cx)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -54,6 +54,7 @@ impl sqlez::bindable::Bind for SerializedAxis {
|
||||
}
|
||||
}
|
||||
|
||||
// > https://zed.dev/cla
|
||||
impl sqlez::bindable::Column for SerializedAxis {
|
||||
fn column(
|
||||
statement: &mut sqlez::statement::Statement,
|
||||
|
||||
@@ -1219,7 +1219,6 @@ impl Workspace {
|
||||
.detach();
|
||||
|
||||
cx.on_focus_lost(window, |this, window, cx| {
|
||||
println!("workspace on_focus_lost");
|
||||
let focus_handle = this.focus_handle(cx);
|
||||
window.focus(&focus_handle);
|
||||
})
|
||||
@@ -1246,8 +1245,6 @@ impl Workspace {
|
||||
|
||||
window.focus(¢er_pane.focus_handle(cx));
|
||||
|
||||
// center_pane.focus_handle(cx).is_focused(window);
|
||||
|
||||
cx.emit(Event::PaneAdded(center_pane.clone()));
|
||||
|
||||
let window_handle = window.window_handle().downcast::<Workspace>().unwrap();
|
||||
|
||||
@@ -50,12 +50,12 @@ fn main() {
|
||||
println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024);
|
||||
}
|
||||
|
||||
let release_channel = option_env!("RELEASE_CHANNEL").unwrap_or("nightly");
|
||||
|
||||
let release_channel = option_env!("RELEASE_CHANNEL").unwrap_or("dev");
|
||||
let icon = match release_channel {
|
||||
"stable" => "resources/windows/app-icon.ico",
|
||||
"preview" => "resources/windows/app-icon-preview.ico",
|
||||
"nightly" => "resources/windows/app-icon-nightly.ico",
|
||||
"dev" => "resources/windows/app-icon-dev.ico",
|
||||
_ => "resources/windows/app-icon-dev.ico",
|
||||
};
|
||||
let icon = std::path::Path::new(icon);
|
||||
|
||||
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 577 KiB After Width: | Height: | Size: 162 KiB |
@@ -56,6 +56,13 @@ function PrepareForBundle {
|
||||
New-Item -Path "$innoDir\tools" -ItemType Directory -Force
|
||||
}
|
||||
|
||||
function GenerateLicenses {
|
||||
$oldErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
. $PSScriptRoot/generate-licenses.ps1
|
||||
$ErrorActionPreference = $oldErrorActionPreference
|
||||
}
|
||||
|
||||
function BuildZedAndItsFriends {
|
||||
Write-Output "Building Zed and its friends, for channel: $channel"
|
||||
# Build zed.exe, cli.exe and auto_update_helper.exe
|
||||
@@ -167,7 +174,7 @@ function BuildInstaller {
|
||||
}
|
||||
"dev" {
|
||||
$appId = "{{8357632E-24A4-4F32-BA97-E575B4D1FE5D}"
|
||||
$appIconName = "app-icon-nightly"
|
||||
$appIconName = "app-icon-dev"
|
||||
$appName = "Zed Dev"
|
||||
$appDisplayName = "Zed Dev"
|
||||
$appSetupName = "ZedEditorUserSetup-x64-$env:RELEASE_VERSION-dev"
|
||||
@@ -238,6 +245,7 @@ $innoDir = "$env:ZED_WORKSPACE\inno"
|
||||
|
||||
CheckEnvironmentVariables
|
||||
PrepareForBundle
|
||||
GenerateLicenses
|
||||
BuildZedAndItsFriends
|
||||
MakeAppx
|
||||
SignZedAndItsFriends
|
||||
|
||||
44
script/generate-licenses.ps1
Normal file
@@ -0,0 +1,44 @@
|
||||
$CARGO_ABOUT_VERSION="0.7"
|
||||
$outputFile=$args[0] ? $args[0] : "$(Get-Location)/assets/licenses.md"
|
||||
$templateFile="script/licenses/template.md.hbs"
|
||||
|
||||
New-Item -Path "$outputFile" -ItemType File -Value "" -Force
|
||||
|
||||
@(
|
||||
"# ###### THEME LICENSES ######\n"
|
||||
Get-Content assets/themes/LICENSES
|
||||
"\n# ###### ICON LICENSES ######\n"
|
||||
Get-Content assets/icons/LICENSES
|
||||
"\n# ###### CODE LICENSES ######\n"
|
||||
) | Add-Content -Path $outputFile
|
||||
|
||||
$versionOutput = cargo about --version
|
||||
if (-not ($versionOutput -match "cargo-about $CARGO_ABOUT_VERSION")) {
|
||||
Write-Host "Installing cargo-about@^$CARGO_ABOUT_VERSION..."
|
||||
cargo install "cargo-about@^$CARGO_ABOUT_VERSION"
|
||||
} else {
|
||||
Write-Host "cargo-about@^$CARGO_ABOUT_VERSION" is already installed
|
||||
}
|
||||
|
||||
Write-Host "Generating cargo licenses"
|
||||
|
||||
$failFlag = $env:ALLOW_MISSING_LICENSES ? "--fail" : ""
|
||||
$args = @('about', 'generate', $failFlag, '-c', 'script/licenses/zed-licenses.toml', $templateFile, '-o', $outputFile) | Where-Object { $_ }
|
||||
cargo @args
|
||||
|
||||
Write-Host "Applying replacements"
|
||||
$replacements = @{
|
||||
'"' = '"'
|
||||
''' = "'"
|
||||
'=' = '='
|
||||
'`' = '`'
|
||||
'<' = '<'
|
||||
'>' = '>'
|
||||
}
|
||||
$content = Get-Content $outputFile
|
||||
foreach ($find in $replacements.keys) {
|
||||
$content = $content -replace $find, $replacements[$find]
|
||||
}
|
||||
$content | Set-Content $outputFile
|
||||
|
||||
Write-Host "generate-licenses completed. See $outputFile"
|
||||