Compare commits
18 Commits
acp-rewind
...
v0.181.2-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cfaadd2af | ||
|
|
b69219e9e0 | ||
|
|
0ae3518b1c | ||
|
|
73964353a4 | ||
|
|
bf6e7cb6ee | ||
|
|
36c4f6082c | ||
|
|
3d032bcf2c | ||
|
|
3e28fa2cc4 | ||
|
|
c42442974d | ||
|
|
f23b972203 | ||
|
|
07d9cd7e88 | ||
|
|
a48238701d | ||
|
|
d17d747c62 | ||
|
|
bc08df2dfd | ||
|
|
0f4b734d91 | ||
|
|
6df29d3279 | ||
|
|
647cca8c8d | ||
|
|
1f81674927 |
43
.config/hakari.toml
Normal file
43
.config/hakari.toml
Normal 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",
|
||||
]
|
||||
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@@ -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
1810
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"]
|
||||
|
||||
1
assets/icons/forward_arrow.svg
Normal file
1
assets/icons/forward_arrow.svg
Normal 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 |
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
},
|
||||
))
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,3 +26,4 @@ serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -19,3 +19,4 @@ smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -15,3 +15,4 @@ workspace = true
|
||||
anyhow.workspace = true
|
||||
gpui.workspace = true
|
||||
rust-embed.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,3 +43,4 @@ serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,3 +29,4 @@ smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
which.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -25,3 +25,4 @@ serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -18,3 +18,4 @@ test-support = []
|
||||
[dependencies]
|
||||
indexmap.workspace = true
|
||||
rustc-hash.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,3 +16,4 @@ doctest = false
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -17,6 +17,7 @@ gpui.workspace = true
|
||||
linkme.workspace = true
|
||||
parking_lot.workspace = true
|
||||
theme.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -24,3 +24,4 @@ ui.workspace = true
|
||||
workspace.workspace = true
|
||||
notifications.workspace = true
|
||||
collections.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -33,3 +33,4 @@ settings.workspace = true
|
||||
smol.workspace = true
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -19,3 +19,4 @@ schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -19,3 +19,4 @@ paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -24,3 +24,4 @@ settings.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -22,3 +22,4 @@ http_client.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -14,6 +14,7 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
regex.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 _,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,3 +37,4 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,3 +34,4 @@ util.workspace = true
|
||||
wasm-encoder.workspace = true
|
||||
wasmparser.workspace = true
|
||||
wit-component.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -30,3 +30,4 @@ tokio = { workspace = true, features = ["full"] }
|
||||
toml.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
wasmtime.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -15,3 +15,4 @@ path = "src/feature_flags.rs"
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
smol.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -18,3 +18,4 @@ serde.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,3 +16,4 @@ doctest = false
|
||||
gpui.workspace = true
|
||||
util.workspace = true
|
||||
log.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -22,3 +22,4 @@ schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,3 +16,4 @@ doctest = false
|
||||
util.workspace = true
|
||||
gpui.workspace = true
|
||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,3 +25,4 @@ log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
url.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -18,3 +18,4 @@ doctest = true
|
||||
[dependencies]
|
||||
rustls.workspace = true
|
||||
rustls-platform-verifier.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -14,3 +14,4 @@ path = "src/icons.rs"
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
strum.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -122,6 +122,7 @@ pub enum IconName {
|
||||
Font,
|
||||
FontSize,
|
||||
FontWeight,
|
||||
ForwardArrow,
|
||||
GenericClose,
|
||||
GenericMaximize,
|
||||
GenericMinimize,
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user