Compare commits

...

18 Commits

Author SHA1 Message Date
Joseph T. Lyons
3cfaadd2af zed 0.181.2 2025-04-03 14:45:38 -04:00
Danilo Leal
b69219e9e0 agent: Add token count in the thread view (#28037)
This PR adds the token count to the active thread view. It doesn't
behaves quite like Assistant 1 where it updates as you type, though; it
updates after you submit the message.

<img
src="https://github.com/user-attachments/assets/82d2a180-554a-43ee-b776-3743359b609b"
width="700" />

---

Release Notes:

- agent: Add token count in the thread view

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-04-03 14:44:32 -04:00
Antonio Scandurra
0ae3518b1c Fix soft-wrapping with fold creases (#28029)
Release Notes:

- Fixed a rendering bug that caused context in the agent to not wrap
properly.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Zed AI <ai+claude-3.7@zed.dev>
2025-04-03 14:21:43 -04:00
Agus Zubiaga
73964353a4 agent: Handle tool use without text (#28030)
### Context 

The Anthropic API fails if a request message contains a tool use and no
`Text` segments or it only contains empty `Text` segments. These are
cases that the model itself produces, but the API doesn't support
sending them back.

#27917 fixed this by appending "Using tool..." in the thread's message,
but this causes the actual conversation to include it, so it would
appear in the UI (we would actually display a gap because we never
rendered its markdown, but "Using tool..." would show up when the thread
was restored).

### Solution

We'll now only append this placeholder when we build the request, so the
API still sees it, but the UI/Thread doesn't.

Another issue we found is that the model starts mimicking these
placeholders in later tool uses which is undesirable. So unfortunately,
we had to add logic to filter them out.

Release Notes:

- agent: Improved rendering of tool uses without text

---------

Co-authored-by: Bennet <bennet@zed.dev>
2025-04-03 14:20:56 -04:00
Danilo Leal
bf6e7cb6ee agent: Add button to continue iterating once all reviews are done (#28027)
This PR adds a button on the review tab empty state that toggles the
focus back to the agent panel so that users can keep iterating on the
thread that's active in the panel.

<img
src="https://github.com/user-attachments/assets/ace5cf93-8869-49bb-8106-e03a9e3c90f2"
width="700"/>

Release Notes:

- N/A
2025-04-03 14:20:37 -04:00
Joseph T. Lyons
36c4f6082c zed 0.181.1 2025-04-03 09:50:23 -04:00
Bennet Bo Fenner
3d032bcf2c agent: Fix thinking step showing up as pending when completion is cancelled (#28019)
Previously the "Thinking..." step would show up as pending, even though
the user cancelled the generation:
<img width="672" alt="image"
src="https://github.com/user-attachments/assets/c9cdce0a-d827-4e23-96f5-b150465911a7"
/>


Release Notes:

- Fixed an issue where the thinking step would show up as pending even
when the generation was cancelled
2025-04-03 09:49:24 -04:00
Agus Zubiaga
3e28fa2cc4 agent: Include active file in recent history (#27914)
This happened because of two reasons:

- `Workspace::recent_navigation_history` didn't include the current file
- The context picker added the current file to a exclude list

The latter was actually intentional because we already show the file in
the suggested context, but now that we actually have mentions, it's just
inconvenient not to have it there.

Release Notes:

- N/A
2025-04-03 09:49:18 -04:00
Julia Ryan
c42442974d workspace-hack: remove openssl from remote_server (#27990)
This was accidentally getting added due to increased feature
unification. We've manually excluded reqwest to go back to the desired
behavior: remote_server, doesn't depend on openssl.

Release Notes:

- N/A

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-04-03 08:54:26 -04:00
Julia Ryan
f23b972203 Add workspace-hack (#27277)
This adds a "workspace-hack" crate, see
[mozilla's](https://hg.mozilla.org/mozilla-central/file/3a265fdc9f33e5946f0ca0a04af73acd7e6d1a39/build/workspace-hack/Cargo.toml#l7)
for a concise explanation of why this is useful. For us in practice this
means that if I were to run all the tests (`cargo nextest r
--workspace`) and then `cargo r`, all the deps from the previous cargo
command will be reused. Before this PR it would rebuild many deps due to
resolving different sets of features for them. For me this frequently
caused long rebuilds when things "should" already be cached.

To avoid manually maintaining our workspace-hack crate, we will use
[cargo hakari](https://docs.rs/cargo-hakari) to update the build files
when there's a necessary change. I've added a step to CI that checks
whether the workspace-hack crate is up to date, and instructs you to
re-run `script/update-workspace-hack` when it fails.

Finally, to make sure that people can still depend on crates in our
workspace without pulling in all the workspace deps, we use a `[patch]`
section following [hakari's
instructions](https://docs.rs/cargo-hakari/0.9.36/cargo_hakari/patch_directive/index.html)

One possible followup task would be making guppy use our
`rust-toolchain.toml` instead of having to duplicate that list in its
config, I opened an issue for that upstream: guppy-rs/guppy#481.

TODO:
- [x] Fix the extension test failure
- [x] Ensure the dev dependencies aren't being unified by Hakari into
the main dependencies
- [x] Ensure that the remote-server binary continues to not depend on
LibSSL

Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-04-03 08:54:26 -04:00
5brian
07d9cd7e88 agent: Update thread label to use plural form (#27971)
Update thread label to match the other contexts.

|Before|After|
|--|--|

|![image](https://github.com/user-attachments/assets/6e02808e-50d7-480f-a9ca-251e9519a71d)|![image](https://github.com/user-attachments/assets/174aad84-9e55-4531-bb4a-1a1adaa46418)|

Release Notes:

- N/A
2025-04-03 08:54:01 -04:00
Marshall Bowers
a48238701d agent: Allow editing previous messages (#27965)
This PR adds the ability to edit previous user messages in the thread.

Release Notes:

- Agent: Added the ability to edit previous user messages
(Preview-only).
2025-04-03 08:54:01 -04:00
Danilo Leal
d17d747c62 agent: Change loading label if command is waiting on permission (#27955)
If there's a command pending confirmation, the label changes from
"Generating" to "Waiting for confirmation".

<img
src="https://github.com/user-attachments/assets/d804e382-5315-40b0-9588-c257cca2430c"
width="600"/>

Release Notes:

- N/A
2025-04-03 08:54:01 -04:00
Danilo Leal
bc08df2dfd agent: Refine feedback message input (#27948)
<img
src="https://github.com/user-attachments/assets/cde37a88-9973-4c27-80b7-459f5e986c74"
width="650" />

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-04-03 08:54:01 -04:00
Shardul Vaidya
0f4b734d91 aws_http_client: Copy response headers (#27941)
Preemptive fixes required for #26734

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-04-03 08:53:37 -04:00
Marshall Bowers
6df29d3279 agent: Do some cleanup of feedback comments submission (#27940)
This PR does some stylistic cleanup of the feedback comments submission
code.

Release Notes:

- N/A
2025-04-03 08:53:29 -04:00
Michael Sloan
647cca8c8d Use worktree qualified paths in agent file context + some code cleanup (#27943)
Release Notes:

- N/A
2025-04-03 08:53:19 -04:00
Joseph T. Lyons
1f81674927 v0.181.x preview 2025-04-02 13:44:55 -04:00
214 changed files with 3170 additions and 1423 deletions

43
.config/hakari.toml Normal file
View File

@@ -0,0 +1,43 @@
# This file contains settings for `cargo hakari`.
# See https://docs.rs/cargo-hakari/latest/cargo_hakari/config for a full list of options.
hakari-package = "workspace-hack"
resolver = "2"
dep-format-version = "4"
workspace-hack-line-style = "workspace-dotted"
# this should be the same list as "targets" in ../rust-toolchain.toml
platforms = [
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"x86_64-unknown-linux-musl", # remote server
]
[traversal-excludes]
workspace-members = [
"remote_server",
]
third-party = [
{ name = "reqwest", version = "0.11.27" },
]
[final-excludes]
workspace-members = [
"zed_extension_api",
# exclude all extensions
"zed_emmet",
"zed_glsl",
"zed_html",
"perplexity",
"zed_proto",
"zed_ruff",
"slash_commands_example",
"zed_snippets",
"zed_test_extension",
"zed_toml",
]

View File

@@ -110,6 +110,37 @@ jobs:
input: "crates/proto/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
workspace_hack:
timeout-minutes: 60
name: Check workspace-hack crate
needs: [job_spec]
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-8vcpu-ubuntu-2204
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install cargo-hakari
uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 # v2
with:
command: install
args: cargo-hakari@0.9.35
- name: Check workspace-hack Cargo.toml is up-to-date
run: |
cargo hakari generate --diff || {
echo "To fix, run script/update-workspace-hack";
false
}
- name: Check all crates depend on workspace-hack
run: |
cargo hakari manage-deps --dry-run || {
echo "To fix, run script/update-workspace-hack"
false
}
style:
timeout-minutes: 60
name: Check formatting and spelling
@@ -432,6 +463,7 @@ jobs:
- job_spec
- style
- migration_checks
- workspace_hack
- linux_tests
- build_remote_server
- macos_tests

1810
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -192,6 +192,7 @@ members = [
# Tooling
#
"tooling/workspace-hack",
"tooling/xtask",
]
default-members = ["crates/zed"]
@@ -590,6 +591,7 @@ wasmtime = { version = "29", default-features = false, features = [
wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.4"
zstd = "0.11"
metal = "0.29"
@@ -660,6 +662,9 @@ real-async-tls = { git = "https://github.com/zed-industries/async-tls", rev = "1
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
# Makes the workspace hack crate refer to the local one, but only when you're building locally
workspace-hack = { path = "tooling/workspace-hack" }
[profile.dev]
split-debuginfo = "unpacked"
debug = "limited"
@@ -772,4 +777,4 @@ let_underscore_future = "allow"
too_many_arguments = "allow"
[workspace.metadata.cargo-machete]
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme"]
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme", "workspace-hack"]

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-forward-icon lucide-forward"><polyline points="15 17 20 12 15 7"/><path d="M4 18v-2a4 4 0 0 1 4-4h12"/></svg>

After

Width:  |  Height:  |  Size: 312 B

View File

@@ -657,6 +657,15 @@
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"bindings": {

View File

@@ -317,6 +317,15 @@
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"use_key_equivalents": true,

View File

@@ -25,6 +25,7 @@ smallvec.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -86,6 +86,7 @@ uuid.workspace = true
vim_mode_setting.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }

View File

@@ -55,8 +55,7 @@ pub struct ActiveThread {
notifications: Vec<WindowHandle<AgentNotification>>,
_subscriptions: Vec<Subscription>,
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
showing_feedback_comments: bool,
feedback_comments_editor: Option<Entity<Editor>>,
feedback_message_editor: Option<Entity<Editor>>,
}
struct RenderedMessage {
@@ -371,8 +370,7 @@ impl ActiveThread {
notifications: Vec::new(),
_subscriptions: subscriptions,
notification_subscriptions: HashMap::default(),
showing_feedback_comments: false,
feedback_comments_editor: None,
feedback_message_editor: None,
};
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
@@ -863,19 +861,6 @@ impl ActiveThread {
cx.notify();
}
fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
self.messages
.iter()
.rev()
.find(|message_id| {
self.thread
.read(cx)
.message(**message_id)
.map_or(false, |message| message.role == Role::User)
})
.cloned()
}
fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
self.messages
.iter()
@@ -923,77 +908,59 @@ impl ActiveThread {
}
fn handle_show_feedback_comments(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.showing_feedback_comments = true;
if self.feedback_comments_editor.is_none() {
let buffer = cx.new(|cx| {
let empty_string = String::new();
MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
});
let editor = cx.new(|cx| {
Editor::new(
editor::EditorMode::AutoHeight { max_lines: 4 },
buffer,
None,
window,
cx,
)
});
self.feedback_comments_editor = Some(editor);
if self.feedback_message_editor.is_some() {
return;
}
let buffer = cx.new(|cx| {
let empty_string = String::new();
MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
});
let editor = cx.new(|cx| {
let mut editor = Editor::new(
editor::EditorMode::AutoHeight { max_lines: 4 },
buffer,
None,
window,
cx,
);
editor.set_placeholder_text(
"What went wrong? Share your feedback so we can improve.",
cx,
);
editor
});
editor.read(cx).focus_handle(cx).focus(window);
self.feedback_message_editor = Some(editor);
cx.notify();
}
fn handle_submit_comments(
&mut self,
_: &ClickEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(editor) = self.feedback_comments_editor.clone() {
let comments = editor.read(cx).text(cx);
fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
let Some(editor) = self.feedback_message_editor.clone() else {
return;
};
// Submit negative feedback
let report = self.thread.update(cx, |thread, cx| {
thread.report_feedback(ThreadFeedback::Negative, cx)
});
let report_task = self.thread.update(cx, |thread, cx| {
thread.report_feedback(ThreadFeedback::Negative, cx)
});
if !comments.is_empty() {
let thread_id = self.thread.read(cx).id().clone();
let comments_value = String::from(comments.as_str());
let comments = editor.read(cx).text(cx);
if !comments.is_empty() {
let thread_id = self.thread.read(cx).id().clone();
// Log comments as a separate telemetry event
telemetry::event!(
"Assistant Thread Feedback Comments",
thread_id,
comments = comments_value
);
}
self.showing_feedback_comments = false;
self.feedback_comments_editor = None;
let this = cx.entity().downgrade();
cx.spawn(async move |_, cx| {
report.await?;
this.update(cx, |_this, cx| cx.notify())
})
.detach_and_log_err(cx);
telemetry::event!("Assistant Thread Feedback Comments", thread_id, comments);
}
}
fn handle_cancel_comments(
&mut self,
_: &ClickEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.showing_feedback_comments = false;
self.feedback_comments_editor = None;
cx.notify();
self.feedback_message_editor = None;
let this = cx.entity().downgrade();
cx.spawn(async move |_, cx| {
report_task.await?;
this.update(cx, |_this, cx| cx.notify())
})
.detach_and_log_err(cx);
}
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
@@ -1021,8 +988,7 @@ impl ActiveThread {
return Empty.into_any();
}
let allow_editing_message =
message.role == Role::User && self.last_user_message(cx) == Some(message_id);
let allow_editing_message = message.role == Role::User;
let edit_message_editor = self
.editing_message
@@ -1133,36 +1099,47 @@ impl ActiveThread {
.into_any_element(),
};
let message_content = v_flex()
.gap_1p5()
.child(
if let Some(edit_message_editor) = edit_message_editor.clone() {
div()
.key_context("EditMessageEditor")
.on_action(cx.listener(Self::cancel_editing_message))
.on_action(cx.listener(Self::confirm_editing_message))
.min_h_6()
.child(edit_message_editor)
} else {
div()
.min_h_6()
.text_ui(cx)
.child(self.render_message_content(
message_id,
rendered_message,
has_tool_uses,
cx,
))
},
)
.when(!context.is_empty(), |parent| {
parent.child(
h_flex()
.flex_wrap()
.gap_1()
.children(context.into_iter().map(|context| {
let context_id = context.id();
ContextPill::added(AddedContext::new(context, cx), false, false, None)
let message_is_empty = message.should_display_content();
let has_content = !message_is_empty || !context.is_empty();
let message_content =
has_content.then(|| {
v_flex()
.gap_1p5()
.when(!message_is_empty, |parent| {
parent.child(
if let Some(edit_message_editor) = edit_message_editor.clone() {
div()
.key_context("EditMessageEditor")
.on_action(cx.listener(Self::cancel_editing_message))
.on_action(cx.listener(Self::confirm_editing_message))
.min_h_6()
.child(edit_message_editor)
.into_any()
} else {
div()
.min_h_6()
.text_ui(cx)
.child(self.render_message_content(
message_id,
rendered_message,
has_tool_uses,
cx,
))
.into_any()
},
)
})
.when(!context.is_empty(), |parent| {
parent.child(h_flex().flex_wrap().gap_1().children(
context.into_iter().map(|context| {
let context_id = context.id();
ContextPill::added(
AddedContext::new(context, cx),
false,
false,
None,
)
.on_click(Rc::new(cx.listener({
let workspace = workspace.clone();
let context_store = context_store.clone();
@@ -1179,8 +1156,9 @@ impl ActiveThread {
}
}
})))
})),
)
}),
))
})
});
let styled_message = match message.role {
@@ -1229,10 +1207,6 @@ impl ActiveThread {
)
.child(
h_flex()
// DL: To double-check whether we want to fully remove
// the editing feature from meassages. Checkpoint sort of
// solve the same problem.
.invisible()
.gap_1()
.when_some(
edit_message_editor.clone(),
@@ -1299,7 +1273,7 @@ impl ActiveThread {
),
),
)
.child(div().p_2().child(message_content)),
.child(div().p_2().children(message_content)),
),
Role::Assistant => v_flex()
.id(("message-container", ix))
@@ -1308,7 +1282,9 @@ impl ActiveThread {
.pr_4()
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.child(message_content)
.children(message_content)
.gap_2p5()
.pb_2p5()
.when(!tool_uses.is_empty(), |parent| {
parent.child(
v_flex().children(
@@ -1322,7 +1298,7 @@ impl ActiveThread {
v_flex()
.bg(colors.editor_background)
.rounded_sm()
.child(div().p_4().child(message_content)),
.child(div().p_4().children(message_content)),
),
};
@@ -1404,51 +1380,76 @@ impl ActiveThread {
.when(
show_feedback && !self.thread.read(cx).is_generating(),
|parent| {
parent
.child(feedback_items)
.when(self.showing_feedback_comments, |parent| {
parent.child(feedback_items).when_some(
self.feedback_message_editor.clone(),
|parent, feedback_editor| {
let focus_handle = feedback_editor.focus_handle(cx);
parent.child(
v_flex()
.gap_1()
.px_4()
.child(
Label::new(
"Please share your feedback to help us improve:",
)
.size(LabelSize::Small),
)
.child(
div()
.p_2()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(
self.feedback_comments_editor
.as_ref()
.unwrap()
.clone(),
),
)
.key_context("AgentFeedbackMessageEditor")
.on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
this.feedback_message_editor = None;
cx.notify();
}))
.on_action(cx.listener(|this, _: &menu::Confirm, _, cx| {
this.submit_feedback_message(cx);
cx.notify();
}))
.on_action(cx.listener(Self::confirm_editing_message))
.mx_4()
.mb_3()
.p_2()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(feedback_editor)
.child(
h_flex()
.gap_1()
.justify_end()
.pb_2()
.child(
Button::new("cancel-comments", "Cancel").on_click(
cx.listener(Self::handle_cancel_comments),
),
Button::new("dismiss-feedback-message", "Cancel")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(|this, _, _, cx| {
this.feedback_message_editor = None;
cx.notify();
})),
)
.child(
Button::new("submit-comments", "Submit").on_click(
cx.listener(Self::handle_submit_comments),
),
Button::new(
"submit-feedback-message",
"Share Feedback",
)
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(|this, _, _, cx| {
this.submit_feedback_message(cx);
cx.notify();
})),
),
),
)
})
},
)
},
)
.into_any()
@@ -1462,7 +1463,8 @@ impl ActiveThread {
cx: &Context<Self>,
) -> impl IntoElement {
let is_last_message = self.messages.last() == Some(&message_id);
let pending_thinking_segment_index = if is_last_message && !has_tool_uses {
let is_generating = self.thread.read(cx).is_generating();
let pending_thinking_segment_index = if is_generating && is_last_message && !has_tool_uses {
rendered_message
.segments
.iter()
@@ -1828,7 +1830,7 @@ impl ActiveThread {
div().map(|element| {
if !tool_use.needs_confirmation {
element.py_2p5().child(
element.child(
v_flex()
.child(
h_flex()
@@ -1900,145 +1902,164 @@ impl ActiveThread {
}),
)
} else {
element.py_2().child(
v_flex()
.rounded_lg()
.border_1()
.border_color(self.tool_card_border_color(cx))
.overflow_hidden()
.child(
h_flex()
.group("disclosure-header")
.relative()
.justify_between()
.py_1()
.map(|element| {
if is_status_finished {
element.pl_2().pr_0p5()
} else {
element.px_2()
}
})
.bg(self.tool_card_header_bg(cx))
.map(|element| {
if is_open {
element.border_b_1().rounded_t_md()
} else if needs_confirmation {
element.rounded_t_md()
} else {
element.rounded_md()
}
})
.border_color(self.tool_card_border_color(cx))
.child(
h_flex()
.id("tool-label-container")
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(
Icon::new(tool_use.icon)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
h_flex().pr_8().text_ui_sm(cx).children(
self.rendered_tool_use_labels
.get(&tool_use.id)
.cloned(),
),
),
)
.child(
h_flex()
.gap_1()
.child(
div().visible_on_hover("disclosure-header").child(
Disclosure::new("tool-use-disclosure", is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener({
let tool_use_id = tool_use.id.clone();
move |this, _event, _window, _cx| {
let is_open = this
.expanded_tool_uses
.entry(tool_use_id.clone())
.or_insert(false);
*is_open = !*is_open;
}
})),
),
)
.child(status_icons),
)
.child(gradient_overlay(self.tool_card_header_bg(cx))),
)
.map(|parent| {
if !is_open {
return parent;
}
parent.child(
v_flex()
.bg(cx.theme().colors().editor_background)
.map(|element| {
if needs_confirmation {
element.rounded_none()
} else {
element.rounded_b_lg()
}
})
.child(results_content),
)
})
.when(needs_confirmation, |this| {
this.child(
v_flex()
.rounded_lg()
.border_1()
.border_color(self.tool_card_border_color(cx))
.overflow_hidden()
.child(
h_flex()
.group("disclosure-header")
.relative()
.justify_between()
.py_1()
.map(|element| {
if is_status_finished {
element.pl_2().pr_0p5()
} else {
element.px_2()
}
})
.bg(self.tool_card_header_bg(cx))
.map(|element| {
if is_open {
element.border_b_1().rounded_t_md()
} else if needs_confirmation {
element.rounded_t_md()
} else {
element.rounded_md()
}
})
.border_color(self.tool_card_border_color(cx))
.child(
h_flex()
.py_1()
.pl_2()
.pr_1()
.gap_1()
.justify_between()
.bg(cx.theme().colors().editor_background)
.border_t_1()
.border_color(self.tool_card_border_color(cx))
.rounded_b_lg()
.child(Label::new("Action Confirmation").color(Color::Muted).size(LabelSize::Small))
.id("tool-label-container")
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(
h_flex()
.gap_0p5()
.child({
let tool_id = tool_use.id.clone();
Button::new(
"always-allow-tool-action",
"Always Allow",
Icon::new(tool_use.icon)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
h_flex().pr_8().text_ui_sm(cx).children(
self.rendered_tool_use_labels
.get(&tool_use.id)
.cloned(),
),
),
)
.child(
h_flex()
.gap_1()
.child(
div().visible_on_hover("disclosure-header").child(
Disclosure::new("tool-use-disclosure", is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener({
let tool_use_id = tool_use.id.clone();
move |this, _event, _window, _cx| {
let is_open = this
.expanded_tool_uses
.entry(tool_use_id.clone())
.or_insert(false);
*is_open = !*is_open;
}
})),
),
)
.child(status_icons),
)
.child(gradient_overlay(self.tool_card_header_bg(cx))),
)
.map(|parent| {
if !is_open {
return parent;
}
parent.child(
v_flex()
.bg(cx.theme().colors().editor_background)
.map(|element| {
if needs_confirmation {
element.rounded_none()
} else {
element.rounded_b_lg()
}
})
.child(results_content),
)
})
.when(needs_confirmation, |this| {
this.child(
h_flex()
.py_1()
.pl_2()
.pr_1()
.gap_1()
.justify_between()
.bg(cx.theme().colors().editor_background)
.border_t_1()
.border_color(self.tool_card_border_color(cx))
.rounded_b_lg()
.child(Label::new("Action Confirmation").color(Color::Muted).size(LabelSize::Small))
.child(
h_flex()
.gap_0p5()
.child({
let tool_id = tool_use.id.clone();
Button::new(
"always-allow-tool-action",
"Always Allow",
)
.label_size(LabelSize::Small)
.icon(IconName::CheckDouble)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Success)
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Never ask for permission",
None,
"Restore the original behavior in your Agent Panel settings",
window,
cx,
)
.label_size(LabelSize::Small)
.icon(IconName::CheckDouble)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Success)
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Never ask for permission",
None,
"Restore the original behavior in your Agent Panel settings",
})
.on_click(cx.listener(
move |this, event, window, cx| {
if let Some(fs) = fs.clone() {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
|settings, _| {
settings.set_always_allow_tool_actions(true);
},
);
}
this.handle_allow_tool(
tool_id.clone(),
event,
window,
cx,
)
})
},
))
})
.child(ui::Divider::vertical())
.child({
let tool_id = tool_use.id.clone();
Button::new("allow-tool-action", "Allow")
.label_size(LabelSize::Small)
.icon(IconName::Check)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Success)
.on_click(cx.listener(
move |this, event, window, cx| {
if let Some(fs) = fs.clone() {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
|settings, _| {
settings.set_always_allow_tool_actions(true);
},
);
}
this.handle_allow_tool(
tool_id.clone(),
event,
@@ -2047,52 +2068,31 @@ impl ActiveThread {
)
},
))
})
.child(ui::Divider::vertical())
.child({
let tool_id = tool_use.id.clone();
Button::new("allow-tool-action", "Allow")
.label_size(LabelSize::Small)
.icon(IconName::Check)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Success)
.on_click(cx.listener(
move |this, event, window, cx| {
this.handle_allow_tool(
tool_id.clone(),
event,
window,
cx,
)
},
))
})
.child({
let tool_id = tool_use.id.clone();
let tool_name: Arc<str> = tool_use.name.into();
Button::new("deny-tool", "Deny")
.label_size(LabelSize::Small)
.icon(IconName::Close)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Error)
.on_click(cx.listener(
move |this, event, window, cx| {
this.handle_deny_tool(
tool_id.clone(),
tool_name.clone(),
event,
window,
cx,
)
},
))
}),
),
)
}),
)
})
.child({
let tool_id = tool_use.id.clone();
let tool_name: Arc<str> = tool_use.name.into();
Button::new("deny-tool", "Deny")
.label_size(LabelSize::Small)
.icon(IconName::Close)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Error)
.on_click(cx.listener(
move |this, event, window, cx| {
this.handle_deny_tool(
tool_id.clone(),
tool_name.clone(),
event,
window,
cx,
)
},
))
}),
),
)
})
}
})
}

View File

@@ -1,4 +1,4 @@
use crate::{Thread, ThreadEvent};
use crate::{Keep, Reject, Thread, ThreadEvent};
use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::HashSet;
@@ -26,6 +26,7 @@ use workspace::{
item::{BreadcrumbText, ItemEvent, TabContentParams},
searchable::SearchableItemHandle,
};
use zed_actions::assistant::ToggleFocus;
pub struct AgentDiff {
multibuffer: Entity<MultiBuffer>,
@@ -553,11 +554,12 @@ impl Item for AgentDiff {
}
impl Render for AgentDiff {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_empty = self.multibuffer.read(cx).is_empty();
let focus_handle = &self.focus_handle;
div()
.track_focus(&self.focus_handle)
.track_focus(focus_handle)
.key_context(if is_empty { "EmptyPane" } else { "AgentDiff" })
.on_action(cx.listener(Self::keep))
.on_action(cx.listener(Self::reject))
@@ -568,7 +570,32 @@ impl Render for AgentDiff {
.items_center()
.justify_center()
.size_full()
.when(is_empty, |el| el.child("No changes to review"))
.when(is_empty, |el| {
el.child(
v_flex()
.items_center()
.gap_2()
.child("No changes to review")
.child(
Button::new("continue-iterating", "Continue Iterating")
.style(ButtonStyle::Filled)
.icon(IconName::ForwardArrow)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&ToggleFocus,
&focus_handle.clone(),
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(ToggleFocus.boxed_clone(), cx)
}),
),
)
})
.when(!is_empty, |el| el.child(self.editor.clone()))
}
}
@@ -604,7 +631,7 @@ fn render_diff_hunk_controls(
.disabled(is_created_file)
.key_binding(
KeyBinding::for_action_in(
&crate::Reject,
&Reject,
&editor.read(cx).focus_handle(cx),
window,
cx,
@@ -625,13 +652,8 @@ fn render_diff_hunk_controls(
}),
Button::new(("keep", row as u64), "Keep")
.key_binding(
KeyBinding::for_action_in(
&crate::Keep,
&editor.read(cx).focus_handle(cx),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
KeyBinding::for_action_in(&Keep, &editor.read(cx).focus_handle(cx), window, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click({
let agent_diff = agent_diff.clone();

View File

@@ -1,5 +1,6 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Result, anyhow};
use assistant_context_editor::{
@@ -14,9 +15,9 @@ use client::zed_urls;
use editor::{Editor, MultiBuffer};
use fs::Fs;
use gpui::{
Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle,
Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity,
action_with_deprecated_aliases, prelude::*,
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task,
UpdateGlobal, WeakEntity, action_with_deprecated_aliases, prelude::*, pulsating_between,
};
use language::LanguageRegistry;
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
@@ -38,7 +39,7 @@ use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
use crate::history_store::{HistoryEntry, HistoryStore};
use crate::message_editor::MessageEditor;
use crate::thread::{Thread, ThreadError, ThreadId};
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{
@@ -715,18 +716,21 @@ impl Panel for AssistantPanel {
impl AssistantPanel {
fn render_toolbar(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let thread = self.thread.read(cx);
let is_empty = thread.is_empty();
let active_thread = self.thread.read(cx);
let thread = active_thread.thread().read(cx);
let token_usage = thread.total_token_usage(cx);
let thread_id = thread.id().clone();
let thread_id = thread.thread().read(cx).id().clone();
let is_generating = thread.is_generating();
let is_empty = active_thread.is_empty();
let focus_handle = self.focus_handle(cx);
let title = match self.active_view {
ActiveView::Thread => {
if is_empty {
thread.summary_or_default(cx)
active_thread.summary_or_default(cx)
} else {
thread
active_thread
.summary(cx)
.unwrap_or_else(|| SharedString::from("Loading Summary…"))
}
@@ -742,6 +746,12 @@ impl AssistantPanel {
ActiveView::Configuration => "Settings".into(),
};
let show_token_count = match self.active_view {
ActiveView::Thread => !is_empty,
ActiveView::PromptEditor => self.context_editor.is_some(),
_ => false,
};
h_flex()
.id("assistant-toolbar")
.h(Tab::container_height(cx))
@@ -764,12 +774,67 @@ impl AssistantPanel {
.pl_2()
.gap_2()
.bg(cx.theme().colors().tab_bar_background)
.children(if matches!(self.active_view, ActiveView::PromptEditor) {
self.context_editor
.as_ref()
.and_then(|editor| render_remaining_tokens(editor, cx))
} else {
None
.when(show_token_count, |parent| match self.active_view {
ActiveView::Thread => {
if token_usage.total == 0 {
return parent;
}
let token_color = match token_usage.ratio {
TokenUsageRatio::Normal => Color::Muted,
TokenUsageRatio::Warning => Color::Warning,
TokenUsageRatio::Exceeded => Color::Error,
};
parent.child(
h_flex()
.gap_0p5()
.child(
Label::new(assistant_context_editor::humanize_token_count(
token_usage.total,
))
.size(LabelSize::Small)
.color(token_color)
.map(|label| {
if is_generating {
label
.with_animation(
"used-tokens-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(
0.6, 1.,
)),
|label, delta| label.alpha(delta),
)
.into_any()
} else {
label.into_any_element()
}
}),
)
.child(
Label::new("/").size(LabelSize::Small).color(Color::Muted),
)
.child(
Label::new(assistant_context_editor::humanize_token_count(
token_usage.max,
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
}
ActiveView::PromptEditor => {
let Some(editor) = self.context_editor.as_ref() else {
return parent;
};
let Some(element) = render_remaining_tokens(editor, cx) else {
return parent;
};
parent.child(element)
}
_ => parent,
})
.child(
h_flex()

View File

@@ -76,7 +76,7 @@ impl ContextPickerMode {
Self::File => "Files & Directories",
Self::Symbol => "Symbols",
Self::Fetch => "Fetch",
Self::Thread => "Thread",
Self::Thread => "Threads",
}
}
@@ -360,73 +360,15 @@ impl ContextPicker {
}
fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
let Some(workspace) = self.workspace.upgrade().map(|w| w.read(cx)) else {
let Some(workspace) = self.workspace.upgrade() else {
return vec![];
};
let Some(context_store) = self.context_store.upgrade().map(|cs| cs.read(cx)) else {
let Some(context_store) = self.context_store.upgrade() else {
return vec![];
};
let mut recent = Vec::with_capacity(6);
let mut current_files = context_store.file_paths(cx);
if let Some(active_path) = active_singleton_buffer_path(&workspace, cx) {
current_files.insert(active_path);
}
let project = workspace.project().read(cx);
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
.take(4)
.filter_map(|(project_path, _)| {
project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| RecentEntry::File {
project_path,
path_prefix: worktree.read(cx).root_name().into(),
})
}),
);
let mut current_threads = context_store.thread_ids();
if let Some(active_thread) = workspace
.panel::<AssistantPanel>(cx)
.map(|panel| panel.read(cx).active_thread(cx))
{
current_threads.insert(active_thread.read(cx).id().clone());
}
let Some(thread_store) = self
.thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
else {
return recent;
};
thread_store.update(cx, |thread_store, _cx| {
recent.extend(
thread_store
.threads()
.into_iter()
.filter(|thread| !current_threads.contains(&thread.id))
.take(2)
.map(|thread| {
RecentEntry::Thread(ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
}),
)
});
recent
recent_context_picker_entries(context_store, self.thread_store.clone(), workspace, cx)
}
}
@@ -480,16 +422,6 @@ fn supported_context_picker_modes(
modes
}
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
let active_item = workspace.active_item(cx)?;
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let buffer = editor.buffer().read(cx).as_singleton()?;
let path = buffer.read(cx).file()?.path().to_path_buf();
Some(path)
}
fn recent_context_picker_entries(
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
@@ -498,14 +430,8 @@ fn recent_context_picker_entries(
) -> Vec<RecentEntry> {
let mut recent = Vec::with_capacity(6);
let mut current_files = context_store.read(cx).file_paths(cx);
let current_files = context_store.read(cx).file_paths(cx);
let workspace = workspace.read(cx);
if let Some(active_path) = active_singleton_buffer_path(workspace, cx) {
current_files.insert(active_path);
}
let project = workspace.project().read(cx);
recent.extend(

View File

@@ -890,10 +890,10 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
"editor dir/",
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
"four.txt dir/a/",
"Files & Directories",
"Symbols",
"Fetch"
@@ -988,14 +988,14 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](file:dir/a/one.txt) Ipsum [@seven.txt](file:dir/b/seven.txt)"
"Lorem [@one.txt](file:dir/a/one.txt) Ipsum [@editor](file:dir/editor)"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 36),
Point::new(0, 43)..Point::new(0, 77)
Point::new(0, 43)..Point::new(0, 69)
]
);
});
@@ -1005,14 +1005,14 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](file:dir/a/one.txt) Ipsum [@seven.txt](file:dir/b/seven.txt)\n@"
"Lorem [@one.txt](file:dir/a/one.txt) Ipsum [@editor](file:dir/editor)\n@"
);
assert!(editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 36),
Point::new(0, 43)..Point::new(0, 77)
Point::new(0, 43)..Point::new(0, 69)
]
);
});
@@ -1026,15 +1026,15 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](file:dir/a/one.txt) Ipsum [@seven.txt](file:dir/b/seven.txt)\n[@six.txt](file:dir/b/six.txt)"
"Lorem [@one.txt](file:dir/a/one.txt) Ipsum [@editor](file:dir/editor)\n[@seven.txt](file:dir/b/seven.txt)"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 36),
Point::new(0, 43)..Point::new(0, 77),
Point::new(1, 0)..Point::new(1, 30)
Point::new(0, 43)..Point::new(0, 69),
Point::new(1, 0)..Point::new(1, 34)
]
);
});

View File

@@ -6,7 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
use collections::{BTreeMap, HashMap, HashSet};
use futures::future::join_all;
use futures::{self, Future, FutureExt, future};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, SharedString, Task, WeakEntity};
use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
use language::{Buffer, File};
use project::{ProjectItem, ProjectPath, Worktree};
use rope::Rope;
@@ -95,8 +95,8 @@ impl ContextStore {
project.open_buffer(project_path.clone(), cx)
})?;
let buffer_entity = open_buffer_task.await?;
let buffer_id = this.update(cx, |_, cx| buffer_entity.read(cx).remote_id())?;
let buffer = open_buffer_task.await?;
let buffer_id = this.update(cx, |_, cx| buffer.read(cx).remote_id())?;
let already_included = this.update(cx, |this, _cx| {
match this.will_include_buffer(buffer_id, &project_path.path) {
@@ -115,16 +115,8 @@ impl ContextStore {
return anyhow::Ok(());
}
let (buffer_info, text_task) = this.update(cx, |_, cx| {
let buffer = buffer_entity.read(cx);
collect_buffer_info_and_text(
project_path.path.clone(),
buffer_entity,
buffer,
None,
cx.to_async(),
)
})??;
let (buffer_info, text_task) =
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
let text = text_task.await;
@@ -138,23 +130,12 @@ impl ContextStore {
pub fn add_file_from_buffer(
&mut self,
buffer_entity: Entity<Buffer>,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
let (buffer_info, text_task) = this.update(cx, |_, cx| {
let buffer = buffer_entity.read(cx);
let Some(file) = buffer.file() else {
return Err(anyhow!("Buffer has no path."));
};
collect_buffer_info_and_text(
file.path().clone(),
buffer_entity,
buffer,
None,
cx.to_async(),
)
})??;
let (buffer_info, text_task) =
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
let text = text_task.await;
@@ -169,10 +150,8 @@ impl ContextStore {
fn insert_file(&mut self, context_buffer: ContextBuffer) {
let id = self.next_context_id.post_inc();
self.files.insert(context_buffer.id, id);
self.context.push(AssistantContext::File(FileContext {
id,
context_buffer: context_buffer,
}));
self.context
.push(AssistantContext::File(FileContext { id, context_buffer }));
}
pub fn add_directory(
@@ -233,22 +212,13 @@ impl ContextStore {
let mut buffer_infos = Vec::new();
let mut text_tasks = Vec::new();
this.update(cx, |_, cx| {
for (path, buffer_entity) in files.into_iter().zip(buffers) {
// Skip all binary files and other non-UTF8 files
if let Ok(buffer_entity) = buffer_entity {
let buffer = buffer_entity.read(cx);
if let Some((buffer_info, text_task)) = collect_buffer_info_and_text(
path,
buffer_entity,
buffer,
None,
cx.to_async(),
)
.log_err()
{
buffer_infos.push(buffer_info);
text_tasks.push(text_task);
}
// Skip all binary files and other non-UTF8 files
for buffer in buffers.into_iter().flatten() {
if let Some((buffer_info, text_task)) =
collect_buffer_info_and_text(buffer, None, cx).log_err()
{
buffer_infos.push(buffer_info);
text_tasks.push(text_task);
}
}
anyhow::Ok(())
@@ -298,12 +268,8 @@ impl ContextStore {
cx: &mut Context<Self>,
) -> Task<Result<bool>> {
let buffer_ref = buffer.read(cx);
let Some(file) = buffer_ref.file() else {
return Task::ready(Err(anyhow!("Buffer has no path.")));
};
let Some(project_path) = buffer_ref.project_path(cx) else {
return Task::ready(Err(anyhow!("Buffer has no project path.")));
return Task::ready(Err(anyhow!("buffer has no path")));
};
if let Some(symbols_for_path) = self.symbols_by_path.get(&project_path) {
@@ -326,16 +292,11 @@ impl ContextStore {
}
}
let (buffer_info, collect_content_task) = match collect_buffer_info_and_text(
file.path().clone(),
buffer,
buffer_ref,
Some(symbol_enclosing_range.clone()),
cx.to_async(),
) {
Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
Err(err) => return Task::ready(Err(err)),
};
let (buffer_info, collect_content_task) =
match collect_buffer_info_and_text(buffer, Some(symbol_enclosing_range.clone()), cx) {
Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
Err(err) => return Task::ready(Err(err)),
};
cx.spawn(async move |this, cx| {
let content = collect_content_task.await;
@@ -616,16 +577,16 @@ pub enum FileInclusion {
// ContextBuffer without text.
struct BufferInfo {
buffer_entity: Entity<Buffer>,
file: Arc<dyn File>,
id: BufferId,
buffer: Entity<Buffer>,
file: Arc<dyn File>,
version: clock::Global,
}
fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer {
ContextBuffer {
id: info.id,
buffer: info.buffer_entity,
buffer: info.buffer,
file: info.file,
version: info.version,
text,
@@ -644,34 +605,37 @@ fn make_context_symbol(
id: ContextSymbolId { name, range, path },
buffer_version: info.version,
enclosing_range,
buffer: info.buffer_entity,
buffer: info.buffer,
text,
}
}
fn collect_buffer_info_and_text(
path: Arc<Path>,
buffer_entity: Entity<Buffer>,
buffer: &Buffer,
buffer: Entity<Buffer>,
range: Option<Range<Anchor>>,
cx: AsyncApp,
cx: &App,
) -> Result<(BufferInfo, Task<SharedString>)> {
let buffer_info = BufferInfo {
id: buffer.remote_id(),
buffer_entity,
file: buffer
.file()
.context("buffer context must have a file")?
.clone(),
version: buffer.version(),
};
let buffer_ref = buffer.read(cx);
let file = buffer_ref.file().context("file context must have a path")?;
// Important to collect version at the same time as content so that staleness logic is correct.
let version = buffer_ref.version();
let content = if let Some(range) = range {
buffer.text_for_range(range).collect::<Rope>()
buffer_ref.text_for_range(range).collect::<Rope>()
} else {
buffer.as_rope().clone()
buffer_ref.as_rope().clone()
};
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&path, content) });
let buffer_info = BufferInfo {
buffer,
id: buffer_ref.remote_id(),
file: file.clone(),
version,
};
let full_path = file.full_path(cx);
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&full_path, content) });
Ok((buffer_info, text_task))
}
@@ -920,16 +884,9 @@ fn refresh_context_buffer(
cx: &App,
) -> Option<impl Future<Output = ContextBuffer> + use<>> {
let buffer = context_buffer.buffer.read(cx);
let path = buffer_path_log_err(buffer, cx)?;
if buffer.version.changed_since(&context_buffer.version) {
let (buffer_info, text_task) = collect_buffer_info_and_text(
path,
context_buffer.buffer.clone(),
buffer,
None,
cx.to_async(),
)
.log_err()?;
let (buffer_info, text_task) =
collect_buffer_info_and_text(context_buffer.buffer.clone(), None, cx).log_err()?;
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
} else {
None
@@ -941,15 +898,12 @@ fn refresh_context_symbol(
cx: &App,
) -> Option<impl Future<Output = ContextSymbol> + use<>> {
let buffer = context_symbol.buffer.read(cx);
let path = buffer_path_log_err(buffer, cx)?;
let project_path = buffer.project_path(cx)?;
if buffer.version.changed_since(&context_symbol.buffer_version) {
let (buffer_info, text_task) = collect_buffer_info_and_text(
path,
context_symbol.buffer.clone(),
buffer,
Some(context_symbol.enclosing_range.clone()),
cx.to_async(),
cx,
)
.log_err()?;
let name = context_symbol.id.name.clone();

View File

@@ -28,7 +28,7 @@ use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerComplet
use crate::context_store::{ContextStore, refresh_context_store_text};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::thread::{RequestKind, Thread};
use crate::thread::{RequestKind, Thread, TokenUsageRatio};
use crate::thread_store::ThreadStore;
use crate::{
AgentDiff, Chat, ChatMode, NewThread, OpenAgentDiff, RemoveAllContext, ThreadEvent,
@@ -338,9 +338,12 @@ impl Render for MessageEditor {
let thread = self.thread.read(cx);
let is_generating = thread.is_generating();
let is_too_long = thread.is_getting_too_long(cx);
let total_token_usage = thread.total_token_usage(cx);
let is_model_selected = self.is_model_selected(cx);
let is_editor_empty = self.is_editor_empty(cx);
let needs_confirmation =
thread.has_pending_tool_uses() && thread.tools_needing_confirmation().next().is_some();
let submit_label_color = if is_editor_empty {
Color::Muted
} else {
@@ -432,11 +435,17 @@ impl Render for MessageEditor {
},
),
)
.child(
Label::new("Generating…")
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child({
Label::new(if needs_confirmation {
"Waiting for confirmation…"
} else {
"Generating…"
})
.size(LabelSize::XSmall)
.color(Color::Muted)
})
.child(ui::Divider::vertical())
.child(
Button::new("cancel-generation", "Cancel")
@@ -779,7 +788,7 @@ impl Render for MessageEditor {
),
)
)
.when(is_too_long, |parent| {
.when(total_token_usage.ratio != TokenUsageRatio::Normal, |parent| {
parent.child(
h_flex()
.p_2()

View File

@@ -35,7 +35,7 @@ use crate::thread_store::{
SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult,
SerializedToolUse,
};
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState};
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState, USING_TOOL_MARKER};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
@@ -85,6 +85,12 @@ pub struct Message {
}
impl Message {
/// Returns whether the message contains any meaningful text that should be displayed
/// The model sometimes runs tool without producing any text or just a marker ([`USING_TOOL_MARKER`])
pub fn should_display_content(&self) -> bool {
self.segments.iter().all(|segment| segment.should_display())
}
pub fn push_thinking(&mut self, text: &str) {
if let Some(MessageSegment::Thinking(segment)) = self.segments.last_mut() {
segment.push_str(text);
@@ -131,6 +137,16 @@ impl MessageSegment {
Self::Thinking(text) => text,
}
}
pub fn should_display(&self) -> bool {
// We add USING_TOOL_MARKER when making a request that includes tool uses
// without non-whitespace text around them, and this can cause the model
// to mimic the pattern, so we consider those segments not displayable.
match self {
Self::Text(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER,
Self::Thinking(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -198,6 +214,21 @@ pub enum DetailedSummaryState {
},
}
#[derive(Default)]
pub struct TotalTokenUsage {
pub total: usize,
pub max: usize,
pub ratio: TokenUsageRatio,
}
#[derive(Default, PartialEq, Eq)]
pub enum TokenUsageRatio {
#[default]
Normal,
Warning,
Exceeded,
}
/// A thread of conversation with the LLM.
pub struct Thread {
id: ThreadId,
@@ -1083,32 +1114,15 @@ impl Thread {
}
}
LanguageModelCompletionEvent::ToolUse(tool_use) => {
let last_assistant_message = thread
let last_assistant_message_id = thread
.messages
.iter_mut()
.rfind(|message| message.role == Role::Assistant);
.rfind(|message| message.role == Role::Assistant)
.map(|message| message.id)
.unwrap_or_else(|| {
thread.insert_message(Role::Assistant, vec![], cx)
});
let last_assistant_message_id =
if let Some(message) = last_assistant_message {
if let Some(segment) = message.segments.first_mut() {
let text = segment.text_mut();
if text.is_empty() {
text.push_str("Using tool...");
}
} else {
message.segments.push(MessageSegment::Text(
"Using tool...".to_string(),
));
}
message.id
} else {
thread.insert_message(
Role::Assistant,
vec![MessageSegment::Text("Using tool...".to_string())],
cx,
)
};
thread.tool_use.request_tool_use(
last_assistant_message_id,
tool_use,
@@ -1724,26 +1738,33 @@ impl Thread {
self.cumulative_token_usage.clone()
}
pub fn is_getting_too_long(&self, cx: &App) -> bool {
pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(model) = model_registry.active_model() else {
return false;
return TotalTokenUsage::default();
};
let max_tokens = model.max_token_count();
let current_usage =
self.cumulative_token_usage.input_tokens + self.cumulative_token_usage.output_tokens;
let max = model.max_token_count();
#[cfg(debug_assertions)]
let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
.unwrap_or("0.9".to_string())
.unwrap_or("0.8".to_string())
.parse()
.unwrap();
#[cfg(not(debug_assertions))]
let warning_threshold: f32 = 0.9;
let warning_threshold: f32 = 0.8;
current_usage as f32 >= (max_tokens as f32 * warning_threshold)
let total = self.cumulative_token_usage.total_tokens() as usize;
let ratio = if total >= max {
TokenUsageRatio::Exceeded
} else if total as f32 / max as f32 >= warning_threshold {
TokenUsageRatio::Warning
} else {
TokenUsageRatio::Normal
};
TotalTokenUsage { total, max, ratio }
}
pub fn deny_tool_use(

View File

@@ -43,6 +43,8 @@ pub struct ToolUseState {
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
}
pub const USING_TOOL_MARKER: &str = "<using_tool>";
impl ToolUseState {
pub fn new(tools: Arc<ToolWorkingSet>) -> Self {
Self {
@@ -357,8 +359,28 @@ impl ToolUseState {
request_message: &mut LanguageModelRequestMessage,
) {
if let Some(tool_uses) = self.tool_uses_by_assistant_message.get(&message_id) {
let mut found_tool_use = false;
for tool_use in tool_uses {
if self.tool_results.contains_key(&tool_use.id) {
if !found_tool_use {
// The API fails if a message contains a tool use without any (non-whitespace) text around it
match request_message.content.last_mut() {
Some(MessageContent::Text(txt)) => {
if txt.is_empty() {
txt.push_str(USING_TOOL_MARKER);
}
}
None | Some(_) => {
request_message
.content
.push(MessageContent::Text(USING_TOOL_MARKER.into()));
}
};
}
found_tool_use = true;
// Do not send tool uses until they are completed
request_message
.content

View File

@@ -26,3 +26,4 @@ serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -19,3 +19,4 @@ smol.workspace = true
tempfile.workspace = true
util.workspace = true
which.workspace = true
workspace-hack.workspace = true

View File

@@ -15,3 +15,4 @@ workspace = true
anyhow.workspace = true
gpui.workspace = true
rust-embed.workspace = true
workspace-hack.workspace = true

View File

@@ -69,6 +69,7 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -54,6 +54,7 @@ ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
language_model = { workspace = true, features = ["test-support"] }

View File

@@ -3703,6 +3703,18 @@ pub fn humanize_token_count(count: usize) -> String {
format!("{}.{}k", thousands, hundreds)
}
}
1_000_000..=9_999_999 => {
let millions = count / 1_000_000;
let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
if hundred_thousands == 0 {
format!("{}M", millions)
} else if hundred_thousands == 10 {
format!("{}M", millions + 1)
} else {
format!("{}.{}M", millions, hundred_thousands)
}
}
10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
_ => format!("{}k", (count + 500) / 1000),
}
}

View File

@@ -43,3 +43,4 @@ serde_json_lenient.workspace = true
settings.workspace = true
smol.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -26,6 +26,7 @@ deepseek = { workspace = true, features = ["schemars"] }
schemars.workspace = true
serde.workspace = true
settings.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
fs.workspace = true

View File

@@ -26,6 +26,7 @@ serde.workspace = true
serde_json.workspace = true
ui.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -42,6 +42,7 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
worktree.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
env_logger.workspace = true

View File

@@ -28,6 +28,7 @@ serde.workspace = true
serde_json.workspace = true
text.workspace = true
util.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }

View File

@@ -39,6 +39,7 @@ util.workspace = true
workspace.workspace = true
worktree.workspace = true
open = { workspace = true }
workspace-hack.workspace = true
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }

View File

@@ -20,3 +20,4 @@ gpui.workspace = true
parking_lot.workspace = true
rodio = { version = "0.20.0", default-features = false, features = ["wav"] }
util.workspace = true
workspace-hack.workspace = true

View File

@@ -29,3 +29,4 @@ smol.workspace = true
tempfile.workspace = true
which.workspace = true
workspace.workspace = true
workspace-hack.workspace = true

View File

@@ -25,3 +25,4 @@ serde_json.workspace = true
smol.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true

View File

@@ -20,3 +20,4 @@ aws-smithy-types.workspace = true
futures.workspace = true
http_client.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
workspace-hack.workspace = true

View File

@@ -9,7 +9,7 @@ use aws_smithy_runtime_api::client::http::{
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest as AwsHttpRequest, HttpResponse};
use aws_smithy_runtime_api::client::result::ConnectorError;
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
use aws_smithy_runtime_api::http::StatusCode;
use aws_smithy_runtime_api::http::{Headers, StatusCode};
use aws_smithy_types::body::SdkBody;
use futures::AsyncReadExt;
use http_client::{AsyncBody, Inner};
@@ -52,10 +52,17 @@ impl AwsConnector for AwsHttpConnector {
let (parts, body) = response.into_parts();
let body = convert_to_sdk_body(body, handle).await;
Ok(HttpResponse::new(
StatusCode::try_from(parts.status.as_u16()).unwrap(),
body,
))
let mut response =
HttpResponse::new(StatusCode::try_from(parts.status.as_u16()).unwrap(), body);
let headers = match Headers::try_from(parts.headers) {
Ok(headers) => headers,
Err(err) => return Err(ConnectorError::other(err.into(), None)),
};
*response.headers_mut() = headers;
Ok(response)
})
}
}

View File

@@ -26,3 +26,4 @@ serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
workspace-hack.workspace = true

View File

@@ -20,6 +20,7 @@ theme.workspace = true
ui.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -27,6 +27,7 @@ rope.workspace = true
sum_tree.workspace = true
text.workspace = true
util.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -42,6 +42,7 @@ telemetry.workspace = true
util.workspace = true
gpui_tokio.workspace = true
livekit_client.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@@ -32,6 +32,7 @@ sum_tree.workspace = true
text.workspace = true
time.workspace = true
util.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }

View File

@@ -31,6 +31,7 @@ release_channel.workspace = true
serde.workspace = true
util.workspace = true
tempfile.workspace = true
workspace-hack.workspace = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
exec.workspace = true

View File

@@ -51,6 +51,7 @@ url.workspace = true
util.workspace = true
worktree.workspace = true
telemetry.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
clock = { workspace = true, features = ["test-support"] }

View File

@@ -19,3 +19,4 @@ test-support = ["dep:parking_lot"]
parking_lot = { workspace = true, optional = true }
serde.workspace = true
smallvec.workspace = true
workspace-hack.workspace = true

View File

@@ -76,6 +76,7 @@ tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "registry", "tracing-log"] } # workaround for https://github.com/tokio-rs/tracing/issues/2927
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
assistant = { workspace = true, features = ["test-support"] }

View File

@@ -64,6 +64,7 @@ title_bar.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
call = { workspace = true, features = ["test-support"] }

View File

@@ -18,3 +18,4 @@ test-support = []
[dependencies]
indexmap.workspace = true
rustc-hash.workspace = true
workspace-hack.workspace = true

View File

@@ -31,6 +31,7 @@ util.workspace = true
telemetry.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -16,3 +16,4 @@ doctest = false
collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
workspace-hack.workspace = true

View File

@@ -17,6 +17,7 @@ gpui.workspace = true
linkme.workspace = true
parking_lot.workspace = true
theme.workspace = true
workspace-hack.workspace = true
[features]
default = []

View File

@@ -24,3 +24,4 @@ ui.workspace = true
workspace.workspace = true
notifications.workspace = true
collections.workspace = true
workspace-hack.workspace = true

View File

@@ -33,3 +33,4 @@ settings.workspace = true
smol.workspace = true
url = { workspace = true, features = ["serde"] }
util.workspace = true
workspace-hack.workspace = true

View File

@@ -19,3 +19,4 @@ schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
workspace-hack.workspace = true

View File

@@ -52,6 +52,7 @@ task.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[target.'cfg(windows)'.dependencies]
async-std = { version = "1.12.0", features = ["unstable"] }

View File

@@ -19,3 +19,4 @@ paths.workspace = true
release_channel.workspace = true
serde.workspace = true
serde_json.workspace = true
workspace-hack.workspace = true

View File

@@ -48,6 +48,7 @@ smallvec.workspace = true
smol.workspace = true
task.workspace = true
util.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
async-pipe.workspace = true

View File

@@ -32,6 +32,7 @@ serde.workspace = true
serde_json.workspace = true
task.workspace = true
util.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
dap = { workspace = true, features = ["test-support"] }

View File

@@ -26,6 +26,7 @@ smol.workspace = true
sqlez.workspace = true
sqlez_macros.workspace = true
util.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -24,3 +24,4 @@ settings.workspace = true
smol.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true

View File

@@ -50,6 +50,7 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
dap = { workspace = true, features = ["test-support"] }

View File

@@ -22,3 +22,4 @@ http_client.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
workspace-hack.workspace = true

View File

@@ -30,6 +30,7 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@@ -14,6 +14,7 @@ serde_json.workspace = true
settings.workspace = true
regex.workspace = true
util.workspace = true
workspace-hack.workspace = true
[lints]
workspace = true

View File

@@ -87,6 +87,7 @@ util.workspace = true
uuid.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -37,7 +37,7 @@ pub use block_map::{
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
pub use fold_map::{ChunkRenderer, ChunkRendererContext, Fold, FoldId, FoldPlaceholder, FoldPoint};
use fold_map::{FoldMap, FoldSnapshot};
use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
pub use inlay_map::Inlay;
@@ -45,8 +45,7 @@ use inlay_map::{InlayMap, InlaySnapshot};
pub use inlay_map::{InlayOffset, InlayPoint};
pub use invisibles::{is_invisible, replacement};
use language::{
ChunkRenderer, OffsetUtf16, Point, Subscription as BufferSubscription,
language_settings::language_settings,
OffsetUtf16, Point, Subscription as BufferSubscription, language_settings::language_settings,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
@@ -515,6 +514,33 @@ impl DisplayMap {
.update(cx, |map, cx| map.set_wrap_width(width, cx))
}
pub fn update_fold_widths(
&mut self,
widths: impl IntoIterator<Item = (FoldId, Pixels)>,
cx: &mut Context<Self>,
) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.update_fold_widths(widths);
let widths_changed = !edits.is_empty();
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
widths_changed
}
pub(crate) fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
self.inlay_map.current_inlays()
}

View File

@@ -1,11 +1,12 @@
use super::{
Highlights,
fold_map::Chunk,
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
};
use crate::{EditorStyle, GutterDimensions};
use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, App, EntityId, Pixels, Window};
use language::{Chunk, Patch, Point};
use language::{Patch, Point};
use multi_buffer::{
Anchor, ExcerptId, ExcerptInfo, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, RowInfo,
ToOffset, ToPoint as _,

View File

@@ -2,8 +2,9 @@ use super::{
Highlights,
inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
};
use gpui::{AnyElement, App, ElementId};
use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary};
use gpui::{AnyElement, App, ElementId, HighlightStyle, Pixels, Window};
use language::{Edit, HighlightId, Point, TextSummary};
use lsp::DiagnosticSeverity;
use multi_buffer::{
Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset,
};
@@ -14,7 +15,7 @@ use std::{
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
sync::Arc,
};
use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary};
use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary, TreeMap};
use ui::IntoElement as _;
use util::post_inc;
@@ -177,6 +178,13 @@ impl FoldMapWriter<'_> {
let mut new_tree = SumTree::new(buffer);
let mut cursor = self.0.snapshot.folds.cursor::<FoldRange>(buffer);
for fold in folds {
self.0.snapshot.fold_metadata_by_id.insert(
fold.id,
FoldMetadata {
range: fold.range.clone(),
width: None,
},
);
new_tree.append(cursor.slice(&fold.range, Bias::Right, buffer), buffer);
new_tree.push(fold, buffer);
}
@@ -240,6 +248,7 @@ impl FoldMapWriter<'_> {
});
}
fold_ixs_to_delete.push(*folds_cursor.start());
self.0.snapshot.fold_metadata_by_id.remove(&fold.id);
}
folds_cursor.next(buffer);
}
@@ -263,6 +272,42 @@ impl FoldMapWriter<'_> {
let edits = self.0.sync(snapshot.clone(), edits);
(self.0.snapshot.clone(), edits)
}
pub(crate) fn update_fold_widths(
&mut self,
new_widths: impl IntoIterator<Item = (FoldId, Pixels)>,
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let inlay_snapshot = self.0.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
for (id, new_width) in new_widths {
if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() {
if Some(new_width) != metadata.width {
let buffer_start = metadata.range.start.to_offset(buffer);
let buffer_end = metadata.range.end.to_offset(buffer);
let inlay_range = inlay_snapshot.to_inlay_offset(buffer_start)
..inlay_snapshot.to_inlay_offset(buffer_end);
edits.push(InlayEdit {
old: inlay_range.clone(),
new: inlay_range.clone(),
});
self.0.snapshot.fold_metadata_by_id.insert(
id,
FoldMetadata {
range: metadata.range,
width: Some(new_width),
},
);
}
}
}
let edits = consolidate_inlay_edits(edits);
let edits = self.0.sync(inlay_snapshot, edits);
(self.0.snapshot.clone(), edits)
}
}
/// Decides where the fold indicators should be; also tracks parts of a source file that are currently folded.
@@ -290,6 +335,7 @@ impl FoldMap {
),
inlay_snapshot: inlay_snapshot.clone(),
version: 0,
fold_metadata_by_id: TreeMap::default(),
},
next_fold_id: FoldId::default(),
};
@@ -481,6 +527,7 @@ impl FoldMap {
placeholder: Some(TransformPlaceholder {
text: ELLIPSIS,
renderer: ChunkRenderer {
id: fold.id,
render: Arc::new(move |cx| {
(fold.placeholder.render)(
fold_id,
@@ -489,6 +536,7 @@ impl FoldMap {
)
}),
constrain_width: fold.placeholder.constrain_width,
measured_width: self.snapshot.fold_width(&fold_id),
},
}),
},
@@ -573,6 +621,7 @@ impl FoldMap {
pub struct FoldSnapshot {
transforms: SumTree<Transform>,
folds: SumTree<Fold>,
fold_metadata_by_id: TreeMap<FoldId, FoldMetadata>,
pub inlay_snapshot: InlaySnapshot,
pub version: usize,
}
@@ -582,6 +631,10 @@ impl FoldSnapshot {
&self.inlay_snapshot.buffer
}
fn fold_width(&self, fold_id: &FoldId) -> Option<Pixels> {
self.fold_metadata_by_id.get(fold_id)?.width
}
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(FoldOffset(0)..self.len(), false, Highlights::default())
@@ -1006,7 +1059,7 @@ impl sum_tree::Summary for TransformSummary {
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Ord, PartialOrd, Hash)]
pub struct FoldId(usize);
impl From<FoldId> for ElementId {
@@ -1045,6 +1098,12 @@ impl Default for FoldRange {
}
}
#[derive(Clone, Debug)]
struct FoldMetadata {
range: FoldRange,
width: Option<Pixels>,
}
impl sum_tree::Item for Fold {
type Summary = FoldSummary;
@@ -1181,10 +1240,74 @@ impl Iterator for FoldRows<'_> {
}
}
/// A chunk of a buffer's text, along with its syntax highlight and
/// diagnostic status.
#[derive(Clone, Debug, Default)]
pub struct Chunk<'a> {
/// The text of the chunk.
pub text: &'a str,
/// The syntax highlighting style of the chunk.
pub syntax_highlight_id: Option<HighlightId>,
/// The highlight style that has been applied to this chunk in
/// the editor.
pub highlight_style: Option<HighlightStyle>,
/// The severity of diagnostic associated with this chunk, if any.
pub diagnostic_severity: Option<DiagnosticSeverity>,
/// Whether this chunk of text is marked as unnecessary.
pub is_unnecessary: bool,
/// Whether this chunk of text was originally a tab character.
pub is_tab: bool,
/// An optional recipe for how the chunk should be presented.
pub renderer: Option<ChunkRenderer>,
}
/// A recipe for how the chunk should be presented.
#[derive(Clone)]
pub struct ChunkRenderer {
/// The id of the fold associated with this chunk.
pub id: FoldId,
/// Creates a custom element to represent this chunk.
pub render: Arc<dyn Send + Sync + Fn(&mut ChunkRendererContext) -> AnyElement>,
/// If true, the element is constrained to the shaped width of the text.
pub constrain_width: bool,
/// The width of the element, as measured during the last layout pass.
///
/// This is None if the element has not been laid out yet.
pub measured_width: Option<Pixels>,
}
pub struct ChunkRendererContext<'a, 'b> {
pub window: &'a mut Window,
pub context: &'b mut App,
pub max_width: Pixels,
}
impl fmt::Debug for ChunkRenderer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("ChunkRenderer")
.field("constrain_width", &self.constrain_width)
.finish()
}
}
impl Deref for ChunkRendererContext<'_, '_> {
type Target = App;
fn deref(&self) -> &Self::Target {
self.context
}
}
impl DerefMut for ChunkRendererContext<'_, '_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.context
}
}
pub struct FoldChunks<'a> {
transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
inlay_chunks: InlayChunks<'a>,
inlay_chunk: Option<(InlayOffset, Chunk<'a>)>,
inlay_chunk: Option<(InlayOffset, language::Chunk<'a>)>,
inlay_offset: InlayOffset,
output_offset: FoldOffset,
max_output_offset: FoldOffset,
@@ -1292,7 +1415,15 @@ impl<'a> Iterator for FoldChunks<'a> {
self.inlay_offset = chunk_end;
self.output_offset.0 += chunk.text.len();
return Some(chunk);
return Some(Chunk {
text: chunk.text,
syntax_highlight_id: chunk.syntax_highlight_id,
highlight_style: chunk.highlight_style,
diagnostic_severity: chunk.diagnostic_severity,
is_unnecessary: chunk.is_unnecessary,
is_tab: chunk.is_tab,
renderer: None,
});
}
None

View File

@@ -1,8 +1,8 @@
use super::{
Highlights,
fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
fold_map::{self, Chunk, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
};
use language::{Chunk, Point};
use language::Point;
use multi_buffer::MultiBufferSnapshot;
use std::{cmp, mem, num::NonZeroU32, ops::Range};
use sum_tree::Bias;

View File

@@ -1,10 +1,10 @@
use super::{
Highlights,
fold_map::FoldRows,
fold_map::{Chunk, FoldRows},
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
};
use gpui::{App, AppContext as _, Context, Entity, Font, LineWrapper, Pixels, Task};
use language::{Chunk, Point};
use language::Point;
use multi_buffer::{MultiBufferSnapshot, RowInfo};
use smol::future::yield_now;
use std::sync::LazyLock;
@@ -454,6 +454,7 @@ impl WrapSnapshot {
}
let mut line = String::new();
let mut line_fragments = Vec::new();
let mut remaining = None;
let mut chunks = new_tab_snapshot.chunks(
TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
@@ -462,15 +463,26 @@ impl WrapSnapshot {
);
let mut edit_transforms = Vec::<Transform>::new();
for _ in edit.new_rows.start..edit.new_rows.end {
while let Some(chunk) =
remaining.take().or_else(|| chunks.next().map(|c| c.text))
{
if let Some(ix) = chunk.find('\n') {
line.push_str(&chunk[..ix + 1]);
remaining = Some(&chunk[ix + 1..]);
while let Some(chunk) = remaining.take().or_else(|| chunks.next()) {
if let Some(ix) = chunk.text.find('\n') {
let (prefix, suffix) = chunk.text.split_at(ix + 1);
line_fragments.push(gpui::LineFragment::text(prefix));
line.push_str(prefix);
remaining = Some(Chunk {
text: suffix,
..chunk
});
break;
} else {
line.push_str(chunk)
if let Some(width) =
chunk.renderer.as_ref().and_then(|r| r.measured_width)
{
line_fragments
.push(gpui::LineFragment::element(width, chunk.text.len()));
} else {
line_fragments.push(gpui::LineFragment::text(chunk.text));
}
line.push_str(chunk.text);
}
}
@@ -479,7 +491,7 @@ impl WrapSnapshot {
}
let mut prev_boundary_ix = 0;
for boundary in line_wrapper.wrap_line(&line, wrap_width) {
for boundary in line_wrapper.wrap_line(&line_fragments, wrap_width) {
let wrapped = &line[prev_boundary_ix..boundary.ix];
push_isomorphic(&mut edit_transforms, TextSummary::from(wrapped));
edit_transforms.push(Transform::wrap(boundary.next_indent));
@@ -494,6 +506,7 @@ impl WrapSnapshot {
}
line.clear();
line_fragments.clear();
yield_now().await;
}
@@ -1173,7 +1186,7 @@ mod tests {
display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
test::test_font,
};
use gpui::{px, test::observe};
use gpui::{LineFragment, px, test::observe};
use rand::prelude::*;
use settings::SettingsStore;
use smol::stream::StreamExt;
@@ -1228,8 +1241,7 @@ mod tests {
log::info!("TabMap text: {:?}", tabs_snapshot.text());
let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size);
let unwrapped_text = tabs_snapshot.text();
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
let expected_text = wrap_text(&tabs_snapshot, wrap_width, &mut line_wrapper);
let (wrap_map, _) =
cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font, font_size, wrap_width, cx));
@@ -1246,9 +1258,10 @@ mod tests {
let actual_text = initial_snapshot.text();
assert_eq!(
actual_text, expected_text,
actual_text,
expected_text,
"unwrapped text is: {:?}",
unwrapped_text
tabs_snapshot.text()
);
log::info!("Wrapped text: {:?}", actual_text);
@@ -1311,8 +1324,7 @@ mod tests {
let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
log::info!("TabMap text: {:?}", tabs_snapshot.text());
let unwrapped_text = tabs_snapshot.text();
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
let expected_text = wrap_text(&tabs_snapshot, wrap_width, &mut line_wrapper);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx));
snapshot.check_invariants();
@@ -1328,8 +1340,9 @@ mod tests {
}
if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
let (mut wrapped_snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
let (mut wrapped_snapshot, wrap_edits) = wrap_map.update(cx, |map, cx| {
map.sync(tabs_snapshot.clone(), Vec::new(), cx)
});
let actual_text = wrapped_snapshot.text();
let actual_longest_row = wrapped_snapshot.longest_row();
log::info!("Wrapping finished: {:?}", actual_text);
@@ -1337,9 +1350,10 @@ mod tests {
wrapped_snapshot.verify_chunks(&mut rng);
edits.push((wrapped_snapshot.clone(), wrap_edits));
assert_eq!(
actual_text, expected_text,
actual_text,
expected_text,
"unwrapped text is: {:?}",
unwrapped_text
tabs_snapshot.text()
);
let mut summary = TextSummary::default();
@@ -1425,19 +1439,19 @@ mod tests {
}
fn wrap_text(
unwrapped_text: &str,
tab_snapshot: &TabSnapshot,
wrap_width: Option<Pixels>,
line_wrapper: &mut LineWrapper,
) -> String {
if let Some(wrap_width) = wrap_width {
let mut wrapped_text = String::new();
for (row, line) in unwrapped_text.split('\n').enumerate() {
for (row, line) in tab_snapshot.text().split('\n').enumerate() {
if row > 0 {
wrapped_text.push('\n')
wrapped_text.push('\n');
}
let mut prev_ix = 0;
for boundary in line_wrapper.wrap_line(line, wrap_width) {
for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line)], wrap_width) {
wrapped_text.push_str(&line[prev_ix..boundary.ix]);
wrapped_text.push('\n');
wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
@@ -1445,9 +1459,10 @@ mod tests {
}
wrapped_text.push_str(&line[prev_ix..]);
}
wrapped_text
} else {
unwrapped_text.to_string()
tab_snapshot.text()
}
}

View File

@@ -58,7 +58,7 @@ use clock::ReplicaId;
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
use display_map::*;
pub use display_map::{DisplayPoint, FoldPlaceholder};
pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder};
use editor_settings::GoToDefinitionFallback;
pub use editor_settings::{
CurrentLineHighlight, EditorSettings, HideMouseMode, ScrollBeyondLastLine, SearchSettings,
@@ -15040,6 +15040,15 @@ impl Editor {
self.active_indent_guides_state.dirty = true;
}
pub fn update_fold_widths(
&mut self,
widths: impl IntoIterator<Item = (FoldId, Pixels)>,
cx: &mut Context<Self>,
) -> bool {
self.display_map
.update(cx, |map, cx| map.update_fold_widths(widths, cx))
}
pub fn default_fold_placeholder(&self, cx: &App) -> FoldPlaceholder {
self.display_map.read(cx).fold_placeholder.clone()
}

View File

@@ -1,16 +1,16 @@
use crate::{
BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, ChunkReplacement,
ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow,
DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock,
GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason,
InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, MAX_LINE_LEN,
MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp,
Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold,
BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, ChunkRendererContext,
ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk,
DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode,
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection,
SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
Block, BlockContext, BlockStyle, DisplaySnapshot, FoldId, HighlightedChunk, ToDisplayPoint,
},
editor_settings::{
CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine,
@@ -43,12 +43,8 @@ use gpui::{
transparent_black,
};
use itertools::Itertools;
use language::{
ChunkRendererContext,
language_settings::{
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
ShowWhitespaceSetting,
},
use language::language_settings::{
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
@@ -5809,6 +5805,7 @@ pub(crate) struct LineWithInvisibles {
enum LineFragment {
Text(ShapedLine),
Element {
id: FoldId,
element: Option<AnyElement>,
size: Size<Pixels>,
len: usize,
@@ -5910,6 +5907,7 @@ impl LineWithInvisibles {
width += size.width;
len += highlighted_chunk.text.len();
fragments.push(LineFragment::Element {
id: renderer.id,
element: Some(element),
size,
len: highlighted_chunk.text.len(),
@@ -6865,6 +6863,24 @@ impl Element for EditorElement {
window,
cx,
);
let new_fold_widths = line_layouts
.iter()
.flat_map(|layout| &layout.fragments)
.filter_map(|fragment| {
if let LineFragment::Element { id, size, .. } = fragment {
Some((*id, size.width))
} else {
None
}
});
if self.editor.update(cx, |editor, cx| {
editor.update_fold_widths(new_fold_widths, cx)
}) {
// If the fold widths have changed, we need to prepaint
// the element again to account for any changes in
// wrapping.
return self.prepaint(None, bounds, &mut (), window, cx);
}
let longest_line_blame_width = self
.editor

View File

@@ -37,3 +37,4 @@ serde_json.workspace = true
settings.workspace = true
smol.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -483,8 +483,8 @@ async fn run_eval_project(
for (ix, result) in results.iter().enumerate() {
if result.path.as_ref() == Path::new(&expected_result.file) {
file_matched = true;
let start_matched = result.row_range.contains(&expected_result.lines.start());
let end_matched = result.row_range.contains(&expected_result.lines.end());
let start_matched = result.row_range.contains(expected_result.lines.start());
let end_matched = result.row_range.contains(expected_result.lines.end());
if start_matched || end_matched {
range_overlapped = true;

View File

@@ -34,3 +34,4 @@ util.workspace = true
wasm-encoder.workspace = true
wasmparser.workspace = true
wit-component.workspace = true
workspace-hack.workspace = true

View File

@@ -30,3 +30,4 @@ tokio = { workspace = true, features = ["full"] }
toml.workspace = true
tree-sitter.workspace = true
wasmtime.workspace = true
workspace-hack.workspace = true

View File

@@ -51,6 +51,7 @@ util.workspace = true
wasmparser.workspace = true
wasmtime-wasi.workspace = true
wasmtime.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -38,6 +38,7 @@ util.workspace = true
vim_mode_setting.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -15,3 +15,4 @@ path = "src/feature_flags.rs"
futures.workspace = true
gpui.workspace = true
smol.workspace = true
workspace-hack.workspace = true

View File

@@ -27,6 +27,7 @@ urlencoding.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -32,6 +32,7 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -468,15 +468,9 @@ impl Matches {
path: found_path.clone(),
panel_match: None,
};
self.matches
.extend(currently_opened.into_iter().map(path_to_entry));
self.matches.extend(
history_items
.into_iter()
.filter(|found_path| Some(*found_path) != currently_opened)
.map(path_to_entry),
);
self.matches
.extend(history_items.into_iter().map(path_to_entry));
return;
};

View File

@@ -18,3 +18,4 @@ serde.workspace = true
settings.workspace = true
theme.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -33,6 +33,7 @@ tempfile.workspace = true
text.workspace = true
time.workspace = true
util.workspace = true
workspace-hack.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
fsevent.workspace = true

View File

@@ -15,6 +15,7 @@ doctest = false
[dependencies]
bitflags.workspace = true
parking_lot.workspace = true
workspace-hack.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true

View File

@@ -16,3 +16,4 @@ doctest = false
gpui.workspace = true
util.workspace = true
log.workspace = true
workspace-hack.workspace = true

View File

@@ -38,6 +38,7 @@ url.workspace = true
util.workspace = true
uuid.workspace = true
futures.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true

View File

@@ -25,6 +25,7 @@ serde_json.workspace = true
settings.workspace = true
url.workspace = true
util.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
indoc.workspace = true

View File

@@ -59,6 +59,7 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -26,6 +26,7 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -22,3 +22,4 @@ schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
workspace-hack.workspace = true

View File

@@ -118,6 +118,7 @@ util.workspace = true
uuid.workspace = true
waker-fn = "1.2.0"
lyon = "1.0"
workspace-hack.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
block = "0.1"

View File

@@ -32,7 +32,7 @@ impl LineWrapper {
/// Wrap a line of text to the given width with this wrapper's font and font size.
pub fn wrap_line<'a>(
&'a mut self,
line: &'a str,
fragments: &'a [LineFragment],
wrap_width: Pixels,
) -> impl Iterator<Item = Boundary> + 'a {
let mut width = px(0.);
@@ -42,32 +42,61 @@ impl LineWrapper {
let mut last_candidate_width = px(0.);
let mut last_wrap_ix = 0;
let mut prev_c = '\0';
let mut char_indices = line.char_indices();
let mut index = 0;
let mut candidates = fragments
.into_iter()
.flat_map(move |fragment| fragment.wrap_boundary_candidates())
.peekable();
iter::from_fn(move || {
for (ix, c) in char_indices.by_ref() {
if c == '\n' {
continue;
}
for candidate in candidates.by_ref() {
let ix = index;
index += candidate.len_utf8();
let mut new_prev_c = prev_c;
let item_width = match candidate {
WrapBoundaryCandidate::Char { character: c } => {
if c == '\n' {
continue;
}
if Self::is_word_char(c) {
if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
if Self::is_word_char(c) {
if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
}
} else {
// CJK may not be space separated, e.g.: `Hello world你好世界`
if c != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
}
}
if c != ' ' && first_non_whitespace_ix.is_none() {
first_non_whitespace_ix = Some(ix);
}
new_prev_c = c;
self.width_for_char(c)
}
} else {
// CJK may not be space separated, e.g.: `Hello world你好世界`
if c != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
WrapBoundaryCandidate::Element {
width: element_width,
..
} => {
if prev_c == ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
}
if first_non_whitespace_ix.is_none() {
first_non_whitespace_ix = Some(ix);
}
element_width
}
}
};
if c != ' ' && first_non_whitespace_ix.is_none() {
first_non_whitespace_ix = Some(ix);
}
let char_width = self.width_for_char(c);
width += char_width;
width += item_width;
if width > wrap_width && ix > last_wrap_ix {
if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
{
@@ -82,7 +111,7 @@ impl LineWrapper {
last_candidate_ix = 0;
} else {
last_wrap_ix = ix;
width = char_width;
width = item_width;
}
if let Some(indent) = indent {
@@ -91,7 +120,8 @@ impl LineWrapper {
return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
}
prev_c = c;
prev_c = new_prev_c;
}
None
@@ -213,6 +243,65 @@ fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<Tex
}
}
/// A fragment of a line that can be wrapped.
pub enum LineFragment<'a> {
/// A text fragment consisting of characters.
Text {
/// The text content of the fragment.
text: &'a str,
},
/// A non-text element with a fixed width.
Element {
/// The width of the element in pixels.
width: Pixels,
/// The UTF-8 encoded length of the element.
len_utf8: usize,
},
}
impl<'a> LineFragment<'a> {
/// Creates a new text fragment from the given text.
pub fn text(text: &'a str) -> Self {
LineFragment::Text { text }
}
/// Creates a new non-text element with the given width and UTF-8 encoded length.
pub fn element(width: Pixels, len_utf8: usize) -> Self {
LineFragment::Element { width, len_utf8 }
}
fn wrap_boundary_candidates(&self) -> impl Iterator<Item = WrapBoundaryCandidate> {
let text = match self {
LineFragment::Text { text } => text,
LineFragment::Element { .. } => "\0",
};
text.chars().map(move |character| {
if let LineFragment::Element { width, len_utf8 } = self {
WrapBoundaryCandidate::Element {
width: *width,
len_utf8: *len_utf8,
}
} else {
WrapBoundaryCandidate::Char { character }
}
})
}
}
enum WrapBoundaryCandidate {
Char { character: char },
Element { width: Pixels, len_utf8: usize },
}
impl WrapBoundaryCandidate {
pub fn len_utf8(&self) -> usize {
match self {
WrapBoundaryCandidate::Char { character } => character.len_utf8(),
WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len,
}
}
}
/// A boundary between two lines of text.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Boundary {
@@ -278,7 +367,7 @@ mod tests {
assert_eq!(
wrapper
.wrap_line("aa bbb cccc ddddd eeee", px(72.))
.wrap_line(&[LineFragment::text("aa bbb cccc ddddd eeee")], px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
@@ -288,7 +377,7 @@ mod tests {
);
assert_eq!(
wrapper
.wrap_line("aaa aaaaaaaaaaaaaaaaaa", px(72.0))
.wrap_line(&[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")], px(72.0))
.collect::<Vec<_>>(),
&[
Boundary::new(4, 0),
@@ -298,7 +387,7 @@ mod tests {
);
assert_eq!(
wrapper
.wrap_line(" aaaaaaa", px(72.))
.wrap_line(&[LineFragment::text(" aaaaaaa")], px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 5),
@@ -308,7 +397,10 @@ mod tests {
);
assert_eq!(
wrapper
.wrap_line(" ", px(72.))
.wrap_line(
&[LineFragment::text(" ")],
px(72.)
)
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
@@ -318,7 +410,7 @@ mod tests {
);
assert_eq!(
wrapper
.wrap_line(" aaaaaaaaaaaaaa", px(72.))
.wrap_line(&[LineFragment::text(" aaaaaaaaaaaaaa")], px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
@@ -327,6 +419,84 @@ mod tests {
Boundary::new(22, 3),
]
);
// Test wrapping multiple text fragments
assert_eq!(
wrapper
.wrap_line(
&[
LineFragment::text("aa bbb "),
LineFragment::text("cccc ddddd eeee")
],
px(72.)
)
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(12, 0),
Boundary::new(18, 0)
],
);
// Test wrapping with a mix of text and element fragments
assert_eq!(
wrapper
.wrap_line(
&[
LineFragment::text("aa "),
LineFragment::element(px(20.), 1),
LineFragment::text(" bbb "),
LineFragment::element(px(30.), 1),
LineFragment::text(" cccc")
],
px(72.)
)
.collect::<Vec<_>>(),
&[
Boundary::new(5, 0),
Boundary::new(9, 0),
Boundary::new(11, 0)
],
);
// Test with element at the beginning and text afterward
assert_eq!(
wrapper
.wrap_line(
&[
LineFragment::element(px(50.), 1),
LineFragment::text(" aaaa bbbb cccc dddd")
],
px(72.)
)
.collect::<Vec<_>>(),
&[
Boundary::new(2, 0),
Boundary::new(7, 0),
Boundary::new(12, 0),
Boundary::new(17, 0)
],
);
// Test with a large element that forces wrapping by itself
assert_eq!(
wrapper
.wrap_line(
&[
LineFragment::text("short text "),
LineFragment::element(px(100.), 1),
LineFragment::text(" more text")
],
px(72.)
)
.collect::<Vec<_>>(),
&[
Boundary::new(6, 0),
Boundary::new(11, 0),
Boundary::new(12, 0),
Boundary::new(18, 0)
],
);
}
#[test]

View File

@@ -17,6 +17,7 @@ doctest = true
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
gpui.workspace = true

View File

@@ -16,3 +16,4 @@ doctest = false
util.workspace = true
gpui.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
workspace-hack.workspace = true

View File

@@ -20,6 +20,7 @@ anyhow.workspace = true
html5ever.workspace = true
markup5ever_rcdom.workspace = true
regex.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
indoc.workspace = true

View File

@@ -25,3 +25,4 @@ log.workspace = true
serde.workspace = true
serde_json.workspace = true
url.workspace = true
workspace-hack.workspace = true

View File

@@ -18,3 +18,4 @@ doctest = true
[dependencies]
rustls.workspace = true
rustls-platform-verifier.workspace = true
workspace-hack.workspace = true

View File

@@ -14,3 +14,4 @@ path = "src/icons.rs"
[dependencies]
serde.workspace = true
strum.workspace = true
workspace-hack.workspace = true

View File

@@ -122,6 +122,7 @@ pub enum IconName {
Font,
FontSize,
FontWeight,
ForwardArrow,
GenericClose,
GenericMaximize,
GenericMinimize,

View File

@@ -29,6 +29,7 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -31,6 +31,7 @@ paths.workspace = true
serde.workspace = true
strum.workspace = true
util.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
indoc.workspace = true

Some files were not shown because too many files have changed in this diff Show More