Compare commits

..

23 Commits

Author SHA1 Message Date
Conrad Irwin
53cf8a4e0e WIP WIPW IPW
t
2025-07-11 16:49:14 -06:00
Conrad Irwin
6a9ec10dac WIP 2025-07-11 16:02:53 -06:00
Julia Ryan
c3edc2cfc1 DAP log view improvements (#34311)
Now DAP logs show the label of each session which makes it much easier
to pick out the right one.

Also "initialization sequence" now shows up correctly when that view is
selected.

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-07-11 19:34:53 +00:00
Mikayla Maki
625a4b90a5 Tinker with the reporting of telemetry events (#34239)
Release Notes:

- N/A

---------

Co-authored-by: Katie Geer <katie@zed.dev>
2025-07-11 12:02:40 -07:00
Lukas Spiss
fbead09c30 go: Write envFile properties back to env config (#34300)
Closes https://github.com/zed-industries/zed/issues/32984

Note that while https://github.com/zed-industries/zed/pull/33666 did the
reading of the `envFile` just fine, the read values were never passed
along. This was mentioned by [this
comment](https://github.com/zed-industries/zed/pull/33666#issuecomment-3060785970)
and also confirmed by myself.

With the changes here, I successfully debugged a project of mine and all
the environment variables from my `.env` were present.

Release Notes:

- Fix Go debugger ignoring env vars from the envFile setting.
2025-07-11 18:26:46 +00:00
Ben Kunkle
0797f7b66e keymap_ui: Show existing keystrokes as placeholders in edit modal (#34307)
Closes #ISSUE

Previously, the keystroke input would be empty, even when editing an
existing binding. This meant you had to re-enter the bindings even if
you just wanted to edit the context. Now, the existing keystrokes are
rendered as a placeholder, are re-shown if newly entered keystrokes are
cleared, and will be returned from the `KeystrokeInput::keystrokes()`
method if no new keystrokes were entered.

Additionally fixed a bug in `KeymapFile::update_keybinding` where
semantically identical contexts would be treated as unequal due to
formatting differences.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-11 18:07:04 +00:00
Agus Zubiaga
6f6c2915b2 Display and jump to tool locations (#34304)
Release Notes:

- N/A
2025-07-11 14:52:21 -03:00
Julia Ryan
0bd65829f7 Truncate multi-line debug value hints (#34305)
Release Notes:

- Multi-line debug inline values are now truncated.

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-07-11 17:49:52 +00:00
Finn Evers
90bf602ceb Reduce number of snapshots and notifies during editor scrolling (#34228)
We not do not create new snapshots anymore when autoscrolling
horizontally and also do not notify any longer should the new scroll
position match the old one.

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-07-11 17:34:45 +00:00
localcc
cd024b8870 Add licenses.md for Windows build (#34272)
Release Notes:

- N/A
2025-07-11 19:28:48 +02:00
Finn Evers
af71e15ea0 editor: Fix scrolling stuttering at the top of multibuffers (#34295)
Release Notes:

- Fixed an issue where scrolling would stutter at the top of
multibuffers.
2025-07-11 19:10:39 +02:00
Agus Zubiaga
d0e01dbd8f Improve thread message history (#34299)
- Keep history across threads
- Reset position when edited

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-07-11 16:24:41 +00:00
Danilo Leal
d65855c4a1 git: Change merge conflict button labels (#34297)
Following feedback that "Take Ours" and "Take Theirs" was confusing,
leading to users not knowing what exactly happened with each of these
buttons. It's now "Use HEAD" and "Use Origin", which also match what is
written in Git markers, helping parse them out more easily. Future
improvement is to have the actual branch target name in the "Use Origin"
button.

Release Notes:

- git: Improved merge conflict buttons clarity by changing labels to
"Use HEAD" and "Use Origin".
2025-07-11 13:15:06 -03:00
Alisina Bahadori
70351360d7 Fix bad kerning in integrated terminal (#34292)
Closes #16869

Release Notes:

- (preview only): Fix bad kerning in integrated terminal.
2025-07-11 09:50:48 -06:00
Conrad Irwin
993e0f55ec ACP follow (#34235)
Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-07-11 12:38:42 -03:00
Ben Kunkle
496bf0ec43 keymap_ui: Ensure keymap UI opens in local workspace (#34291)
Closes #ISSUE

Use `workspace.with_local_workspace` to ensure the keymap UI is opened
in a local workspace, even in remote. This was tested by removing the
feature flag handling code, as with the feature flag logic the action
does not appear which is likely a bug.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-11 11:32:59 -04:00
morgankrey
c09f484ec4 collab: Add ability to add tax ID during Stripe Checkout (#34246)
### 1. **Added Tax ID Collection Types**
- Created a new `StripeTaxIdCollection` struct with an `enabled` field
- Added `tax_id_collection` field to `StripeCreateCheckoutSessionParams`

### 2. **Updated the Stripe Client Interface**
- Modified the real Stripe client to handle tax ID collection conversion
- Updated the fake Stripe client for testing purposes
- Added proper imports across all affected files

### 3. **Enabled Tax ID Collection in Checkout Sessions**
- Both `checkout_with_zed_pro` and `checkout_with_zed_pro_trial` methods
now enable tax ID collection
- The implementation correctly sets `tax_id_collection.enabled = true`
for all checkout sessions

### 4. **Key Implementation Details**
- Tax ID collection will be shown to new customers and existing
customers without tax IDs
- Collected tax IDs will be automatically saved to the customer's
`tax_ids` array in Stripe
- Business names will be saved to the customer's `name` property
- The existing `customer_update.name = auto` setting ensures
compatibility with tax ID collection

Release Notes:

- N/A
2025-07-11 11:26:36 -04:00
Finn Evers
a58a75c0f6 keymap_ui: Hide tooltips when context menu is shown (#34286)
This PR ensures tooltips are dismissed/not shown once the context menu
is opened.

It also ensures the context menu is dismissed once the list is scrolled.

Release Notes:

- N/A
2025-07-11 15:17:34 +00:00
Ben Kunkle
d1a6c5d494 keymap_ui: Hover tooltip for context (#34290)
Closes #ISSUE

Ideally the tooltip would only appear if the context was overflowing
it's column, but for now, we just unconditionally show a tooltip so that
long contexts can be seen.

This PR also includes a change to the tooltip element, allowing for
tooltips with non-text contents which is used here for syntax
highlighting

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: Anthony <anthony@zed.dev>
2025-07-11 10:54:08 -04:00
Peter Tripp
10028aaae8 Ensure *.json recognized as JSONC if checkout folder not zed (#34289)
Follow-up to: https://github.com/zed-industries/zed/pull/33410

Release Notes:

- N/A
2025-07-11 14:25:09 +00:00
Ben Kunkle
3b9bb521f4 keymap_ui: Only show conflicts between user bindings (#34284)
Closes #ISSUE

This makes it so conflicts are only shown between user bindings. User
bindings that override bindings in the Vim, Base, and Default keymaps
are not identified as conflicts

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: Anthony <anthony@zed.dev>
2025-07-11 13:51:21 +00:00
Anthony Eid
7eb739d489 Add initial support for search by keystroke to keybinding editor (#34274)
This PR adds preliminary support for searching keybindings by keystrokes
in the keybinding editor.

Release Notes:

- N/A
2025-07-11 09:29:29 -04:00
localcc
b4cbea50bb Fix icon size on Windows (#34277)
Closes #34122

Release Notes:

- N/A
2025-07-11 15:09:10 +02:00
56 changed files with 2400 additions and 674 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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"

View File

@@ -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"
}
}
]

View File

@@ -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"
}
}
]

View File

@@ -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

View File

@@ -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| {

View File

@@ -2,4 +2,5 @@ mod completion_provider;
mod message_history;
mod thread_view;
pub use message_history::MessageHistory;
pub use thread_view::AcpThreadView;

View File

@@ -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);

View File

@@ -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)),
),
)
}
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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

View File

@@ -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")?)

View File

@@ -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>,

View File

@@ -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 {

View File

@@ -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,
}
}
}

View File

@@ -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(())
}

View File

@@ -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";

View File

@@ -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))

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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,
)
})?;

View File

@@ -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);
}
});
}

View File

@@ -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();

View File

@@ -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")
})??;

View File

@@ -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()

View File

@@ -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> {

View File

@@ -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
}
}

View File

@@ -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();

View File

@@ -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();
}
})

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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

View File

@@ -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"));

View File

@@ -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(

View File

@@ -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());
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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);
});

View File

@@ -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(&section.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],

View File

@@ -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),

View File

@@ -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,
);

View File

@@ -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",

View File

@@ -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

View File

@@ -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)
}
})

View File

@@ -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,

View File

@@ -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(&center_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();

View File

@@ -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);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 162 KiB

View File

@@ -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

View 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 = @{
'&quot;' = '"'
'&#x27;' = "'"
'&#x3D;' = '='
'&#x60;' = '`'
'&lt;' = '<'
'&gt;' = '>'
}
$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"