Compare commits
37 Commits
assertion-
...
debugger-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eb192f0ad | ||
|
|
07120789ad | ||
|
|
a7e85e59f1 | ||
|
|
5679906a43 | ||
|
|
0be850e4b7 | ||
|
|
a031beed4e | ||
|
|
fda5c53ee5 | ||
|
|
dbbbb1a3b0 | ||
|
|
88a0aec2f3 | ||
|
|
e6c5833106 | ||
|
|
0ce0763c9a | ||
|
|
545abd95fd | ||
|
|
04060f6d5c | ||
|
|
0cb2ff84ed | ||
|
|
351adc23f1 | ||
|
|
0dd28acd51 | ||
|
|
1b13060ab7 | ||
|
|
0b5b0e8825 | ||
|
|
30cf4a81a2 | ||
|
|
59f99b062c | ||
|
|
ecae316e93 | ||
|
|
c285cdc1c1 | ||
|
|
9d59e9d7b5 | ||
|
|
0b878b6ae7 | ||
|
|
076683635a | ||
|
|
cd4a322c78 | ||
|
|
f47fcb9afa | ||
|
|
3f283bd325 | ||
|
|
e68cae9ba0 | ||
|
|
f181d5913a | ||
|
|
f9a07827c9 | ||
|
|
1e59dbb224 | ||
|
|
7f1fb6c63d | ||
|
|
7e44a285f1 | ||
|
|
db593612bd | ||
|
|
c2b70384e5 | ||
|
|
29987f9074 |
71
.github/workflows/eval.yml
vendored
71
.github/workflows/eval.yml
vendored
@@ -1,71 +0,0 @@
|
||||
name: Run Agent Eval
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_EVAL_TELEMETRY: 1
|
||||
|
||||
jobs:
|
||||
run_eval:
|
||||
timeout-minutes: 60
|
||||
name: Run Agent Eval
|
||||
if: >
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-provider: "buildjet"
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: ./script/linux
|
||||
|
||||
- name: Configure CI
|
||||
run: |
|
||||
mkdir -p ./../.cargo
|
||||
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
||||
|
||||
- name: Compile eval
|
||||
run: cargo build --package=eval
|
||||
|
||||
- name: Run eval
|
||||
run: cargo run --package=eval -- --repetitions=3 --concurrency=1
|
||||
|
||||
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
|
||||
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
|
||||
# to clean up the config file, I’ve included the cleanup code here as a precaution.
|
||||
# While it’s not strictly necessary at this moment, I believe it’s better to err on the side of caution.
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
28
.github/workflows/run_agent_eval_daily.yml
vendored
Normal file
28
.github/workflows/run_agent_eval_daily.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Run Eval Daily
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 2 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
run_eval:
|
||||
name: Run Eval
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Run cargo eval
|
||||
run: cargo run -p eval
|
||||
413
Cargo.lock
generated
413
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -605,7 +605,7 @@ wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.7.0"
|
||||
zed_llm_client = "0.6.1"
|
||||
zstd = "0.11"
|
||||
metal = "0.29"
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<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-image-icon lucide-image"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
|
||||
|
Before Width: | Height: | Size: 372 B |
@@ -1028,10 +1028,10 @@
|
||||
// Using `ctrl-shift-space` in Zed requires disabling the macOS global shortcut.
|
||||
// System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck)
|
||||
"ctrl-shift-space": "terminal::ToggleViMode",
|
||||
"ctrl-alt-up": "pane::SplitUp",
|
||||
"ctrl-alt-down": "pane::SplitDown",
|
||||
"ctrl-alt-left": "pane::SplitLeft",
|
||||
"ctrl-alt-right": "pane::SplitRight"
|
||||
"ctrl-k up": "pane::SplitUp",
|
||||
"ctrl-k down": "pane::SplitDown",
|
||||
"ctrl-k left": "pane::SplitLeft",
|
||||
"ctrl-k right": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1489,12 +1489,7 @@
|
||||
"use_multiline_find": false,
|
||||
"use_smartcase_find": false,
|
||||
"highlight_on_yank_duration": 200,
|
||||
"custom_digraphs": {},
|
||||
// Cursor shape for the each mode.
|
||||
// Specify the mode as the key and the shape as the value.
|
||||
// The mode can be one of the following: "normal", "replace", "insert", "visual".
|
||||
// The shape can be one of the following: "block", "bar", "underline", "hollow".
|
||||
"cursor_shape": {}
|
||||
"custom_digraphs": {}
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
|
||||
@@ -6,9 +6,7 @@ use crate::thread::{
|
||||
};
|
||||
use crate::thread_store::{RulesLoadingError, ThreadStore};
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse};
|
||||
use crate::ui::{
|
||||
AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
|
||||
};
|
||||
use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
|
||||
use crate::{AssistantPanel, OpenActiveThreadAsMarkdown};
|
||||
use anyhow::Context as _;
|
||||
use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
|
||||
@@ -433,39 +431,47 @@ fn render_markdown_code_block(
|
||||
workspace
|
||||
.update(cx, {
|
||||
|workspace, cx| {
|
||||
let Some(project_path) = workspace
|
||||
if let Some(project_path) = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.find_project_path(&path_range.path, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(target) = path_range.range.as_ref().map(|range| {
|
||||
Point::new(
|
||||
// Line number is 1-based
|
||||
range.start.line.saturating_sub(1),
|
||||
range.start.col.unwrap_or(0),
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let open_task =
|
||||
workspace.open_path(project_path, None, true, window, cx);
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let item = open_task.await?;
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
active_editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
target, window, cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
{
|
||||
let target = path_range.range.as_ref().map(|range| {
|
||||
Point::new(
|
||||
// Line number is 1-based
|
||||
range.start.line.saturating_sub(1),
|
||||
range.start.col.unwrap_or(0),
|
||||
)
|
||||
});
|
||||
let open_task = workspace.open_path(
|
||||
project_path,
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let item = open_task.await?;
|
||||
if let Some(target) = target {
|
||||
if let Some(active_editor) =
|
||||
item.downcast::<Editor>()
|
||||
{
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor
|
||||
.go_to_singleton_buffer_point(
|
||||
target, window, cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
@@ -664,26 +670,6 @@ fn open_markdown_link(
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
Some(MentionLink::Selection(path, line_range)) => {
|
||||
let open_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(path, None, true, window, cx)
|
||||
});
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let active_editor = open_task
|
||||
.await?
|
||||
.downcast::<Editor>()
|
||||
.context("Item is not an editor")?;
|
||||
active_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
|
||||
s.select_ranges([Point::new(line_range.start as u32, 0)
|
||||
..Point::new(line_range.start as u32, 0)])
|
||||
});
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
Some(MentionLink::Thread(thread_id)) => workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
@@ -799,11 +785,10 @@ impl ActiveThread {
|
||||
self.thread.read(cx).summary_or_default()
|
||||
}
|
||||
|
||||
pub fn cancel_last_completion(&mut self, window: &mut Window, cx: &mut App) -> bool {
|
||||
pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
|
||||
self.last_error.take();
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_last_completion(Some(window.window_handle()), cx)
|
||||
})
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.cancel_last_completion(cx))
|
||||
}
|
||||
|
||||
pub fn last_error(&self) -> Option<ThreadError> {
|
||||
@@ -1025,7 +1010,6 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
ThreadEvent::CheckpointChanged => cx.notify(),
|
||||
ThreadEvent::ReceivedTextChunk => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1088,21 +1072,9 @@ impl ActiveThread {
|
||||
) {
|
||||
let options = AgentNotification::window_options(screen, cx);
|
||||
|
||||
let project_name = self.workspace.upgrade().and_then(|workspace| {
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.map(|worktree| worktree.read(cx).root_name().to_string())
|
||||
});
|
||||
|
||||
if let Some(screen_window) = cx
|
||||
.open_window(options, |_, cx| {
|
||||
cx.new(|_| {
|
||||
AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
|
||||
})
|
||||
cx.new(|_| AgentNotification::new(title.clone(), caption.clone(), icon))
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
@@ -1307,7 +1279,7 @@ impl ActiveThread {
|
||||
fn confirm_editing_message(
|
||||
&mut self,
|
||||
_: &menu::Confirm,
|
||||
window: &mut Window,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some((message_id, state)) = self.editing_message.take() else {
|
||||
@@ -1337,7 +1309,7 @@ impl ActiveThread {
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model.model, Some(window.window_handle()), cx);
|
||||
thread.send_to_model(model.model, cx)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1499,8 +1471,45 @@ impl ActiveThread {
|
||||
|
||||
let needs_confirmation = tool_uses.iter().any(|tool_use| tool_use.needs_confirmation);
|
||||
|
||||
let generating_label = (is_generating && is_last_message)
|
||||
.then(|| AnimatedLabel::new("Generating").size(LabelSize::Small));
|
||||
let generating_label = (is_generating && is_last_message).then(|| {
|
||||
Label::new("Generating")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.with_animations(
|
||||
"generating-label",
|
||||
vec![
|
||||
Animation::new(Duration::from_secs(1)),
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
],
|
||||
|mut label, animation_ix, delta| {
|
||||
match animation_ix {
|
||||
0 => {
|
||||
let chars_to_show = (delta * 10.).ceil() as usize;
|
||||
let text = &"Generating"[0..chars_to_show];
|
||||
label.set_text(text);
|
||||
}
|
||||
1 => {
|
||||
let text = match delta {
|
||||
d if d < 0.25 => "Generating",
|
||||
d if d < 0.5 => "Generating.",
|
||||
d if d < 0.75 => "Generating..",
|
||||
_ => "Generating...",
|
||||
};
|
||||
label.set_text(text);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
label
|
||||
},
|
||||
)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.map_element(|label| label.alpha(delta)),
|
||||
)
|
||||
});
|
||||
|
||||
// Don't render user messages that are just there for returning tool results.
|
||||
if message.role == Role::User && thread.message_has_tool_results(message_id) {
|
||||
@@ -1527,7 +1536,9 @@ impl ActiveThread {
|
||||
.map(|(_, state)| state.editor.clone());
|
||||
|
||||
let colors = cx.theme().colors();
|
||||
let active_color = colors.element_active;
|
||||
let editor_bg_color = colors.editor_background;
|
||||
let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
|
||||
|
||||
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileCode)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
@@ -1654,7 +1665,7 @@ impl ActiveThread {
|
||||
let message_content =
|
||||
has_content.then(|| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.gap_1p5()
|
||||
.when(!message_is_empty, |parent| {
|
||||
parent.child(
|
||||
if let Some(edit_message_editor) = edit_message_editor.clone() {
|
||||
@@ -1677,6 +1688,7 @@ impl ActiveThread {
|
||||
.on_action(cx.listener(Self::cancel_editing_message))
|
||||
.on_action(cx.listener(Self::confirm_editing_message))
|
||||
.min_h_6()
|
||||
.pt_1()
|
||||
.child(EditorElement::new(
|
||||
&edit_message_editor,
|
||||
EditorStyle {
|
||||
@@ -1691,6 +1703,7 @@ impl ActiveThread {
|
||||
} else {
|
||||
div()
|
||||
.min_h_6()
|
||||
.text_ui(cx)
|
||||
.child(self.render_message_content(
|
||||
message_id,
|
||||
rendered_message,
|
||||
@@ -1749,18 +1762,35 @@ impl ActiveThread {
|
||||
.pb_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.bg(editor_bg_color)
|
||||
.bg(colors.editor_background)
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.shadow_md()
|
||||
.child(div().p_2().children(message_content))
|
||||
.child(
|
||||
h_flex()
|
||||
.p_1()
|
||||
.border_t_1()
|
||||
.border_color(colors.border_variant)
|
||||
.justify_end()
|
||||
.py_1()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.bg(bg_user_message_header)
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
.justify_between()
|
||||
.rounded_t_md()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::PersonCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new("You")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -1813,12 +1843,8 @@ impl ActiveThread {
|
||||
edit_message_editor.is_none() && allow_editing_message,
|
||||
|this| {
|
||||
this.child(
|
||||
Button::new("edit-message", "Edit Message")
|
||||
Button::new("edit-message", "Edit")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Pencil)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener({
|
||||
let message_segments =
|
||||
message.segments.clone();
|
||||
@@ -1835,7 +1861,8 @@ impl ActiveThread {
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(div().p_2().children(message_content)),
|
||||
),
|
||||
Role::Assistant => v_flex()
|
||||
.id(("message-container", ix))
|
||||
@@ -1843,9 +1870,11 @@ impl ActiveThread {
|
||||
.gap_2()
|
||||
.children(message_content)
|
||||
.when(has_tool_uses, |parent| {
|
||||
parent.children(tool_uses.into_iter().map(|tool_use| {
|
||||
self.render_tool_use(tool_use, window, workspace.clone(), cx)
|
||||
}))
|
||||
parent.children(
|
||||
tool_uses
|
||||
.into_iter()
|
||||
.map(|tool_use| self.render_tool_use(tool_use, window, cx)),
|
||||
)
|
||||
}),
|
||||
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
|
||||
v_flex()
|
||||
@@ -2072,13 +2101,11 @@ impl ActiveThread {
|
||||
.map(|m| m.role)
|
||||
.unwrap_or(Role::User);
|
||||
|
||||
let is_assistant_message = message_role == Role::Assistant;
|
||||
let is_user_message = message_role == Role::User;
|
||||
let is_assistant = message_role == Role::Assistant;
|
||||
|
||||
v_flex()
|
||||
.text_ui(cx)
|
||||
.gap_2()
|
||||
.when(is_user_message, |this| this.text_xs())
|
||||
.children(
|
||||
rendered_message.segments.iter().enumerate().map(
|
||||
|(index, segment)| match segment {
|
||||
@@ -2099,28 +2126,10 @@ impl ActiveThread {
|
||||
RenderedMessageSegment::Text(markdown) => {
|
||||
let markdown_element = MarkdownElement::new(
|
||||
markdown.clone(),
|
||||
if is_user_message {
|
||||
let mut style = default_markdown_style(window, cx);
|
||||
let mut text_style = window.text_style();
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
|
||||
let buffer_font = theme_settings.buffer_font.family.clone();
|
||||
let buffer_font_size = TextSize::Small.rems(cx);
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(buffer_font),
|
||||
font_size: Some(buffer_font_size.into()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
style.base_text_style = text_style;
|
||||
style
|
||||
} else {
|
||||
default_markdown_style(window, cx)
|
||||
},
|
||||
default_markdown_style(window, cx),
|
||||
);
|
||||
|
||||
let markdown_element = if is_assistant_message {
|
||||
let markdown_element = if is_assistant {
|
||||
markdown_element.code_block_renderer(
|
||||
markdown::CodeBlockRenderer::Custom {
|
||||
render: Arc::new({
|
||||
@@ -2259,7 +2268,34 @@ impl ActiveThread {
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(AnimatedLabel::new("Thinking").size(LabelSize::Small)),
|
||||
.child({
|
||||
Label::new("Thinking")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"generating-label",
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
|mut label, delta| {
|
||||
let text = match delta {
|
||||
d if d < 0.25 => "Thinking",
|
||||
d if d < 0.5 => "Thinking.",
|
||||
d if d < 0.75 => "Thinking..",
|
||||
_ => "Thinking...",
|
||||
};
|
||||
label.set_text(text);
|
||||
label
|
||||
},
|
||||
)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| {
|
||||
label.map_element(|label| label.alpha(delta))
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -2438,11 +2474,10 @@ impl ActiveThread {
|
||||
&self,
|
||||
tool_use: ToolUse,
|
||||
window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement + use<> {
|
||||
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
|
||||
return card.render(&tool_use.status, window, workspace, cx);
|
||||
return card.render(&tool_use.status, window, cx);
|
||||
}
|
||||
|
||||
let is_open = self
|
||||
@@ -2458,7 +2493,7 @@ impl ActiveThread {
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).app_state().fs.clone());
|
||||
let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
|
||||
let needs_confirmation_tools = tool_use.needs_confirmation;
|
||||
let edit_tools = tool_use.needs_confirmation;
|
||||
|
||||
let status_icons = div().child(match &tool_use.status {
|
||||
ToolUseStatus::NeedsConfirmation => {
|
||||
@@ -2556,33 +2591,33 @@ impl ActiveThread {
|
||||
)),
|
||||
),
|
||||
ToolUseStatus::InputStillStreaming | ToolUseStatus::Running => container.child(
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(
|
||||
delta,
|
||||
)))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Running…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
results_content_container().child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.pb_1()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(
|
||||
delta,
|
||||
)))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Running…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Error(_) => container.child(
|
||||
results_content_container()
|
||||
@@ -2646,8 +2681,8 @@ impl ActiveThread {
|
||||
))
|
||||
};
|
||||
|
||||
v_flex().gap_1().mb_2().map(|element| {
|
||||
if !needs_confirmation_tools {
|
||||
v_flex().gap_1().mb_3().map(|element| {
|
||||
if !edit_tools {
|
||||
element.child(
|
||||
v_flex()
|
||||
.child(
|
||||
@@ -2825,7 +2860,30 @@ impl ActiveThread {
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.rounded_b_lg()
|
||||
.child(
|
||||
AnimatedLabel::new("Waiting for Confirmation").size(LabelSize::Small)
|
||||
Label::new("Waiting for Confirmation…")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"generating-label",
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
|mut label, delta| {
|
||||
let text = match delta {
|
||||
d if d < 0.25 => "Waiting for Confirmation",
|
||||
d if d < 0.5 => "Waiting for Confirmation.",
|
||||
d if d < 0.75 => "Waiting for Confirmation..",
|
||||
_ => "Waiting for Confirmation...",
|
||||
};
|
||||
label.set_text(text);
|
||||
label
|
||||
},
|
||||
)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.map_element(|label| label.alpha(delta)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -3039,7 +3097,7 @@ impl ActiveThread {
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
_: &ClickEvent,
|
||||
window: &mut Window,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
|
||||
@@ -3055,7 +3113,6 @@ impl ActiveThread {
|
||||
c.input.clone(),
|
||||
&c.messages,
|
||||
c.tool.clone(),
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -3067,12 +3124,11 @@ impl ActiveThread {
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
_: &ClickEvent,
|
||||
window: &mut Window,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let window_handle = window.window_handle();
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.deny_tool_use(tool_use_id, tool_name, Some(window_handle), cx);
|
||||
thread.deny_tool_use(tool_use_id, tool_name, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3226,10 +3282,12 @@ pub(crate) fn open_context(
|
||||
}
|
||||
}
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
let entry_id = directory_context.entry_id;
|
||||
let project_path = directory_context.project_path(cx);
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |_project, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry_id));
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
if let Some(entry) = project.entry_for_path(&project_path, cx) {
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -3252,15 +3310,15 @@ pub(crate) fn open_context(
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
AssistantContext::Selection(selection_context) => {
|
||||
if let Some(project_path) = selection_context
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
if let Some(project_path) = excerpt_context
|
||||
.context_buffer
|
||||
.buffer
|
||||
.read(cx)
|
||||
.project_path(cx)
|
||||
{
|
||||
let snapshot = selection_context.context_buffer.buffer.read(cx).snapshot();
|
||||
let target_position = selection_context.range.start.to_point(&snapshot);
|
||||
let snapshot = excerpt_context.context_buffer.buffer.read(cx).snapshot();
|
||||
let target_position = excerpt_context.range.start.to_point(&snapshot);
|
||||
|
||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||
.detach();
|
||||
@@ -3287,7 +3345,6 @@ pub(crate) fn open_context(
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
AssistantContext::Image(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Keep, KeepAll, Reject, RejectAll, Thread, ThreadEvent, ui::AnimatedLabel};
|
||||
use crate::{Keep, KeepAll, Reject, RejectAll, Thread, ThreadEvent};
|
||||
use anyhow::Result;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -8,8 +8,8 @@ use editor::{
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, Empty, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
Action, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
|
||||
Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
};
|
||||
use language::{Capability, DiskState, OffsetRangeExt, Point};
|
||||
use multi_buffer::PathKey;
|
||||
@@ -307,10 +307,6 @@ impl AgentDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.thread.read(cx).is_generating() {
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||
let diff_hunks_in_ranges = self
|
||||
.editor
|
||||
@@ -343,10 +339,6 @@ impl AgentDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.thread.read(cx).is_generating() {
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||
let diff_hunks_in_ranges = self
|
||||
.editor
|
||||
@@ -658,11 +650,6 @@ fn render_diff_hunk_controls(
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let editor = editor.clone();
|
||||
|
||||
if agent_diff.read(cx).thread.read(cx).is_generating() {
|
||||
return Empty.into_any();
|
||||
}
|
||||
|
||||
h_flex()
|
||||
.h(line_height)
|
||||
.mr_0p5()
|
||||
@@ -870,14 +857,8 @@ impl Render for AgentDiffToolbar {
|
||||
None => return div(),
|
||||
};
|
||||
|
||||
let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
|
||||
if is_generating {
|
||||
return div()
|
||||
.w(rems(6.5625)) // Arbitrary 105px size—so the label doesn't dance around
|
||||
.child(AnimatedLabel::new("Generating"));
|
||||
}
|
||||
|
||||
let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();
|
||||
|
||||
if is_empty {
|
||||
return div();
|
||||
}
|
||||
@@ -988,7 +969,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|_, cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit(
|
||||
|
||||
@@ -356,9 +356,14 @@ impl AssistantPanel {
|
||||
&self.thread_store
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn cancel(
|
||||
&mut self,
|
||||
_: &editor::actions::Cancel,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
|
||||
.update(cx, |thread, cx| thread.cancel_last_completion(cx));
|
||||
}
|
||||
|
||||
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -1545,7 +1550,7 @@ impl AssistantPanel {
|
||||
fn render_usage_banner(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let usage = self.thread.read(cx).last_usage()?;
|
||||
|
||||
Some(UsageBanner::new(zed_llm_client::Plan::ZedProTrial, usage).into_any_element())
|
||||
Some(UsageBanner::new(zed_llm_client::Plan::ZedProTrial, usage.amount).into_any_element())
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
@@ -1946,9 +1951,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (buffer, range) in selection_ranges {
|
||||
store
|
||||
.add_selection(buffer, range, cx)
|
||||
.detach_and_log_err(cx);
|
||||
store.add_excerpt(range, buffer, cx).detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
use std::{
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
|
||||
use futures::{FutureExt, future::Shared};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use language::Buffer;
|
||||
use language_model::{LanguageModelImage, LanguageModelRequestMessage};
|
||||
use project::{ProjectEntryId, ProjectPath, Worktree};
|
||||
use gpui::{App, Entity, SharedString};
|
||||
use language::{Buffer, File};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::{ProjectPath, Worktree};
|
||||
use prompt_store::UserPromptId;
|
||||
use rope::Point;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -33,11 +28,10 @@ pub enum ContextKind {
|
||||
File,
|
||||
Directory,
|
||||
Symbol,
|
||||
Selection,
|
||||
Excerpt,
|
||||
FetchedUrl,
|
||||
Thread,
|
||||
Rules,
|
||||
Image,
|
||||
}
|
||||
|
||||
impl ContextKind {
|
||||
@@ -46,11 +40,10 @@ impl ContextKind {
|
||||
ContextKind::File => IconName::File,
|
||||
ContextKind::Directory => IconName::Folder,
|
||||
ContextKind::Symbol => IconName::Code,
|
||||
ContextKind::Selection => IconName::Context,
|
||||
ContextKind::Excerpt => IconName::Code,
|
||||
ContextKind::FetchedUrl => IconName::Globe,
|
||||
ContextKind::Thread => IconName::MessageBubbles,
|
||||
ContextKind::Rules => RULES_ICON,
|
||||
ContextKind::Image => IconName::Image,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,9 +55,8 @@ pub enum AssistantContext {
|
||||
Symbol(SymbolContext),
|
||||
FetchedUrl(FetchedUrlContext),
|
||||
Thread(ThreadContext),
|
||||
Selection(SelectionContext),
|
||||
Excerpt(ExcerptContext),
|
||||
Rules(RulesContext),
|
||||
Image(ImageContext),
|
||||
}
|
||||
|
||||
impl AssistantContext {
|
||||
@@ -75,9 +67,8 @@ impl AssistantContext {
|
||||
Self::Symbol(symbol) => symbol.id,
|
||||
Self::FetchedUrl(url) => url.id,
|
||||
Self::Thread(thread) => thread.id,
|
||||
Self::Selection(selection) => selection.id,
|
||||
Self::Excerpt(excerpt) => excerpt.id,
|
||||
Self::Rules(rules) => rules.id,
|
||||
Self::Image(image) => image.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,25 +83,17 @@ pub struct FileContext {
|
||||
pub struct DirectoryContext {
|
||||
pub id: ContextId,
|
||||
pub worktree: Entity<Worktree>,
|
||||
pub entry_id: ProjectEntryId,
|
||||
pub last_path: Arc<Path>,
|
||||
pub path: Arc<Path>,
|
||||
/// Buffers of the files within the directory.
|
||||
pub context_buffers: Vec<ContextBuffer>,
|
||||
}
|
||||
|
||||
impl DirectoryContext {
|
||||
pub fn entry<'a>(&self, cx: &'a App) -> Option<&'a project::Entry> {
|
||||
self.worktree.read(cx).entry_for_id(self.entry_id)
|
||||
}
|
||||
|
||||
pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
|
||||
let worktree = self.worktree.read(cx);
|
||||
worktree
|
||||
.entry_for_id(self.entry_id)
|
||||
.map(|entry| ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: entry.path.clone(),
|
||||
})
|
||||
pub fn project_path(&self, cx: &App) -> ProjectPath {
|
||||
ProjectPath {
|
||||
worktree_id: self.worktree.read(cx).id(),
|
||||
path: self.path.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,51 +128,17 @@ impl ThreadContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImageContext {
|
||||
pub id: ContextId,
|
||||
pub original_image: Arc<gpui::Image>,
|
||||
pub image_task: Shared<Task<Option<LanguageModelImage>>>,
|
||||
}
|
||||
|
||||
impl ImageContext {
|
||||
pub fn image(&self) -> Option<LanguageModelImage> {
|
||||
self.image_task.clone().now_or_never().flatten()
|
||||
}
|
||||
|
||||
pub fn is_loading(&self) -> bool {
|
||||
self.image_task.clone().now_or_never().is_none()
|
||||
}
|
||||
|
||||
pub fn is_error(&self) -> bool {
|
||||
self.image_task
|
||||
.clone()
|
||||
.now_or_never()
|
||||
.map(|result| result.is_none())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ContextBuffer {
|
||||
pub id: BufferId,
|
||||
// TODO: Entity<Buffer> holds onto the buffer even if the buffer is deleted. Should probably be
|
||||
// TODO: Entity<Buffer> holds onto the thread even if the thread is deleted. Should probably be
|
||||
// a WeakEntity and handle removal from the UI when it has dropped.
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub last_full_path: Arc<Path>,
|
||||
pub file: Arc<dyn File>,
|
||||
pub version: clock::Global,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
impl ContextBuffer {
|
||||
pub fn full_path(&self, cx: &App) -> PathBuf {
|
||||
let file = self.buffer.read(cx).file();
|
||||
// Note that in practice file can't be `None` because it is present when this is created and
|
||||
// there's no way for buffers to go from having a file to not.
|
||||
file.map_or(self.last_full_path.to_path_buf(), |file| file.full_path(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ContextBuffer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ContextBuffer")
|
||||
@@ -220,7 +169,7 @@ pub struct ContextSymbolId {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SelectionContext {
|
||||
pub struct ExcerptContext {
|
||||
pub id: ContextId,
|
||||
pub range: Range<Anchor>,
|
||||
pub line_range: Range<Point>,
|
||||
@@ -243,7 +192,7 @@ pub fn format_context_as_string<'a>(
|
||||
let mut file_context = Vec::new();
|
||||
let mut directory_context = Vec::new();
|
||||
let mut symbol_context = Vec::new();
|
||||
let mut selection_context = Vec::new();
|
||||
let mut excerpt_context = Vec::new();
|
||||
let mut fetch_context = Vec::new();
|
||||
let mut thread_context = Vec::new();
|
||||
let mut rules_context = Vec::new();
|
||||
@@ -253,18 +202,17 @@ pub fn format_context_as_string<'a>(
|
||||
AssistantContext::File(context) => file_context.push(context),
|
||||
AssistantContext::Directory(context) => directory_context.push(context),
|
||||
AssistantContext::Symbol(context) => symbol_context.push(context),
|
||||
AssistantContext::Selection(context) => selection_context.push(context),
|
||||
AssistantContext::Excerpt(context) => excerpt_context.push(context),
|
||||
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
|
||||
AssistantContext::Thread(context) => thread_context.push(context),
|
||||
AssistantContext::Rules(context) => rules_context.push(context),
|
||||
AssistantContext::Image(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
if file_context.is_empty()
|
||||
&& directory_context.is_empty()
|
||||
&& symbol_context.is_empty()
|
||||
&& selection_context.is_empty()
|
||||
&& excerpt_context.is_empty()
|
||||
&& fetch_context.is_empty()
|
||||
&& thread_context.is_empty()
|
||||
&& rules_context.is_empty()
|
||||
@@ -303,13 +251,13 @@ pub fn format_context_as_string<'a>(
|
||||
result.push_str("</symbols>\n");
|
||||
}
|
||||
|
||||
if !selection_context.is_empty() {
|
||||
result.push_str("<selections>\n");
|
||||
for context in selection_context {
|
||||
if !excerpt_context.is_empty() {
|
||||
result.push_str("<excerpts>\n");
|
||||
for context in excerpt_context {
|
||||
result.push_str(&context.context_buffer.text);
|
||||
result.push('\n');
|
||||
}
|
||||
result.push_str("</selections>\n");
|
||||
result.push_str("</excerpts>\n");
|
||||
}
|
||||
|
||||
if !fetch_context.is_empty() {
|
||||
|
||||
@@ -17,7 +17,6 @@ use gpui::{
|
||||
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use language::Buffer;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::{Entry, ProjectPath};
|
||||
use prompt_store::UserPromptId;
|
||||
@@ -41,35 +40,6 @@ use crate::context_store::ContextStore;
|
||||
use crate::thread::ThreadId;
|
||||
use crate::thread_store::ThreadStore;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ContextPickerEntry {
|
||||
Mode(ContextPickerMode),
|
||||
Action(ContextPickerAction),
|
||||
}
|
||||
|
||||
impl ContextPickerEntry {
|
||||
pub fn keyword(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Mode(mode) => mode.keyword(),
|
||||
Self::Action(action) => action.keyword(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Mode(mode) => mode.label(),
|
||||
Self::Action(action) => action.label(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(&self) -> IconName {
|
||||
match self {
|
||||
Self::Mode(mode) => mode.icon(),
|
||||
Self::Action(action) => action.icon(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ContextPickerMode {
|
||||
File,
|
||||
@@ -79,31 +49,6 @@ enum ContextPickerMode {
|
||||
Rules,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ContextPickerAction {
|
||||
AddSelections,
|
||||
}
|
||||
|
||||
impl ContextPickerAction {
|
||||
pub fn keyword(&self) -> &'static str {
|
||||
match self {
|
||||
Self::AddSelections => "selection",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::AddSelections => "Selection",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(&self) -> IconName {
|
||||
match self {
|
||||
Self::AddSelections => IconName::Context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ContextPickerMode {
|
||||
type Error = String;
|
||||
|
||||
@@ -120,7 +65,7 @@ impl TryFrom<&str> for ContextPickerMode {
|
||||
}
|
||||
|
||||
impl ContextPickerMode {
|
||||
pub fn keyword(&self) -> &'static str {
|
||||
pub fn mention_prefix(&self) -> &'static str {
|
||||
match self {
|
||||
Self::File => "file",
|
||||
Self::Symbol => "symbol",
|
||||
@@ -222,13 +167,7 @@ impl ContextPicker {
|
||||
.enumerate()
|
||||
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
|
||||
|
||||
let entries = self
|
||||
.workspace
|
||||
.upgrade()
|
||||
.map(|workspace| {
|
||||
available_context_picker_entries(&self.thread_store, &workspace, cx)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let modes = supported_context_picker_modes(&self.thread_store);
|
||||
|
||||
menu.when(has_recent, |menu| {
|
||||
menu.custom_row(|_, _| {
|
||||
@@ -244,15 +183,15 @@ impl ContextPicker {
|
||||
})
|
||||
.extend(recent_entries)
|
||||
.when(has_recent, |menu| menu.separator())
|
||||
.extend(entries.into_iter().map(|entry| {
|
||||
.extend(modes.into_iter().map(|mode| {
|
||||
let context_picker = context_picker.clone();
|
||||
|
||||
ContextMenuEntry::new(entry.label())
|
||||
.icon(entry.icon())
|
||||
ContextMenuEntry::new(mode.label())
|
||||
.icon(mode.icon())
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
|
||||
context_picker.update(cx, |this, cx| this.select_mode(mode, window, cx))
|
||||
})
|
||||
}))
|
||||
.keep_open_on_confirm()
|
||||
@@ -271,87 +210,74 @@ impl ContextPicker {
|
||||
self.thread_store.is_some()
|
||||
}
|
||||
|
||||
fn select_entry(
|
||||
fn select_mode(
|
||||
&mut self,
|
||||
entry: ContextPickerEntry,
|
||||
mode: ContextPickerMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let context_picker = cx.entity().downgrade();
|
||||
|
||||
match entry {
|
||||
ContextPickerEntry::Mode(mode) => match mode {
|
||||
ContextPickerMode::File => {
|
||||
self.mode = ContextPickerState::File(cx.new(|cx| {
|
||||
FileContextPicker::new(
|
||||
match mode {
|
||||
ContextPickerMode::File => {
|
||||
self.mode = ContextPickerState::File(cx.new(|cx| {
|
||||
FileContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Symbol => {
|
||||
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
|
||||
SymbolContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Fetch => {
|
||||
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
|
||||
FetchContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Thread => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
self.mode = ContextPickerState::Thread(cx.new(|cx| {
|
||||
ThreadContextPicker::new(
|
||||
thread_store.clone(),
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Symbol => {
|
||||
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
|
||||
SymbolContextPicker::new(
|
||||
}
|
||||
ContextPickerMode::Rules => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
self.mode = ContextPickerState::Rules(cx.new(|cx| {
|
||||
RulesContextPicker::new(
|
||||
thread_store.clone(),
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Rules => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
self.mode = ContextPickerState::Rules(cx.new(|cx| {
|
||||
RulesContextPicker::new(
|
||||
thread_store.clone(),
|
||||
context_picker.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
ContextPickerMode::Fetch => {
|
||||
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
|
||||
FetchContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Thread => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
self.mode = ContextPickerState::Thread(cx.new(|cx| {
|
||||
ThreadContextPicker::new(
|
||||
thread_store.clone(),
|
||||
context_picker.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
ContextPickerEntry::Action(action) => match action {
|
||||
ContextPickerAction::AddSelections => {
|
||||
if let Some((context_store, workspace)) =
|
||||
self.context_store.upgrade().zip(self.workspace.upgrade())
|
||||
{
|
||||
add_selections_as_context(&context_store, &workspace, cx);
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@@ -525,37 +451,19 @@ enum RecentEntry {
|
||||
Thread(ThreadContextEntry),
|
||||
}
|
||||
|
||||
fn available_context_picker_entries(
|
||||
fn supported_context_picker_modes(
|
||||
thread_store: &Option<WeakEntity<ThreadStore>>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Vec<ContextPickerEntry> {
|
||||
let mut entries = vec![
|
||||
ContextPickerEntry::Mode(ContextPickerMode::File),
|
||||
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
|
||||
) -> Vec<ContextPickerMode> {
|
||||
let mut modes = vec![
|
||||
ContextPickerMode::File,
|
||||
ContextPickerMode::Symbol,
|
||||
ContextPickerMode::Fetch,
|
||||
];
|
||||
|
||||
let has_selection = workspace
|
||||
.read(cx)
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
.map_or(false, |editor| {
|
||||
editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
|
||||
});
|
||||
if has_selection {
|
||||
entries.push(ContextPickerEntry::Action(
|
||||
ContextPickerAction::AddSelections,
|
||||
));
|
||||
}
|
||||
|
||||
if thread_store.is_some() {
|
||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
|
||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
|
||||
modes.push(ContextPickerMode::Thread);
|
||||
modes.push(ContextPickerMode::Rules);
|
||||
}
|
||||
|
||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
|
||||
|
||||
entries
|
||||
modes
|
||||
}
|
||||
|
||||
fn recent_context_picker_entries(
|
||||
@@ -614,54 +522,6 @@ fn recent_context_picker_entries(
|
||||
recent
|
||||
}
|
||||
|
||||
fn add_selections_as_context(
|
||||
context_store: &Entity<ContextStore>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let selection_ranges = selection_ranges(workspace, cx);
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
for (buffer, range) in selection_ranges {
|
||||
context_store
|
||||
.add_selection(buffer, range, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn selection_ranges(
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
|
||||
let Some(editor) = workspace
|
||||
.read(cx)
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let selections = editor.selections.all_adjusted(cx);
|
||||
|
||||
let buffer = editor.buffer().clone().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
|
||||
selections
|
||||
.into_iter()
|
||||
.map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
|
||||
.flat_map(|range| {
|
||||
let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
|
||||
let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
|
||||
if start_buffer != end_buffer {
|
||||
return None;
|
||||
}
|
||||
Some((start_buffer, start..end))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn insert_fold_for_mention(
|
||||
excerpt_id: ExcerptId,
|
||||
crease_start: text::Anchor,
|
||||
@@ -681,11 +541,24 @@ pub(crate) fn insert_fold_for_mention(
|
||||
let start = start.bias_right(&snapshot);
|
||||
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
||||
|
||||
let crease = crease_for_mention(
|
||||
crease_label,
|
||||
crease_icon_path,
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
crease_icon_path,
|
||||
crease_label,
|
||||
editor_entity.downgrade(),
|
||||
),
|
||||
merge_adjacent: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
||||
|
||||
let crease = Crease::inline(
|
||||
start..end,
|
||||
editor_entity.downgrade(),
|
||||
placeholder.clone(),
|
||||
fold_toggle("mention"),
|
||||
render_trailer,
|
||||
);
|
||||
|
||||
editor.display_map.update(cx, |display_map, cx| {
|
||||
@@ -694,29 +567,6 @@ pub(crate) fn insert_fold_for_mention(
|
||||
});
|
||||
}
|
||||
|
||||
pub fn crease_for_mention(
|
||||
label: SharedString,
|
||||
icon_path: SharedString,
|
||||
range: Range<Anchor>,
|
||||
editor_entity: WeakEntity<Editor>,
|
||||
) -> Crease<Anchor> {
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(icon_path, label, editor_entity),
|
||||
merge_adjacent: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
||||
|
||||
let crease = Crease::inline(
|
||||
range,
|
||||
placeholder.clone(),
|
||||
fold_toggle("mention"),
|
||||
render_trailer,
|
||||
);
|
||||
crease
|
||||
}
|
||||
|
||||
fn render_fold_icon_button(
|
||||
icon_path: SharedString,
|
||||
label: SharedString,
|
||||
@@ -805,7 +655,6 @@ fn fold_toggle(
|
||||
pub enum MentionLink {
|
||||
File(ProjectPath, Entry),
|
||||
Symbol(ProjectPath, String),
|
||||
Selection(ProjectPath, Range<usize>),
|
||||
Fetch(String),
|
||||
Thread(ThreadId),
|
||||
Rules(UserPromptId),
|
||||
@@ -814,7 +663,6 @@ pub enum MentionLink {
|
||||
impl MentionLink {
|
||||
const FILE: &str = "@file";
|
||||
const SYMBOL: &str = "@symbol";
|
||||
const SELECTION: &str = "@selection";
|
||||
const THREAD: &str = "@thread";
|
||||
const FETCH: &str = "@fetch";
|
||||
const RULES: &str = "@rules";
|
||||
@@ -824,9 +672,8 @@ impl MentionLink {
|
||||
pub fn is_valid(url: &str) -> bool {
|
||||
url.starts_with(Self::FILE)
|
||||
|| url.starts_with(Self::SYMBOL)
|
||||
|| url.starts_with(Self::FETCH)
|
||||
|| url.starts_with(Self::SELECTION)
|
||||
|| url.starts_with(Self::THREAD)
|
||||
|| url.starts_with(Self::FETCH)
|
||||
|| url.starts_with(Self::RULES)
|
||||
}
|
||||
|
||||
@@ -844,19 +691,6 @@ impl MentionLink {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
|
||||
format!(
|
||||
"[@{} ({}-{})]({}:{}:{}-{})",
|
||||
file_name,
|
||||
line_range.start,
|
||||
line_range.end,
|
||||
Self::SELECTION,
|
||||
full_path,
|
||||
line_range.start,
|
||||
line_range.end
|
||||
)
|
||||
}
|
||||
|
||||
pub fn for_thread(thread: &ThreadContextEntry) -> String {
|
||||
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
|
||||
}
|
||||
@@ -905,20 +739,6 @@ impl MentionLink {
|
||||
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||
Some(MentionLink::Symbol(project_path, symbol.to_string()))
|
||||
}
|
||||
Self::SELECTION => {
|
||||
let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
|
||||
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||
|
||||
let line_range = {
|
||||
let (start, end) = line_args
|
||||
.trim_start_matches('(')
|
||||
.trim_end_matches(')')
|
||||
.split_once('-')?;
|
||||
start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
|
||||
};
|
||||
|
||||
Some(MentionLink::Selection(project_path, line_range))
|
||||
}
|
||||
Self::THREAD => {
|
||||
let thread_id = ThreadId::from(argument);
|
||||
Some(MentionLink::Thread(thread_id))
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::Result;
|
||||
use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use file_icons::FileIcons;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use http_client::HttpClientWithUrl;
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
|
||||
use prompt_store::PromptId;
|
||||
use rope::Point;
|
||||
use text::{Anchor, OffsetRangeExt, ToPoint};
|
||||
use text::{Anchor, ToPoint};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -33,8 +32,8 @@ use super::rules_context_picker::{RulesContextEntry, search_rules};
|
||||
use super::symbol_context_picker::SymbolMatch;
|
||||
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
|
||||
use super::{
|
||||
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
|
||||
available_context_picker_entries, recent_context_picker_entries, selection_ranges,
|
||||
ContextPickerMode, MentionLink, RecentEntry, recent_context_picker_entries,
|
||||
supported_context_picker_modes,
|
||||
};
|
||||
|
||||
pub(crate) enum Match {
|
||||
@@ -43,19 +42,19 @@ pub(crate) enum Match {
|
||||
Thread(ThreadMatch),
|
||||
Fetch(SharedString),
|
||||
Rules(RulesContextEntry),
|
||||
Entry(EntryMatch),
|
||||
Mode(ModeMatch),
|
||||
}
|
||||
|
||||
pub struct EntryMatch {
|
||||
pub struct ModeMatch {
|
||||
mat: Option<StringMatch>,
|
||||
entry: ContextPickerEntry,
|
||||
mode: ContextPickerMode,
|
||||
}
|
||||
|
||||
impl Match {
|
||||
pub fn score(&self) -> f64 {
|
||||
match self {
|
||||
Match::File(file) => file.mat.score,
|
||||
Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
|
||||
Match::Mode(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
|
||||
Match::Thread(_) => 1.,
|
||||
Match::Symbol(_) => 1.,
|
||||
Match::Fetch(_) => 1.,
|
||||
@@ -163,14 +162,9 @@ fn search(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
matches.extend(
|
||||
available_context_picker_entries(&thread_store, &workspace, cx)
|
||||
supported_context_picker_modes(&thread_store)
|
||||
.into_iter()
|
||||
.map(|mode| {
|
||||
Match::Entry(EntryMatch {
|
||||
entry: mode,
|
||||
mat: None,
|
||||
})
|
||||
}),
|
||||
.map(|mode| Match::Mode(ModeMatch { mode, mat: None })),
|
||||
);
|
||||
|
||||
Task::ready(matches)
|
||||
@@ -180,11 +174,11 @@ fn search(
|
||||
let search_files_task =
|
||||
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
|
||||
let entries = available_context_picker_entries(&thread_store, &workspace, cx);
|
||||
let entry_candidates = entries
|
||||
let modes = supported_context_picker_modes(&thread_store);
|
||||
let mode_candidates = modes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
|
||||
.map(|(ix, mode)| StringMatchCandidate::new(ix, mode.mention_prefix()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
@@ -194,8 +188,8 @@ fn search(
|
||||
.map(Match::File)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let entry_matches = fuzzy::match_strings(
|
||||
&entry_candidates,
|
||||
let mode_matches = fuzzy::match_strings(
|
||||
&mode_candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
@@ -204,9 +198,9 @@ fn search(
|
||||
)
|
||||
.await;
|
||||
|
||||
matches.extend(entry_matches.into_iter().map(|mat| {
|
||||
Match::Entry(EntryMatch {
|
||||
entry: entries[mat.candidate_id],
|
||||
matches.extend(mode_matches.into_iter().map(|mat| {
|
||||
Match::Mode(ModeMatch {
|
||||
mode: modes[mat.candidate_id],
|
||||
mat: Some(mat),
|
||||
})
|
||||
}));
|
||||
@@ -246,137 +240,19 @@ impl ContextPickerCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_for_entry(
|
||||
entry: ContextPickerEntry,
|
||||
excerpt_id: ExcerptId,
|
||||
source_range: Range<Anchor>,
|
||||
editor: Entity<Editor>,
|
||||
context_store: Entity<ContextStore>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Option<Completion> {
|
||||
match entry {
|
||||
ContextPickerEntry::Mode(mode) => Some(Completion {
|
||||
replace_range: source_range.clone(),
|
||||
new_text: format!("@{} ", mode.keyword()),
|
||||
label: CodeLabel::plain(mode.label().to_string(), None),
|
||||
icon_path: Some(mode.icon().path().into()),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
// This ensures that when a user accepts this completion, the
|
||||
// completion menu will still be shown after "@category " is
|
||||
// inserted
|
||||
confirm: Some(Arc::new(|_, _, _| true)),
|
||||
}),
|
||||
ContextPickerEntry::Action(action) => {
|
||||
let (new_text, on_action) = match action {
|
||||
ContextPickerAction::AddSelections => {
|
||||
let selections = selection_ranges(workspace, cx);
|
||||
|
||||
let selection_infos = selections
|
||||
.iter()
|
||||
.map(|(buffer, range)| {
|
||||
let full_path = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| file.full_path(cx))
|
||||
.unwrap_or_else(|| PathBuf::from("untitled"));
|
||||
let file_name = full_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let line_range = range.to_point(&buffer.read(cx).snapshot());
|
||||
|
||||
let link = MentionLink::for_selection(
|
||||
&file_name,
|
||||
&full_path.to_string_lossy(),
|
||||
line_range.start.row as usize..line_range.end.row as usize,
|
||||
);
|
||||
(file_name, link, line_range)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let new_text = selection_infos.iter().map(|(_, link, _)| link).join(" ");
|
||||
|
||||
let callback = Arc::new({
|
||||
let context_store = context_store.clone();
|
||||
let selections = selections.clone();
|
||||
let selection_infos = selection_infos.clone();
|
||||
move |_, _: &mut Window, cx: &mut App| {
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
for (buffer, range) in &selections {
|
||||
context_store
|
||||
.add_selection(buffer.clone(), range.clone(), cx)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
});
|
||||
|
||||
let editor = editor.clone();
|
||||
let selection_infos = selection_infos.clone();
|
||||
cx.defer(move |cx| {
|
||||
let mut current_offset = 0;
|
||||
for (file_name, link, line_range) in selection_infos.iter() {
|
||||
let snapshot =
|
||||
editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let Some(start) = snapshot
|
||||
.anchor_in_excerpt(excerpt_id, source_range.start)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let offset = start.to_offset(&snapshot) + current_offset;
|
||||
let text_len = link.len();
|
||||
|
||||
let range = snapshot.anchor_after(offset)
|
||||
..snapshot.anchor_after(offset + text_len);
|
||||
|
||||
let crease = super::crease_for_mention(
|
||||
format!(
|
||||
"{} ({}-{})",
|
||||
file_name,
|
||||
line_range.start.row + 1,
|
||||
line_range.end.row + 1
|
||||
)
|
||||
.into(),
|
||||
IconName::Context.path().into(),
|
||||
range,
|
||||
editor.downgrade(),
|
||||
);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.display_map.update(cx, |display_map, cx| {
|
||||
display_map.fold(vec![crease], cx);
|
||||
});
|
||||
});
|
||||
|
||||
current_offset += text_len + 1;
|
||||
}
|
||||
});
|
||||
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
(new_text, callback)
|
||||
}
|
||||
};
|
||||
|
||||
Some(Completion {
|
||||
replace_range: source_range.clone(),
|
||||
new_text,
|
||||
label: CodeLabel::plain(action.label().to_string(), None),
|
||||
icon_path: Some(action.icon().path().into()),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
// This ensures that when a user accepts this completion, the
|
||||
// completion menu will still be shown after "@category " is
|
||||
// inserted
|
||||
confirm: Some(on_action),
|
||||
})
|
||||
}
|
||||
fn completion_for_mode(source_range: Range<Anchor>, mode: ContextPickerMode) -> Completion {
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
new_text: format!("@{} ", mode.mention_prefix()),
|
||||
label: CodeLabel::plain(mode.label().to_string(), None),
|
||||
icon_path: Some(mode.icon().path().into()),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
// This ensures that when a user accepts this completion, the
|
||||
// completion menu will still be shown after "@category " is
|
||||
// inserted
|
||||
confirm: Some(Arc::new(|_, _, _| true)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,15 +686,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
context_store.clone(),
|
||||
http_client.clone(),
|
||||
)),
|
||||
Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
|
||||
entry,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
&workspace,
|
||||
cx,
|
||||
),
|
||||
Match::Mode(ModeMatch { mode, .. }) => {
|
||||
Some(Self::completion_for_mode(source_range.clone(), mode))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})?))
|
||||
|
||||
@@ -6,10 +6,9 @@ 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 _, Context, Entity, Image, SharedString, Task, WeakEntity};
|
||||
use language::Buffer;
|
||||
use language_model::LanguageModelImage;
|
||||
use project::{Project, ProjectEntryId, ProjectItem, ProjectPath, Worktree};
|
||||
use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
|
||||
use language::{Buffer, File};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::UserPromptId;
|
||||
use rope::{Point, Rope};
|
||||
use text::{Anchor, BufferId, OffsetRangeExt};
|
||||
@@ -18,8 +17,7 @@ use util::{ResultExt as _, maybe};
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{
|
||||
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
|
||||
FetchedUrlContext, FileContext, ImageContext, RulesContext, SelectionContext, SymbolContext,
|
||||
ThreadContext,
|
||||
ExcerptContext, FetchedUrlContext, FileContext, RulesContext, SymbolContext, ThreadContext,
|
||||
};
|
||||
use crate::context_strip::SuggestedContext;
|
||||
use crate::thread::{Thread, ThreadId};
|
||||
@@ -114,12 +112,13 @@ impl ContextStore {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
let context_buffer = this
|
||||
.update(cx, |_, cx| load_context_buffer(buffer, cx))??
|
||||
.await;
|
||||
let (buffer_info, text_task) =
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_file(context_buffer, cx);
|
||||
this.insert_file(make_context_buffer(buffer_info, text), cx);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -132,11 +131,14 @@ impl ContextStore {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let context_buffer = this
|
||||
.update(cx, |_, cx| load_context_buffer(buffer, cx))??
|
||||
.await;
|
||||
let (buffer_info, text_task) =
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
|
||||
|
||||
this.update(cx, |this, cx| this.insert_file(context_buffer, cx))?;
|
||||
let text = text_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_file(make_context_buffer(buffer_info, text), cx)
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
@@ -160,14 +162,6 @@ impl ContextStore {
|
||||
return Task::ready(Err(anyhow!("failed to read project")));
|
||||
};
|
||||
|
||||
let Some(entry_id) = project
|
||||
.read(cx)
|
||||
.entry_for_path(&project_path, cx)
|
||||
.map(|entry| entry.id)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("no entry found for directory context")));
|
||||
};
|
||||
|
||||
let already_included = match self.includes_directory(&project_path) {
|
||||
Some(FileInclusion::Direct(context_id)) => {
|
||||
if remove_if_exists {
|
||||
@@ -209,15 +203,27 @@ impl ContextStore {
|
||||
|
||||
let buffers = open_buffers_task.await;
|
||||
|
||||
let context_buffer_tasks = this.update(cx, |_, cx| {
|
||||
buffers
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(move |buffer| load_context_buffer(buffer, cx).log_err())
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
let mut buffer_infos = Vec::new();
|
||||
let mut text_tasks = Vec::new();
|
||||
this.update(cx, |_, cx| {
|
||||
// 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, cx).log_err()
|
||||
{
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
let context_buffers = future::join_all(context_buffer_tasks).await;
|
||||
let buffer_texts = future::join_all(text_tasks).await;
|
||||
let context_buffers = buffer_infos
|
||||
.into_iter()
|
||||
.zip(buffer_texts)
|
||||
.map(|(info, text)| make_context_buffer(info, text))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if context_buffers.is_empty() {
|
||||
let full_path = cx.update(|cx| worktree.read(cx).full_path(&project_path.path))?;
|
||||
@@ -225,7 +231,7 @@ impl ContextStore {
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_directory(worktree, entry_id, project_path, context_buffers, cx);
|
||||
this.insert_directory(worktree, project_path, context_buffers, cx);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -235,21 +241,19 @@ impl ContextStore {
|
||||
fn insert_directory(
|
||||
&mut self,
|
||||
worktree: Entity<Worktree>,
|
||||
entry_id: ProjectEntryId,
|
||||
project_path: ProjectPath,
|
||||
context_buffers: Vec<ContextBuffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
let last_path = project_path.path.clone();
|
||||
let path = project_path.path.clone();
|
||||
self.directories.insert(project_path, id);
|
||||
|
||||
self.context
|
||||
.push(AssistantContext::Directory(DirectoryContext {
|
||||
id,
|
||||
worktree,
|
||||
entry_id,
|
||||
last_path,
|
||||
path,
|
||||
context_buffers,
|
||||
}));
|
||||
cx.notify();
|
||||
@@ -289,23 +293,27 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
let context_buffer_task =
|
||||
match load_context_buffer_range(buffer, symbol_enclosing_range.clone(), cx) {
|
||||
Ok((_line_range, context_buffer_task)) => context_buffer_task,
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let (buffer_info, collect_content_task) = match collect_buffer_info_and_text_for_range(
|
||||
buffer,
|
||||
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 context_buffer = context_buffer_task.await;
|
||||
let content = collect_content_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_symbol(
|
||||
make_context_symbol(
|
||||
context_buffer,
|
||||
buffer_info,
|
||||
project_path,
|
||||
symbol_name,
|
||||
symbol_range,
|
||||
symbol_enclosing_range,
|
||||
content,
|
||||
),
|
||||
cx,
|
||||
)
|
||||
@@ -450,54 +458,33 @@ impl ContextStore {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_image(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
|
||||
let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.context.push(AssistantContext::Image(ImageContext {
|
||||
id,
|
||||
original_image: image,
|
||||
image_task,
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn wait_for_images(&self, cx: &App) -> Task<()> {
|
||||
let tasks = self
|
||||
.context
|
||||
.iter()
|
||||
.filter_map(|ctx| match ctx {
|
||||
AssistantContext::Image(ctx) => Some(ctx.image_task.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
join_all(tasks).await;
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_selection(
|
||||
pub fn add_excerpt(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut Context<ContextStore>,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (line_range, context_buffer_task) = this.update(cx, |_, cx| {
|
||||
load_context_buffer_range(buffer, range.clone(), cx)
|
||||
let (line_range, buffer_info, text_task) = this.update(cx, |_, cx| {
|
||||
collect_buffer_info_and_text_for_range(buffer, range.clone(), cx)
|
||||
})??;
|
||||
|
||||
let context_buffer = context_buffer_task.await;
|
||||
let text = text_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_selection(context_buffer, range, line_range, cx)
|
||||
this.insert_excerpt(
|
||||
make_context_buffer(buffer_info, text),
|
||||
range,
|
||||
line_range,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_selection(
|
||||
fn insert_excerpt(
|
||||
&mut self,
|
||||
context_buffer: ContextBuffer,
|
||||
range: Range<Anchor>,
|
||||
@@ -505,13 +492,12 @@ impl ContextStore {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.context
|
||||
.push(AssistantContext::Selection(SelectionContext {
|
||||
id,
|
||||
range,
|
||||
line_range,
|
||||
context_buffer,
|
||||
}));
|
||||
self.context.push(AssistantContext::Excerpt(ExcerptContext {
|
||||
id,
|
||||
range,
|
||||
line_range,
|
||||
context_buffer,
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -564,7 +550,7 @@ impl ContextStore {
|
||||
self.symbol_buffers.remove(&symbol.context_symbol.id);
|
||||
self.symbols.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
AssistantContext::Selection(_) => {}
|
||||
AssistantContext::Excerpt(_) => {}
|
||||
AssistantContext::FetchedUrl(_) => {
|
||||
self.fetched_urls.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
@@ -574,7 +560,6 @@ impl ContextStore {
|
||||
AssistantContext::Rules(RulesContext { prompt_id, .. }) => {
|
||||
self.user_rules.remove(&prompt_id);
|
||||
}
|
||||
AssistantContext::Image(_) => {}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@@ -700,11 +685,10 @@ impl ContextStore {
|
||||
}
|
||||
AssistantContext::Directory(_)
|
||||
| AssistantContext::Symbol(_)
|
||||
| AssistantContext::Selection(_)
|
||||
| AssistantContext::Excerpt(_)
|
||||
| AssistantContext::FetchedUrl(_)
|
||||
| AssistantContext::Thread(_)
|
||||
| AssistantContext::Rules(_)
|
||||
| AssistantContext::Image(_) => None,
|
||||
| AssistantContext::Rules(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -719,78 +703,92 @@ pub enum FileInclusion {
|
||||
InDirectory(ProjectPath),
|
||||
}
|
||||
|
||||
// ContextBuffer without text.
|
||||
struct BufferInfo {
|
||||
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,
|
||||
file: info.file,
|
||||
version: info.version,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_context_symbol(
|
||||
context_buffer: ContextBuffer,
|
||||
info: BufferInfo,
|
||||
path: ProjectPath,
|
||||
name: SharedString,
|
||||
range: Range<Anchor>,
|
||||
enclosing_range: Range<Anchor>,
|
||||
text: SharedString,
|
||||
) -> ContextSymbol {
|
||||
ContextSymbol {
|
||||
id: ContextSymbolId { name, range, path },
|
||||
buffer_version: context_buffer.version,
|
||||
buffer_version: info.version,
|
||||
enclosing_range,
|
||||
buffer: context_buffer.buffer,
|
||||
text: context_buffer.text,
|
||||
buffer: info.buffer,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_context_buffer_range(
|
||||
fn collect_buffer_info_and_text_for_range(
|
||||
buffer: Entity<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
cx: &App,
|
||||
) -> Result<(Range<Point>, Task<ContextBuffer>)> {
|
||||
let buffer_ref = buffer.read(cx);
|
||||
let id = buffer_ref.remote_id();
|
||||
) -> Result<(Range<Point>, BufferInfo, Task<SharedString>)> {
|
||||
let content = buffer
|
||||
.read(cx)
|
||||
.text_for_range(range.clone())
|
||||
.collect::<Rope>();
|
||||
|
||||
let file = buffer_ref.file().context("context buffer missing path")?;
|
||||
let full_path = file.full_path(cx);
|
||||
let line_range = range.to_point(&buffer.read(cx).snapshot());
|
||||
|
||||
// Important to collect version at the same time as content so that staleness logic is correct.
|
||||
let version = buffer_ref.version();
|
||||
let content = buffer_ref.text_for_range(range.clone()).collect::<Rope>();
|
||||
let line_range = range.to_point(&buffer_ref.snapshot());
|
||||
let buffer_info = collect_buffer_info(buffer, cx)?;
|
||||
let full_path = buffer_info.file.full_path(cx);
|
||||
|
||||
// Build the text on a background thread.
|
||||
let task = cx.background_spawn({
|
||||
let text_task = cx.background_spawn({
|
||||
let line_range = line_range.clone();
|
||||
async move {
|
||||
let text = to_fenced_codeblock(&full_path, content, Some(line_range));
|
||||
ContextBuffer {
|
||||
id,
|
||||
buffer,
|
||||
last_full_path: full_path.into(),
|
||||
version,
|
||||
text,
|
||||
}
|
||||
}
|
||||
async move { to_fenced_codeblock(&full_path, content, Some(line_range)) }
|
||||
});
|
||||
|
||||
Ok((line_range, task))
|
||||
Ok((line_range, buffer_info, text_task))
|
||||
}
|
||||
|
||||
fn load_context_buffer(buffer: Entity<Buffer>, cx: &App) -> Result<Task<ContextBuffer>> {
|
||||
let buffer_ref = buffer.read(cx);
|
||||
let id = buffer_ref.remote_id();
|
||||
fn collect_buffer_info_and_text(
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &App,
|
||||
) -> Result<(BufferInfo, Task<SharedString>)> {
|
||||
let content = buffer.read(cx).as_rope().clone();
|
||||
|
||||
let file = buffer_ref.file().context("context buffer missing path")?;
|
||||
let full_path = file.full_path(cx);
|
||||
let buffer_info = collect_buffer_info(buffer, cx)?;
|
||||
let full_path = buffer_info.file.full_path(cx);
|
||||
|
||||
let text_task =
|
||||
cx.background_spawn(async move { to_fenced_codeblock(&full_path, content, None) });
|
||||
|
||||
Ok((buffer_info, text_task))
|
||||
}
|
||||
|
||||
fn collect_buffer_info(buffer: Entity<Buffer>, cx: &App) -> Result<BufferInfo> {
|
||||
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 = buffer_ref.as_rope().clone();
|
||||
|
||||
// Build the text on a background thread.
|
||||
Ok(cx.background_spawn(async move {
|
||||
let text = to_fenced_codeblock(&full_path, content, None);
|
||||
ContextBuffer {
|
||||
id,
|
||||
buffer,
|
||||
last_full_path: full_path.into(),
|
||||
version,
|
||||
text,
|
||||
}
|
||||
}))
|
||||
Ok(BufferInfo {
|
||||
buffer,
|
||||
id: buffer_ref.remote_id(),
|
||||
file: file.clone(),
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
fn to_fenced_codeblock(
|
||||
@@ -877,7 +875,6 @@ pub fn refresh_context_store_text(
|
||||
let task = maybe!({
|
||||
match context {
|
||||
AssistantContext::File(file_context) => {
|
||||
// TODO: Should refresh if the path has changed, as it's in the text.
|
||||
if changed_buffers.is_empty()
|
||||
|| changed_buffers.contains(&file_context.context_buffer.buffer)
|
||||
{
|
||||
@@ -886,9 +883,8 @@ pub fn refresh_context_store_text(
|
||||
}
|
||||
}
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
let directory_path = directory_context.project_path(cx)?;
|
||||
let should_refresh = directory_path.path != directory_context.last_path
|
||||
|| changed_buffers.is_empty()
|
||||
let directory_path = directory_context.project_path(cx);
|
||||
let should_refresh = changed_buffers.is_empty()
|
||||
|| changed_buffers.iter().any(|buffer| {
|
||||
let Some(buffer_path) = buffer.read(cx).project_path(cx) else {
|
||||
return false;
|
||||
@@ -898,16 +894,10 @@ pub fn refresh_context_store_text(
|
||||
|
||||
if should_refresh {
|
||||
let context_store = context_store.clone();
|
||||
return refresh_directory_text(
|
||||
context_store,
|
||||
directory_context,
|
||||
directory_path,
|
||||
cx,
|
||||
);
|
||||
return refresh_directory_text(context_store, directory_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Symbol(symbol_context) => {
|
||||
// TODO: Should refresh if the path has changed, as it's in the text.
|
||||
if changed_buffers.is_empty()
|
||||
|| changed_buffers.contains(&symbol_context.context_symbol.buffer)
|
||||
{
|
||||
@@ -915,13 +905,12 @@ pub fn refresh_context_store_text(
|
||||
return refresh_symbol_text(context_store, symbol_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Selection(selection_context) => {
|
||||
// TODO: Should refresh if the path has changed, as it's in the text.
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
if changed_buffers.is_empty()
|
||||
|| changed_buffers.contains(&selection_context.context_buffer.buffer)
|
||||
|| changed_buffers.contains(&excerpt_context.context_buffer.buffer)
|
||||
{
|
||||
let context_store = context_store.clone();
|
||||
return refresh_selection_text(context_store, selection_context, cx);
|
||||
return refresh_excerpt_text(context_store, excerpt_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Thread(thread_context) => {
|
||||
@@ -938,7 +927,6 @@ pub fn refresh_context_store_text(
|
||||
let context_store = context_store.clone();
|
||||
return Some(refresh_user_rules(context_store, user_rules_context, cx));
|
||||
}
|
||||
AssistantContext::Image(_) => {}
|
||||
}
|
||||
|
||||
None
|
||||
@@ -977,7 +965,6 @@ fn refresh_file_text(
|
||||
fn refresh_directory_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
directory_context: &DirectoryContext,
|
||||
directory_path: ProjectPath,
|
||||
cx: &App,
|
||||
) -> Option<Task<()>> {
|
||||
let mut stale = false;
|
||||
@@ -1002,8 +989,7 @@ fn refresh_directory_text(
|
||||
|
||||
let id = directory_context.id;
|
||||
let worktree = directory_context.worktree.clone();
|
||||
let entry_id = directory_context.entry_id;
|
||||
let last_path = directory_path.path;
|
||||
let path = directory_context.path.clone();
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let context_buffers = context_buffers.await;
|
||||
context_store
|
||||
@@ -1011,8 +997,7 @@ fn refresh_directory_text(
|
||||
let new_directory_context = DirectoryContext {
|
||||
id,
|
||||
worktree,
|
||||
entry_id,
|
||||
last_path,
|
||||
path,
|
||||
context_buffers,
|
||||
};
|
||||
context_store.replace_context(AssistantContext::Directory(new_directory_context));
|
||||
@@ -1043,27 +1028,26 @@ fn refresh_symbol_text(
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_selection_text(
|
||||
fn refresh_excerpt_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
selection_context: &SelectionContext,
|
||||
excerpt_context: &ExcerptContext,
|
||||
cx: &App,
|
||||
) -> Option<Task<()>> {
|
||||
let id = selection_context.id;
|
||||
let range = selection_context.range.clone();
|
||||
let task = refresh_context_excerpt(&selection_context.context_buffer, range.clone(), cx);
|
||||
let id = excerpt_context.id;
|
||||
let range = excerpt_context.range.clone();
|
||||
let task = refresh_context_excerpt(&excerpt_context.context_buffer, range.clone(), cx);
|
||||
if let Some(task) = task {
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let (line_range, context_buffer) = task.await;
|
||||
context_store
|
||||
.update(cx, |context_store, _| {
|
||||
let new_selection_context = SelectionContext {
|
||||
let new_excerpt_context = ExcerptContext {
|
||||
id,
|
||||
range,
|
||||
line_range,
|
||||
context_buffer,
|
||||
};
|
||||
context_store
|
||||
.replace_context(AssistantContext::Selection(new_selection_context));
|
||||
context_store.replace_context(AssistantContext::Excerpt(new_excerpt_context));
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
@@ -1132,10 +1116,15 @@ fn refresh_user_rules(
|
||||
})
|
||||
}
|
||||
|
||||
fn refresh_context_buffer(context_buffer: &ContextBuffer, cx: &App) -> Option<Task<ContextBuffer>> {
|
||||
fn refresh_context_buffer(
|
||||
context_buffer: &ContextBuffer,
|
||||
cx: &App,
|
||||
) -> Option<impl Future<Output = ContextBuffer> + use<>> {
|
||||
let buffer = context_buffer.buffer.read(cx);
|
||||
if buffer.version.changed_since(&context_buffer.version) {
|
||||
load_context_buffer(context_buffer.buffer.clone(), cx).log_err()
|
||||
let (buffer_info, text_task) =
|
||||
collect_buffer_info_and_text(context_buffer.buffer.clone(), cx).log_err()?;
|
||||
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -1148,9 +1137,10 @@ fn refresh_context_excerpt(
|
||||
) -> Option<impl Future<Output = (Range<Point>, ContextBuffer)> + use<>> {
|
||||
let buffer = context_buffer.buffer.read(cx);
|
||||
if buffer.version.changed_since(&context_buffer.version) {
|
||||
let (line_range, context_buffer_task) =
|
||||
load_context_buffer_range(context_buffer.buffer.clone(), range, cx).log_err()?;
|
||||
Some(context_buffer_task.map(move |context_buffer| (line_range, context_buffer)))
|
||||
let (line_range, buffer_info, text_task) =
|
||||
collect_buffer_info_and_text_for_range(context_buffer.buffer.clone(), range, cx)
|
||||
.log_err()?;
|
||||
Some(text_task.map(move |text| (line_range, make_context_buffer(buffer_info, text))))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -1163,7 +1153,7 @@ fn refresh_context_symbol(
|
||||
let buffer = context_symbol.buffer.read(cx);
|
||||
let project_path = buffer.project_path(cx)?;
|
||||
if buffer.version.changed_since(&context_symbol.buffer_version) {
|
||||
let (_line_range, context_buffer_task) = load_context_buffer_range(
|
||||
let (_, buffer_info, text_task) = collect_buffer_info_and_text_for_range(
|
||||
context_symbol.buffer.clone(),
|
||||
context_symbol.enclosing_range.clone(),
|
||||
cx,
|
||||
@@ -1172,8 +1162,15 @@ fn refresh_context_symbol(
|
||||
let name = context_symbol.id.name.clone();
|
||||
let range = context_symbol.id.range.clone();
|
||||
let enclosing_range = context_symbol.enclosing_range.clone();
|
||||
Some(context_buffer_task.map(move |context_buffer| {
|
||||
make_context_symbol(context_buffer, project_path, name, range, enclosing_range)
|
||||
Some(text_task.map(move |text| {
|
||||
make_context_symbol(
|
||||
buffer_info,
|
||||
project_path,
|
||||
name,
|
||||
range,
|
||||
enclosing_range,
|
||||
text,
|
||||
)
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -1328,7 +1328,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<InlineAssist>(
|
||||
row_range,
|
||||
cx.theme().status().info_background,
|
||||
Default::default(),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1393,7 +1393,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
cx.theme().status().deleted_background,
|
||||
Default::default(),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::context::{AssistantContext, format_context_as_string};
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashSet;
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
use editor::actions::MoveUp;
|
||||
use editor::{
|
||||
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode,
|
||||
EditorStyle, MultiBuffer,
|
||||
@@ -14,8 +14,8 @@ use editor::{
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, ClipboardEntry, Entity, EventEmitter, Focusable, Subscription,
|
||||
Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
|
||||
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage};
|
||||
@@ -195,7 +195,6 @@ impl MessageEditor {
|
||||
editor.set_mode(EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: false,
|
||||
})
|
||||
} else {
|
||||
editor.set_mode(EditorMode::AutoHeight {
|
||||
@@ -272,18 +271,15 @@ impl MessageEditor {
|
||||
|
||||
let refresh_task =
|
||||
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
|
||||
let wait_for_images = self.context_store.read(cx).wait_for_images(cx);
|
||||
|
||||
let thread = self.thread.clone();
|
||||
let context_store = self.context_store.clone();
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
|
||||
let window_handle = window.window_handle();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let checkpoint = checkpoint.await.ok();
|
||||
refresh_task.await;
|
||||
wait_for_images.await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
@@ -297,12 +293,7 @@ impl MessageEditor {
|
||||
let excerpt_ids = context_store
|
||||
.context()
|
||||
.iter()
|
||||
.filter(|ctx| {
|
||||
matches!(
|
||||
ctx,
|
||||
AssistantContext::Selection(_) | AssistantContext::Image(_)
|
||||
)
|
||||
})
|
||||
.filter(|ctx| matches!(ctx, AssistantContext::Excerpt(_)))
|
||||
.map(|ctx| ctx.id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -335,7 +326,7 @@ impl MessageEditor {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model, Some(window_handle), cx);
|
||||
thread.send_to_model(model, cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
@@ -343,9 +334,9 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let cancelled = self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_last_completion(Some(window.window_handle()), cx)
|
||||
});
|
||||
let cancelled = self
|
||||
.thread
|
||||
.update(cx, |thread, cx| thread.cancel_last_completion(cx));
|
||||
|
||||
if cancelled {
|
||||
self.set_editor_is_expanded(false, cx);
|
||||
@@ -379,38 +370,8 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let images = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| {
|
||||
item.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::Image(image) = entry {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if images.is_empty() {
|
||||
return;
|
||||
}
|
||||
cx.stop_propagation();
|
||||
|
||||
self.context_store.update(cx, |store, cx| {
|
||||
for image in images {
|
||||
store.add_image(Arc::new(image), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.edits_expanded = true;
|
||||
fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_file_click(
|
||||
@@ -484,7 +445,6 @@ impl MessageEditor {
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::toggle_chat_mode))
|
||||
.on_action(cx.listener(Self::expand_message_editor))
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(editor_bg_color)
|
||||
|
||||
@@ -13,12 +13,10 @@ use feature_flags::{self, FeatureFlagAppExt};
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt, StreamExt as _};
|
||||
use git::repository::DiffType;
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
|
||||
};
|
||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
|
||||
LanguageModelImage, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, StopReason,
|
||||
@@ -40,7 +38,7 @@ use crate::thread_store::{
|
||||
SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult,
|
||||
SerializedToolUse, SharedProjectContext,
|
||||
};
|
||||
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState};
|
||||
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState, USING_TOOL_MARKER};
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,
|
||||
@@ -99,7 +97,6 @@ pub struct Message {
|
||||
pub role: Role,
|
||||
pub segments: Vec<MessageSegment>,
|
||||
pub context: String,
|
||||
pub images: Vec<LanguageModelImage>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
@@ -170,9 +167,12 @@ pub enum MessageSegment {
|
||||
|
||||
impl MessageSegment {
|
||||
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(),
|
||||
Self::Thinking { text, .. } => text.is_empty(),
|
||||
Self::Text(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER,
|
||||
Self::Thinking { text, .. } => text.is_empty() || text.trim() == USING_TOOL_MARKER,
|
||||
Self::RedactedThinking(_) => false,
|
||||
}
|
||||
}
|
||||
@@ -317,7 +317,6 @@ pub struct Thread {
|
||||
request_callback: Option<
|
||||
Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>,
|
||||
>,
|
||||
remaining_turns: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -371,7 +370,6 @@ impl Thread {
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,7 +418,6 @@ impl Thread {
|
||||
})
|
||||
.collect(),
|
||||
context: message.context,
|
||||
images: Vec::new(),
|
||||
})
|
||||
.collect(),
|
||||
next_message_id,
|
||||
@@ -446,7 +443,6 @@ impl Thread {
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,7 +523,7 @@ impl Thread {
|
||||
self.messages.iter().find(|message| message.id == id)
|
||||
}
|
||||
|
||||
pub fn messages(&self) -> impl ExactSizeIterator<Item = &Message> {
|
||||
pub fn messages(&self) -> impl Iterator<Item = &Message> {
|
||||
self.messages.iter()
|
||||
}
|
||||
|
||||
@@ -621,12 +617,24 @@ impl Thread {
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
if !equal {
|
||||
if equal {
|
||||
git_store
|
||||
.update(cx, |store, cx| {
|
||||
store.delete_checkpoint(pending_checkpoint.git_checkpoint, cx)
|
||||
})?
|
||||
.detach();
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_checkpoint(pending_checkpoint, cx)
|
||||
})?;
|
||||
}
|
||||
|
||||
git_store
|
||||
.update(cx, |store, cx| {
|
||||
store.delete_checkpoint(final_checkpoint, cx)
|
||||
})?
|
||||
.detach();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => this.update(cx, |this, cx| {
|
||||
@@ -742,41 +750,33 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(message) = self.messages.iter_mut().find(|m| m.id == message_id) {
|
||||
message.images = new_context
|
||||
.iter()
|
||||
.filter_map(|context| {
|
||||
if let AssistantContext::Image(image_context) = context {
|
||||
image_context.image_task.clone().now_or_never().flatten()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
self.action_log.update(cx, |log, cx| {
|
||||
// Track all buffers added as context
|
||||
for ctx in &new_context {
|
||||
match ctx {
|
||||
AssistantContext::File(file_ctx) => {
|
||||
log.track_buffer(file_ctx.context_buffer.buffer.clone(), cx);
|
||||
log.buffer_added_as_context(file_ctx.context_buffer.buffer.clone(), cx);
|
||||
}
|
||||
AssistantContext::Directory(dir_ctx) => {
|
||||
for context_buffer in &dir_ctx.context_buffers {
|
||||
log.track_buffer(context_buffer.buffer.clone(), cx);
|
||||
log.buffer_added_as_context(context_buffer.buffer.clone(), cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Symbol(symbol_ctx) => {
|
||||
log.track_buffer(symbol_ctx.context_symbol.buffer.clone(), cx);
|
||||
log.buffer_added_as_context(
|
||||
symbol_ctx.context_symbol.buffer.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AssistantContext::Selection(selection_context) => {
|
||||
log.track_buffer(selection_context.context_buffer.buffer.clone(), cx);
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
log.buffer_added_as_context(
|
||||
excerpt_context.context_buffer.buffer.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AssistantContext::FetchedUrl(_)
|
||||
| AssistantContext::Thread(_)
|
||||
| AssistantContext::Rules(_)
|
||||
| AssistantContext::Image(_) => {}
|
||||
| AssistantContext::Rules(_) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -817,7 +817,6 @@ impl Thread {
|
||||
role,
|
||||
segments,
|
||||
context: String::new(),
|
||||
images: Vec::new(),
|
||||
});
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageAdded(id));
|
||||
@@ -945,26 +944,7 @@ impl Thread {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remaining_turns(&self) -> u32 {
|
||||
self.remaining_turns
|
||||
}
|
||||
|
||||
pub fn set_remaining_turns(&mut self, remaining_turns: u32) {
|
||||
self.remaining_turns = remaining_turns;
|
||||
}
|
||||
|
||||
pub fn send_to_model(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.remaining_turns == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.remaining_turns -= 1;
|
||||
|
||||
pub fn send_to_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
||||
let mut request = self.to_completion_request(cx);
|
||||
if model.supports_tools() {
|
||||
request.tools = {
|
||||
@@ -989,7 +969,7 @@ impl Thread {
|
||||
};
|
||||
}
|
||||
|
||||
self.stream_completion(request, model, window, cx);
|
||||
self.stream_completion(request, model, cx);
|
||||
}
|
||||
|
||||
pub fn used_tools_since_last_user_message(&self) -> bool {
|
||||
@@ -1060,21 +1040,6 @@ impl Thread {
|
||||
.push(MessageContent::Text(message.context.to_string()));
|
||||
}
|
||||
|
||||
if !message.images.is_empty() {
|
||||
// Some providers only support image parts after an initial text part
|
||||
if request_message.content.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text("Images attached by user:".to_string()));
|
||||
}
|
||||
|
||||
for image in &message.images {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Image(image.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
for segment in &message.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(text) => {
|
||||
@@ -1208,7 +1173,6 @@ impl Thread {
|
||||
&mut self,
|
||||
request: LanguageModelRequest,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let pending_completion_id = post_inc(&mut self.completion_count);
|
||||
@@ -1270,7 +1234,6 @@ impl Thread {
|
||||
current_token_usage = token_usage;
|
||||
}
|
||||
LanguageModelCompletionEvent::Text(chunk) => {
|
||||
cx.emit(ThreadEvent::ReceivedTextChunk);
|
||||
if let Some(last_message) = thread.messages.last_mut() {
|
||||
if last_message.role == Role::Assistant {
|
||||
last_message.push_text(&chunk);
|
||||
@@ -1390,7 +1353,7 @@ impl Thread {
|
||||
match result.as_ref() {
|
||||
Ok(stop_reason) => match stop_reason {
|
||||
StopReason::ToolUse => {
|
||||
let tool_uses = thread.use_pending_tools(window, cx);
|
||||
let tool_uses = thread.use_pending_tools(cx);
|
||||
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
|
||||
}
|
||||
StopReason::EndTurn => {}
|
||||
@@ -1435,7 +1398,7 @@ impl Thread {
|
||||
}));
|
||||
}
|
||||
|
||||
thread.cancel_last_completion(window, cx);
|
||||
thread.cancel_last_completion(cx);
|
||||
}
|
||||
}
|
||||
cx.emit(ThreadEvent::Stopped(result.map_err(Arc::new)));
|
||||
@@ -1604,11 +1567,7 @@ impl Thread {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn use_pending_tools(
|
||||
&mut self,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<PendingToolUse> {
|
||||
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) -> Vec<PendingToolUse> {
|
||||
self.auto_capture_telemetry(cx);
|
||||
let request = self.to_completion_request(cx);
|
||||
let messages = Arc::new(request.messages);
|
||||
@@ -1640,7 +1599,6 @@ impl Thread {
|
||||
tool_use.input.clone(),
|
||||
&messages,
|
||||
tool,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1657,10 +1615,9 @@ impl Thread {
|
||||
input: serde_json::Value,
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
tool: Arc<dyn Tool>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Thread>,
|
||||
) {
|
||||
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, window, cx);
|
||||
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
|
||||
self.tool_use
|
||||
.run_pending_tool(tool_use_id, ui_text.into(), task);
|
||||
}
|
||||
@@ -1671,7 +1628,6 @@ impl Thread {
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
input: serde_json::Value,
|
||||
tool: Arc<dyn Tool>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Thread>,
|
||||
) -> Task<()> {
|
||||
let tool_name: Arc<str> = tool.name().into();
|
||||
@@ -1684,7 +1640,6 @@ impl Thread {
|
||||
messages,
|
||||
self.project.clone(),
|
||||
self.action_log.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
};
|
||||
@@ -1707,7 +1662,7 @@ impl Thread {
|
||||
output,
|
||||
cx,
|
||||
);
|
||||
thread.tool_finished(tool_use_id, pending_tool_use, false, window, cx);
|
||||
thread.tool_finished(tool_use_id, pending_tool_use, false, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -1719,7 +1674,6 @@ impl Thread {
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
pending_tool_use: Option<PendingToolUse>,
|
||||
canceled: bool,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.all_tools_finished() {
|
||||
@@ -1727,7 +1681,7 @@ impl Thread {
|
||||
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
|
||||
self.attach_tool_results(cx);
|
||||
if !canceled {
|
||||
self.send_to_model(model, window, cx);
|
||||
self.send_to_model(model, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1749,11 +1703,7 @@ impl Thread {
|
||||
/// Cancels the last pending completion, if there are any pending.
|
||||
///
|
||||
/// Returns whether a completion was canceled.
|
||||
pub fn cancel_last_completion(
|
||||
&mut self,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
pub fn cancel_last_completion(&mut self, cx: &mut Context<Self>) -> bool {
|
||||
let canceled = if self.pending_completions.pop().is_some() {
|
||||
true
|
||||
} else {
|
||||
@@ -1764,7 +1714,6 @@ impl Thread {
|
||||
pending_tool_use.id.clone(),
|
||||
Some(pending_tool_use),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1834,7 +1783,7 @@ impl Thread {
|
||||
thread_data,
|
||||
final_project_snapshot
|
||||
);
|
||||
client.telemetry().flush_events().await;
|
||||
client.telemetry().flush_events();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -1879,7 +1828,7 @@ impl Thread {
|
||||
thread_data,
|
||||
final_project_snapshot
|
||||
);
|
||||
client.telemetry().flush_events().await;
|
||||
client.telemetry().flush_events();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -2135,7 +2084,7 @@ impl Thread {
|
||||
github_login = github_login
|
||||
);
|
||||
|
||||
client.telemetry().flush_events().await;
|
||||
client.telemetry().flush_events();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2221,7 +2170,6 @@ impl Thread {
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let err = Err(anyhow::anyhow!(
|
||||
@@ -2230,7 +2178,7 @@ impl Thread {
|
||||
|
||||
self.tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), tool_name, err, cx);
|
||||
self.tool_finished(tool_use_id.clone(), None, true, window, cx);
|
||||
self.tool_finished(tool_use_id.clone(), None, true, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2254,7 +2202,6 @@ pub enum ThreadEvent {
|
||||
ShowError(ThreadError),
|
||||
UsageUpdated(RequestUsage),
|
||||
StreamedCompletion,
|
||||
ReceivedTextChunk,
|
||||
StreamedAssistantText(MessageId, String),
|
||||
StreamedAssistantThinking(MessageId, String),
|
||||
StreamedToolUse {
|
||||
|
||||
@@ -27,6 +27,8 @@ pub struct ToolUse {
|
||||
pub needs_confirmation: bool,
|
||||
}
|
||||
|
||||
pub const USING_TOOL_MARKER: &str = "<using_tool>";
|
||||
|
||||
pub struct ToolUseState {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
|
||||
@@ -450,8 +452,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
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
mod agent_notification;
|
||||
mod animated_label;
|
||||
mod context_pill;
|
||||
mod usage_banner;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use animated_label::*;
|
||||
pub use context_pill::*;
|
||||
pub use usage_banner::*;
|
||||
|
||||
@@ -12,7 +12,6 @@ pub struct AgentNotification {
|
||||
title: SharedString,
|
||||
caption: SharedString,
|
||||
icon: IconName,
|
||||
project_name: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl AgentNotification {
|
||||
@@ -20,13 +19,11 @@ impl AgentNotification {
|
||||
title: impl Into<SharedString>,
|
||||
caption: impl Into<SharedString>,
|
||||
icon: IconName,
|
||||
project_name: Option<impl Into<SharedString>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
caption: caption.into(),
|
||||
icon,
|
||||
project_name: project_name.map(|name| name.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,34 +130,11 @@ impl Render for AgentNotification {
|
||||
.child(gradient_overflow()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
div()
|
||||
.relative()
|
||||
.gap_1p5()
|
||||
.text_size(px(12.))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.truncate()
|
||||
.when_some(
|
||||
self.project_name.clone(),
|
||||
|description, project_name| {
|
||||
description.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
div()
|
||||
.max_w_16()
|
||||
.truncate()
|
||||
.child(project_name),
|
||||
)
|
||||
.child(
|
||||
div().size(px(3.)).rounded_full().bg(cx
|
||||
.theme()
|
||||
.colors()
|
||||
.text
|
||||
.opacity(0.5)),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(self.caption.clone())
|
||||
.child(gradient_overflow()),
|
||||
),
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
use gpui::{Animation, AnimationExt, FontWeight, pulsating_between};
|
||||
use std::time::Duration;
|
||||
use ui::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct AnimatedLabel {
|
||||
base: Label,
|
||||
text: SharedString,
|
||||
}
|
||||
|
||||
impl AnimatedLabel {
|
||||
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||
let text = text.into();
|
||||
AnimatedLabel {
|
||||
base: Label::new(text.clone()),
|
||||
text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for AnimatedLabel {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn weight(mut self, weight: FontWeight) -> Self {
|
||||
self.base = self.base.weight(weight);
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.base = self.base.line_height_style(line_height_style);
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.base = self.base.color(color);
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self) -> Self {
|
||||
self.base = self.base.strikethrough();
|
||||
self
|
||||
}
|
||||
|
||||
fn italic(mut self) -> Self {
|
||||
self.base = self.base.italic();
|
||||
self
|
||||
}
|
||||
|
||||
fn alpha(mut self, alpha: f32) -> Self {
|
||||
self.base = self.base.alpha(alpha);
|
||||
self
|
||||
}
|
||||
|
||||
fn underline(mut self) -> Self {
|
||||
self.base = self.base.underline();
|
||||
self
|
||||
}
|
||||
|
||||
fn truncate(mut self) -> Self {
|
||||
self.base = self.base.truncate();
|
||||
self
|
||||
}
|
||||
|
||||
fn single_line(mut self) -> Self {
|
||||
self.base = self.base.single_line();
|
||||
self
|
||||
}
|
||||
|
||||
fn buffer_font(mut self, cx: &App) -> Self {
|
||||
self.base = self.base.buffer_font(cx);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AnimatedLabel {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let text = self.text.clone();
|
||||
|
||||
self.base
|
||||
.color(Color::Muted)
|
||||
.with_animations(
|
||||
"animated-label",
|
||||
vec![
|
||||
Animation::new(Duration::from_secs(1)),
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
],
|
||||
move |mut label, animation_ix, delta| {
|
||||
match animation_ix {
|
||||
0 => {
|
||||
let chars_to_show = (delta * text.len() as f32).ceil() as usize;
|
||||
let text = SharedString::from(text[0..chars_to_show].to_string());
|
||||
label.set_text(text);
|
||||
}
|
||||
1 => match delta {
|
||||
d if d < 0.25 => label.set_text(text.clone()),
|
||||
d if d < 0.5 => label.set_text(format!("{}.", text)),
|
||||
d if d < 0.75 => label.set_text(format!("{}..", text)),
|
||||
_ => label.set_text(format!("{}...", text)),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
label
|
||||
},
|
||||
)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.map_element(|label| label.alpha(delta)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use futures::FutureExt;
|
||||
use gpui::{Animation, AnimationExt as _, Image, MouseButton, pulsating_between};
|
||||
use gpui::{ClickEvent, Task};
|
||||
use language_model::LanguageModelImage;
|
||||
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
|
||||
use gpui::ClickEvent;
|
||||
use gpui::{Animation, AnimationExt as _, pulsating_between};
|
||||
use ui::{IconButtonShape, Tooltip, prelude::*};
|
||||
|
||||
use crate::context::{AssistantContext, ContextId, ContextKind, ImageContext};
|
||||
use crate::context::{AssistantContext, ContextId, ContextKind};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub enum ContextPill {
|
||||
@@ -123,100 +120,74 @@ impl RenderOnce for ContextPill {
|
||||
on_remove,
|
||||
focused,
|
||||
on_click,
|
||||
} => {
|
||||
let status_is_error = matches!(context.status, ContextStatus::Error { .. });
|
||||
|
||||
base_pill
|
||||
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
|
||||
.map(|pill| {
|
||||
if status_is_error {
|
||||
pill.bg(cx.theme().status().error_background)
|
||||
.border_color(cx.theme().status().error_border)
|
||||
} else if *focused {
|
||||
pill.bg(color.element_background)
|
||||
.border_color(color.border_focused)
|
||||
} else {
|
||||
pill.bg(color.element_background)
|
||||
.border_color(color.border.opacity(0.5))
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id("context-data")
|
||||
.gap_1()
|
||||
.child(
|
||||
div().max_w_64().child(
|
||||
Label::new(context.name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.when_some(context.parent.as_ref(), |element, parent_name| {
|
||||
if *dupe_name {
|
||||
element.child(
|
||||
Label::new(parent_name.clone())
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
element
|
||||
}
|
||||
})
|
||||
.when_some(context.tooltip.as_ref(), |element, tooltip| {
|
||||
element.tooltip(Tooltip::text(tooltip.clone()))
|
||||
})
|
||||
.map(|element| match &context.status {
|
||||
ContextStatus::Ready => element
|
||||
.when_some(
|
||||
context.render_preview.as_ref(),
|
||||
|element, render_preview| {
|
||||
element.hoverable_tooltip({
|
||||
let render_preview = render_preview.clone();
|
||||
move |_, cx| {
|
||||
cx.new(|_| ContextPillPreview {
|
||||
render_preview: render_preview.clone(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
.into_any(),
|
||||
ContextStatus::Loading { message } => element
|
||||
.tooltip(ui::Tooltip::text(message.clone()))
|
||||
.with_animation(
|
||||
"pulsating-ctx-pill",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.opacity(delta),
|
||||
)
|
||||
.into_any_element(),
|
||||
ContextStatus::Error { message } => element
|
||||
.tooltip(ui::Tooltip::text(message.clone()))
|
||||
.into_any_element(),
|
||||
} => base_pill
|
||||
.bg(color.element_background)
|
||||
.border_color(if *focused {
|
||||
color.border_focused
|
||||
} else {
|
||||
color.border.opacity(0.5)
|
||||
})
|
||||
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
|
||||
.child(
|
||||
h_flex()
|
||||
.id("context-data")
|
||||
.gap_1()
|
||||
.child(
|
||||
div().max_w_64().child(
|
||||
Label::new(context.name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.when_some(context.parent.as_ref(), |element, parent_name| {
|
||||
if *dupe_name {
|
||||
element.child(
|
||||
Label::new(parent_name.clone())
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
element
|
||||
}
|
||||
})
|
||||
.when_some(context.tooltip.as_ref(), |element, tooltip| {
|
||||
element.tooltip(Tooltip::text(tooltip.clone()))
|
||||
}),
|
||||
)
|
||||
.when_some(on_remove.as_ref(), |element, on_remove| {
|
||||
element.child(
|
||||
IconButton::new(("remove", context.id.0), IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(Tooltip::text("Remove Context"))
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, window, cx| on_remove(event, window, cx)
|
||||
}),
|
||||
)
|
||||
.when_some(on_remove.as_ref(), |element, on_remove| {
|
||||
element.child(
|
||||
IconButton::new(("remove", context.id.0), IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(Tooltip::text("Remove Context"))
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, window, cx| on_remove(event, window, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(on_click.as_ref(), |element, on_click| {
|
||||
let on_click = on_click.clone();
|
||||
})
|
||||
.when_some(on_click.as_ref(), |element, on_click| {
|
||||
let on_click = on_click.clone();
|
||||
element
|
||||
.cursor_pointer()
|
||||
.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
.map(|element| {
|
||||
if context.summarizing {
|
||||
element
|
||||
.cursor_pointer()
|
||||
.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
.tooltip(ui::Tooltip::text("Summarizing..."))
|
||||
.with_animation(
|
||||
"pulsating-ctx-pill",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.opacity(delta),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
element.into_any()
|
||||
}
|
||||
}),
|
||||
ContextPill::Suggested {
|
||||
name,
|
||||
icon_path: _,
|
||||
@@ -227,15 +198,15 @@ impl RenderOnce for ContextPill {
|
||||
.cursor_pointer()
|
||||
.pr_1()
|
||||
.border_dashed()
|
||||
.map(|pill| {
|
||||
if *focused {
|
||||
pill.border_color(color.border_focused)
|
||||
.bg(color.element_background.opacity(0.5))
|
||||
} else {
|
||||
pill.border_color(color.border)
|
||||
}
|
||||
.border_color(if *focused {
|
||||
color.border_focused
|
||||
} else {
|
||||
color.border
|
||||
})
|
||||
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
||||
.when(*focused, |this| {
|
||||
this.bg(color.element_background.opacity(0.5))
|
||||
})
|
||||
.child(
|
||||
div().max_w_64().child(
|
||||
Label::new(name.clone())
|
||||
@@ -256,13 +227,6 @@ impl RenderOnce for ContextPill {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextStatus {
|
||||
Ready,
|
||||
Loading { message: SharedString },
|
||||
Error { message: SharedString },
|
||||
}
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct AddedContext {
|
||||
pub id: ContextId,
|
||||
pub kind: ContextKind,
|
||||
@@ -270,15 +234,14 @@ pub struct AddedContext {
|
||||
pub parent: Option<SharedString>,
|
||||
pub tooltip: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
pub status: ContextStatus,
|
||||
pub render_preview: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
|
||||
pub summarizing: bool,
|
||||
}
|
||||
|
||||
impl AddedContext {
|
||||
pub fn new(context: &AssistantContext, cx: &App) -> AddedContext {
|
||||
match context {
|
||||
AssistantContext::File(file_context) => {
|
||||
let full_path = file_context.context_buffer.full_path(cx);
|
||||
let full_path = file_context.context_buffer.file.full_path(cx);
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
@@ -296,20 +259,15 @@ impl AddedContext {
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
summarizing: false,
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
let worktree = directory_context.worktree.read(cx);
|
||||
// If the directory no longer exists, use its last known path.
|
||||
let full_path = worktree
|
||||
.entry_for_id(directory_context.entry_id)
|
||||
.map_or_else(
|
||||
|| directory_context.last_path.clone(),
|
||||
|entry| worktree.full_path(&entry.path).into(),
|
||||
);
|
||||
let full_path = directory_context
|
||||
.worktree
|
||||
.read(cx)
|
||||
.full_path(&directory_context.path);
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
@@ -327,8 +285,7 @@ impl AddedContext {
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
summarizing: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,12 +296,11 @@ impl AddedContext {
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
summarizing: false,
|
||||
},
|
||||
|
||||
AssistantContext::Selection(selection_context) => {
|
||||
let full_path = selection_context.context_buffer.full_path(cx);
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
let full_path = excerpt_context.context_buffer.file.full_path(cx);
|
||||
let mut full_path_string = full_path.to_string_lossy().into_owned();
|
||||
let mut name = full_path
|
||||
.file_name()
|
||||
@@ -353,8 +309,8 @@ impl AddedContext {
|
||||
|
||||
let line_range_text = format!(
|
||||
" ({}-{})",
|
||||
selection_context.line_range.start.row + 1,
|
||||
selection_context.line_range.end.row + 1
|
||||
excerpt_context.line_range.start.row + 1,
|
||||
excerpt_context.line_range.end.row + 1
|
||||
);
|
||||
|
||||
full_path_string.push_str(&line_range_text);
|
||||
@@ -366,25 +322,13 @@ impl AddedContext {
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
|
||||
AddedContext {
|
||||
id: selection_context.id,
|
||||
kind: ContextKind::Selection,
|
||||
id: excerpt_context.id,
|
||||
kind: ContextKind::File, // Use File icon for excerpts
|
||||
name: name.into(),
|
||||
parent,
|
||||
tooltip: None,
|
||||
tooltip: Some(full_path_string.into()),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: Some(Rc::new({
|
||||
let content = selection_context.context_buffer.text.clone();
|
||||
move |_, cx| {
|
||||
div()
|
||||
.id("context-pill-selection-preview")
|
||||
.overflow_scroll()
|
||||
.max_w_128()
|
||||
.max_h_96()
|
||||
.child(Label::new(content.clone()).buffer_font(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
})),
|
||||
summarizing: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,8 +339,7 @@ impl AddedContext {
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
summarizing: false,
|
||||
},
|
||||
|
||||
AssistantContext::Thread(thread_context) => AddedContext {
|
||||
@@ -406,18 +349,10 @@ impl AddedContext {
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: if thread_context
|
||||
summarizing: thread_context
|
||||
.thread
|
||||
.read(cx)
|
||||
.is_generating_detailed_summary()
|
||||
{
|
||||
ContextStatus::Loading {
|
||||
message: "Summarizing…".into(),
|
||||
}
|
||||
} else {
|
||||
ContextStatus::Ready
|
||||
},
|
||||
render_preview: None,
|
||||
.is_generating_detailed_summary(),
|
||||
},
|
||||
|
||||
AssistantContext::Rules(user_rules_context) => AddedContext {
|
||||
@@ -427,122 +362,8 @@ impl AddedContext {
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
},
|
||||
|
||||
AssistantContext::Image(image_context) => AddedContext {
|
||||
id: image_context.id,
|
||||
kind: ContextKind::Image,
|
||||
name: "Image".into(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: if image_context.is_loading() {
|
||||
ContextStatus::Loading {
|
||||
message: "Loading…".into(),
|
||||
}
|
||||
} else if image_context.is_error() {
|
||||
ContextStatus::Error {
|
||||
message: "Failed to load image".into(),
|
||||
}
|
||||
} else {
|
||||
ContextStatus::Ready
|
||||
},
|
||||
render_preview: Some(Rc::new({
|
||||
let image = image_context.original_image.clone();
|
||||
move |_, _| {
|
||||
gpui::img(image.clone())
|
||||
.max_w_96()
|
||||
.max_h_96()
|
||||
.into_any_element()
|
||||
}
|
||||
})),
|
||||
summarizing: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextPillPreview {
|
||||
render_preview: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||
}
|
||||
|
||||
impl Render for ContextPillPreview {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
tooltip_container(window, cx, move |this, window, cx| {
|
||||
this.occlude()
|
||||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child((self.render_preview)(window, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for AddedContext {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn sort_name() -> &'static str {
|
||||
"AddedContext"
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let image_ready = (
|
||||
"Ready",
|
||||
AddedContext::new(
|
||||
&AssistantContext::Image(ImageContext {
|
||||
id: ContextId(0),
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
);
|
||||
|
||||
let image_loading = (
|
||||
"Loading",
|
||||
AddedContext::new(
|
||||
&AssistantContext::Image(ImageContext {
|
||||
id: ContextId(1),
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: cx
|
||||
.background_spawn(async move {
|
||||
smol::Timer::after(Duration::from_secs(60 * 5)).await;
|
||||
Some(LanguageModelImage::empty())
|
||||
})
|
||||
.shared(),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
);
|
||||
|
||||
let image_error = (
|
||||
"Error",
|
||||
AddedContext::new(
|
||||
&AssistantContext::Image(ImageContext {
|
||||
id: ContextId(2),
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(None).shared(),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
);
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.children(
|
||||
vec![image_ready, image_loading, image_error]
|
||||
.into_iter()
|
||||
.map(|(text, context)| {
|
||||
single_example(
|
||||
text,
|
||||
ContextPill::added(context, false, false, None).into_any_element(),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
use client::zed_urls;
|
||||
use language_model::RequestUsage;
|
||||
use ui::{Banner, ProgressBar, Severity, prelude::*};
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct UsageBanner {
|
||||
plan: Plan,
|
||||
usage: RequestUsage,
|
||||
requests: i32,
|
||||
}
|
||||
|
||||
impl UsageBanner {
|
||||
pub fn new(plan: Plan, usage: RequestUsage) -> Self {
|
||||
Self { plan, usage }
|
||||
pub fn new(plan: Plan, requests: i32) -> Self {
|
||||
Self { plan, requests }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for UsageBanner {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let used_percentage = match self.usage.limit {
|
||||
UsageLimit::Limited(limit) => Some((self.usage.amount as f32 / limit as f32) * 100.),
|
||||
let request_limit = self.plan.model_requests_limit();
|
||||
|
||||
let used_percentage = match request_limit {
|
||||
UsageLimit::Limited(limit) => Some((self.requests as f32 / limit as f32) * 100.),
|
||||
UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
let (severity, message) = match self.usage.limit {
|
||||
let (severity, message) = match request_limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
if self.usage.amount >= limit {
|
||||
if self.requests >= limit {
|
||||
let message = match self.plan {
|
||||
Plan::ZedPro => "Monthly request limit reached",
|
||||
Plan::ZedProTrial => "Trial request limit reached",
|
||||
@@ -32,7 +33,7 @@ impl RenderOnce for UsageBanner {
|
||||
};
|
||||
|
||||
(Severity::Error, message)
|
||||
} else if (self.usage.amount as f32 / limit as f32) >= 0.9 {
|
||||
} else if (self.requests as f32 / limit as f32) >= 0.9 {
|
||||
(Severity::Warning, "Approaching request limit")
|
||||
} else {
|
||||
let message = match self.plan {
|
||||
@@ -80,11 +81,11 @@ impl RenderOnce for UsageBanner {
|
||||
.child(ProgressBar::new("usage", percent, 100., cx))
|
||||
}))
|
||||
.child(
|
||||
Label::new(match self.usage.limit {
|
||||
Label::new(match request_limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
format!("{} / {limit}", self.usage.amount)
|
||||
format!("{} / {limit}", self.requests)
|
||||
}
|
||||
UsageLimit::Unlimited => format!("{} / ∞", self.usage.amount),
|
||||
UsageLimit::Unlimited => format!("{} / ∞", self.requests),
|
||||
})
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
@@ -103,131 +104,74 @@ impl Component for UsageBanner {
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
let trial_limit = Plan::ZedProTrial.model_requests_limit();
|
||||
let trial_examples = vec![
|
||||
single_example(
|
||||
"Zed Pro Trial - New User",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedProTrial,
|
||||
RequestUsage {
|
||||
limit: trial_limit,
|
||||
amount: 10,
|
||||
},
|
||||
))
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 10))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedProTrial,
|
||||
RequestUsage {
|
||||
limit: trial_limit,
|
||||
amount: 135,
|
||||
},
|
||||
))
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 135))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedProTrial,
|
||||
RequestUsage {
|
||||
limit: trial_limit,
|
||||
amount: 150,
|
||||
},
|
||||
))
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 150))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let free_limit = Plan::Free.model_requests_limit();
|
||||
let free_examples = vec![
|
||||
single_example(
|
||||
"Free - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::Free,
|
||||
RequestUsage {
|
||||
limit: free_limit,
|
||||
amount: 25,
|
||||
},
|
||||
))
|
||||
.child(UsageBanner::new(Plan::Free, 25))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::Free,
|
||||
RequestUsage {
|
||||
limit: free_limit,
|
||||
amount: 45,
|
||||
},
|
||||
))
|
||||
.child(UsageBanner::new(Plan::Free, 45))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::Free,
|
||||
RequestUsage {
|
||||
limit: free_limit,
|
||||
amount: 50,
|
||||
},
|
||||
))
|
||||
.child(UsageBanner::new(Plan::Free, 50))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let zed_pro_limit = Plan::ZedPro.model_requests_limit();
|
||||
let zed_pro_examples = vec![
|
||||
single_example(
|
||||
"Zed Pro - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedPro,
|
||||
RequestUsage {
|
||||
limit: zed_pro_limit,
|
||||
amount: 250,
|
||||
},
|
||||
))
|
||||
.child(UsageBanner::new(Plan::ZedPro, 250))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedPro,
|
||||
RequestUsage {
|
||||
limit: zed_pro_limit,
|
||||
amount: 450,
|
||||
},
|
||||
))
|
||||
.child(UsageBanner::new(Plan::ZedPro, 450))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedPro,
|
||||
RequestUsage {
|
||||
limit: zed_pro_limit,
|
||||
amount: 500,
|
||||
},
|
||||
))
|
||||
.child(UsageBanner::new(Plan::ZedPro, 500))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -23,6 +23,7 @@ use gpui::{
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
@@ -488,8 +489,8 @@ impl AssistantPanel {
|
||||
|
||||
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
|
||||
// the provider, we want to show a nudge to sign in.
|
||||
let show_zed_ai_notice =
|
||||
client_status.is_signed_out() && model.map_or(true, |model| model.is_provided_by_zed());
|
||||
let show_zed_ai_notice = client_status.is_signed_out()
|
||||
&& model.map_or(true, |model| model.provider.id().0 == ZED_CLOUD_PROVIDER_ID);
|
||||
|
||||
self.show_zed_ai_notice = show_zed_ai_notice;
|
||||
cx.notify();
|
||||
|
||||
@@ -1226,7 +1226,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<InlineAssist>(
|
||||
row_range,
|
||||
cx.theme().status().info_background,
|
||||
Default::default(),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1291,7 +1291,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
cx.theme().status().deleted_background,
|
||||
Default::default(),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
|
||||
@@ -2089,7 +2089,7 @@ impl ContextEditor {
|
||||
continue;
|
||||
};
|
||||
let image_id = image.id();
|
||||
let image_task = LanguageModelImage::from_image(Arc::new(image), cx).shared();
|
||||
let image_task = LanguageModelImage::from_image(image, cx).shared();
|
||||
|
||||
for image_position in image_positions.iter() {
|
||||
context.insert_content(
|
||||
|
||||
@@ -28,7 +28,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -39,9 +39,10 @@ impl ActionLog {
|
||||
self.edited_since_project_diagnostics_check
|
||||
}
|
||||
|
||||
fn track_buffer_internal(
|
||||
fn track_buffer(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
created: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> &mut TrackedBuffer {
|
||||
let tracked_buffer = self
|
||||
@@ -58,11 +59,7 @@ impl ActionLog {
|
||||
let base_text;
|
||||
let status;
|
||||
let unreviewed_changes;
|
||||
if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map_or(true, |file| !file.disk_state().exists())
|
||||
{
|
||||
if created {
|
||||
base_text = Rope::default();
|
||||
status = TrackedBufferStatus::Created;
|
||||
unreviewed_changes = Patch::new(vec![Edit {
|
||||
@@ -149,7 +146,7 @@ impl ActionLog {
|
||||
// resurrected externally, we want to clear the changes we
|
||||
// were tracking and reset the buffer's state.
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.track_buffer_internal(buffer, cx);
|
||||
self.track_buffer(buffer, false, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -263,15 +260,26 @@ impl ActionLog {
|
||||
}
|
||||
|
||||
/// Track a buffer as read, so we can notify the model about user edits.
|
||||
pub fn track_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.track_buffer_internal(buffer, cx);
|
||||
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.track_buffer(buffer, false, cx);
|
||||
}
|
||||
|
||||
/// Track a buffer that was added as context, so we can notify the model about user edits.
|
||||
pub fn buffer_added_as_context(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.track_buffer(buffer, false, cx);
|
||||
}
|
||||
|
||||
/// Track a buffer as read, so we can notify the model about user edits.
|
||||
pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.track_buffer(buffer.clone(), true, cx);
|
||||
self.buffer_edited(buffer, cx)
|
||||
}
|
||||
|
||||
/// Mark a buffer as edited, so we can refresh it in the context
|
||||
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.edited_since_project_diagnostics_check = true;
|
||||
|
||||
let tracked_buffer = self.track_buffer_internal(buffer.clone(), cx);
|
||||
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
|
||||
if let TrackedBufferStatus::Deleted = tracked_buffer.status {
|
||||
tracked_buffer.status = TrackedBufferStatus::Modified;
|
||||
}
|
||||
@@ -279,7 +287,7 @@ impl ActionLog {
|
||||
}
|
||||
|
||||
pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
let tracked_buffer = self.track_buffer_internal(buffer.clone(), cx);
|
||||
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
|
||||
match tracked_buffer.status {
|
||||
TrackedBufferStatus::Created => {
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
@@ -389,7 +397,7 @@ impl ActionLog {
|
||||
|
||||
// Clear all tracked changes for this buffer and start over as if we just read it.
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.track_buffer_internal(buffer.clone(), cx);
|
||||
self.track_buffer(buffer.clone(), false, cx);
|
||||
cx.notify();
|
||||
save
|
||||
}
|
||||
@@ -687,20 +695,12 @@ mod tests {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
|
||||
@@ -765,23 +765,12 @@ mod tests {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({"file": "abc\ndef\nghi\njkl\nmno\npqr"}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
|
||||
@@ -850,20 +839,12 @@ mod tests {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
|
||||
@@ -947,21 +928,25 @@ mod tests {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/dir"), json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/dir"), json!({})).await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
|
||||
.unwrap();
|
||||
|
||||
// Simulate file2 being recreated by a tool.
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
@@ -1082,9 +1067,8 @@ mod tests {
|
||||
.update(cx, |project, cx| project.open_buffer(file2_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer2.clone(), cx));
|
||||
buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
|
||||
.await
|
||||
@@ -1129,7 +1113,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
|
||||
@@ -1264,7 +1248,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
|
||||
@@ -1397,9 +1381,8 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
@@ -1455,7 +1438,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
|
||||
for _ in 0..operations {
|
||||
match rng.gen_range(0..100) {
|
||||
@@ -1507,7 +1490,7 @@ mod tests {
|
||||
log::info!("quiescing...");
|
||||
cx.run_until_parked();
|
||||
action_log.update(cx, |log, cx| {
|
||||
let tracked_buffer = log.track_buffer_internal(buffer.clone(), cx);
|
||||
let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
|
||||
let mut old_text = tracked_buffer.base_text.clone();
|
||||
let new_text = buffer.read(cx).as_rope();
|
||||
for edit in tracked_buffer.unreviewed_changes.edits() {
|
||||
|
||||
@@ -10,16 +10,14 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::AnyElement;
|
||||
use gpui::AnyWindowHandle;
|
||||
use gpui::Context;
|
||||
use gpui::IntoElement;
|
||||
use gpui::Window;
|
||||
use gpui::{App, Entity, SharedString, Task, WeakEntity};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use icons::IconName;
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use project::Project;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::action_log::*;
|
||||
pub use crate::tool_registry::*;
|
||||
@@ -67,7 +65,6 @@ pub trait ToolCard: 'static + Sized {
|
||||
&mut self,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement;
|
||||
}
|
||||
@@ -79,7 +76,6 @@ pub struct AnyToolCard {
|
||||
entity: gpui::AnyEntity,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> AnyElement,
|
||||
}
|
||||
@@ -90,14 +86,11 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
|
||||
entity: gpui::AnyEntity,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let entity = entity.downcast::<T>().unwrap();
|
||||
entity.update(cx, |entity, cx| {
|
||||
entity
|
||||
.render(status, window, workspace, cx)
|
||||
.into_any_element()
|
||||
entity.render(status, window, cx).into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,14 +102,8 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
|
||||
}
|
||||
|
||||
impl AnyToolCard {
|
||||
pub fn render(
|
||||
&self,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
(self.render)(self.entity.clone(), status, window, workspace, cx)
|
||||
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
(self.render)(self.entity.clone(), status, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +163,6 @@ pub trait Tool: 'static + Send + Sync {
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult;
|
||||
}
|
||||
|
||||
@@ -14,11 +14,10 @@ path = "src/assistant_tools.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
@@ -37,14 +36,11 @@ serde_json.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
web_search.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
worktree.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -22,16 +22,15 @@ mod schema;
|
||||
mod symbol_info_tool;
|
||||
mod terminal_tool;
|
||||
mod thinking_tool;
|
||||
mod ui;
|
||||
mod web_search_tool;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use copy_path_tool::CopyPathTool;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::App;
|
||||
use http_client::HttpClientWithUrl;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use move_path_tool::MovePathTool;
|
||||
use web_search_tool::WebSearchTool;
|
||||
|
||||
@@ -56,11 +55,6 @@ use crate::symbol_info_tool::SymbolInfoTool;
|
||||
use crate::terminal_tool::TerminalTool;
|
||||
use crate::thinking_tool::ThinkingTool;
|
||||
|
||||
pub use create_file_tool::CreateFileToolInput;
|
||||
pub use edit_file_tool::EditFileToolInput;
|
||||
pub use path_search_tool::PathSearchToolInput;
|
||||
pub use read_file_tool::ReadFileToolInput;
|
||||
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
|
||||
@@ -88,45 +82,34 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(ThinkingTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
|
||||
cx.subscribe(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
move |registry, event, cx| match event {
|
||||
language_model::Event::DefaultModelChanged => {
|
||||
let using_zed_provider = registry
|
||||
.read(cx)
|
||||
.default_model()
|
||||
.map_or(false, |default| default.is_provided_by_zed());
|
||||
if using_zed_provider {
|
||||
ToolRegistry::global(cx).register_tool(WebSearchTool);
|
||||
} else {
|
||||
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
|
||||
}
|
||||
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
|
||||
move |is_enabled, cx| {
|
||||
if is_enabled {
|
||||
ToolRegistry::global(cx).register_tool(WebSearchTool);
|
||||
} else {
|
||||
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use client::Client;
|
||||
use clock::FakeSystemClock;
|
||||
use http_client::FakeHttpClient;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_builtin_tool_schema_compatibility(cx: &mut App) {
|
||||
settings::init(cx);
|
||||
|
||||
let client = Client::new(
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_200_response(),
|
||||
crate::init(
|
||||
Arc::new(http_client::HttpClientWithUrl::new(
|
||||
FakeHttpClient::with_200_response(),
|
||||
"https://zed.dev",
|
||||
None,
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
language_model::init(client.clone(), cx);
|
||||
crate::init(client.http_client(), cx);
|
||||
|
||||
for tool in ToolRegistry::global(cx).tools() {
|
||||
let actual_schema = tool
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
|
||||
use futures::future::join_all;
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -218,7 +218,6 @@ impl Tool for BatchTool {
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<BatchToolInput>(input) {
|
||||
@@ -259,9 +258,7 @@ impl Tool for BatchTool {
|
||||
let action_log = action_log.clone();
|
||||
let messages = messages.clone();
|
||||
let tool_result = cx
|
||||
.update(|cx| {
|
||||
tool.run(invocation.input, &messages, project, action_log, window, cx)
|
||||
})
|
||||
.update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
|
||||
.map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
|
||||
|
||||
tasks.push(tool_result.output);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{self, Anchor, Buffer, ToPointUtf16};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{self, LspAction, Project};
|
||||
@@ -140,7 +140,6 @@ impl Tool for CodeActionTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CodeActionToolInput>(input) {
|
||||
@@ -160,7 +159,7 @@ impl Tool for CodeActionTool {
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.track_buffer(buffer.clone(), cx);
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
let range = {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use collections::IndexMap;
|
||||
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
|
||||
use gpui::{App, AsyncApp, Entity, Task};
|
||||
use language::{OutlineItem, ParseStatus, Point};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{Project, Symbol};
|
||||
@@ -128,7 +128,6 @@ impl Tool for CodeSymbolsTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
|
||||
@@ -175,7 +174,7 @@ pub async fn file_outline(
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.track_buffer(buffer.clone(), cx);
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
// Wait until the buffer has been fully parsed, so that we can read its outline.
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use gpui::{App, Entity, Task};
|
||||
use itertools::Itertools;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -102,7 +102,6 @@ impl Tool for ContentsTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<ContentsToolInput>(input) {
|
||||
@@ -210,7 +209,7 @@ impl Tool for ContentsTool {
|
||||
})?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.track_buffer(buffer, cx);
|
||||
log.buffer_read(buffer, cx);
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
@@ -222,7 +221,7 @@ impl Tool for ContentsTool {
|
||||
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.track_buffer(buffer, cx);
|
||||
log.buffer_read(buffer, cx);
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::AnyWindowHandle;
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
@@ -77,7 +76,6 @@ impl Tool for CopyPathTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CopyPathToolInput>(input) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::AnyWindowHandle;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
@@ -68,7 +67,6 @@ impl Tool for CreateDirectoryTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::AnyWindowHandle;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
@@ -90,7 +89,6 @@ impl Tool for CreateFileTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
|
||||
@@ -114,12 +112,9 @@ impl Tool for CreateFileTool {
|
||||
.await
|
||||
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.track_buffer(buffer.clone(), cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx));
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_edited(buffer.clone(), cx)
|
||||
action_log.will_create_buffer(buffer.clone(), cx)
|
||||
});
|
||||
})?;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
@@ -62,7 +62,6 @@ impl Tool for DeletePathTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{DiagnosticSeverity, OffsetRangeExt};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -82,7 +82,6 @@ impl Tool for DiagnosticsTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
match serde_json::from_value::<DiagnosticsToolInput>(input)
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
use crate::{
|
||||
replace::{replace_exact, replace_with_flexible_indent},
|
||||
schema::json_schema_for,
|
||||
};
|
||||
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId, Task, WeakEntity,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
|
||||
};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::{Disclosure, Tooltip, Window, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
use crate::replace::replace_exact;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditFileToolInput {
|
||||
@@ -74,7 +61,7 @@ struct PartialInput {
|
||||
|
||||
pub struct EditFileTool;
|
||||
|
||||
const DEFAULT_UI_TEXT: &str = "Editing file";
|
||||
const DEFAULT_UI_TEXT: &str = "Edit file";
|
||||
|
||||
impl Tool for EditFileTool {
|
||||
fn name(&self) -> String {
|
||||
@@ -100,7 +87,7 @@ impl Tool for EditFileTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<EditFileToolInput>(input.clone()) {
|
||||
Ok(input) => input.display_description,
|
||||
Err(_) => "Editing file".to_string(),
|
||||
Err(_) => "Edit file".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +113,6 @@ impl Tool for EditFileTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<EditFileToolInput>(input) {
|
||||
@@ -134,18 +120,7 @@ impl Tool for EditFileTool {
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let card = window.and_then(|window| {
|
||||
window
|
||||
.update(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
let card_clone = card.clone();
|
||||
let task = cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&input.path, cx)
|
||||
@@ -153,38 +128,26 @@ impl Tool for EditFileTool {
|
||||
})??;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?;
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
if input.old_string.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"`old_string` can't be empty, use another tool if you want to create a file."
|
||||
));
|
||||
return Err(anyhow!("`old_string` cannot be empty. Use a different tool if you want to create a file."));
|
||||
}
|
||||
|
||||
if input.old_string == input.new_string {
|
||||
return Err(anyhow!(
|
||||
"The `old_string` and `new_string` are identical, so no changes would be made."
|
||||
));
|
||||
return Err(anyhow!("The `old_string` and `new_string` are identical, so no changes would be made."));
|
||||
}
|
||||
|
||||
let result = cx
|
||||
.background_spawn(async move {
|
||||
// Try to match exactly
|
||||
let diff = replace_exact(&input.old_string, &input.new_string, &snapshot)
|
||||
.await
|
||||
// If that fails, try being flexible about indentation
|
||||
.or_else(|| {
|
||||
replace_with_flexible_indent(
|
||||
&input.old_string,
|
||||
&input.new_string,
|
||||
&snapshot,
|
||||
)
|
||||
})?;
|
||||
.await
|
||||
// If that fails, try being flexible about indentation
|
||||
.or_else(|| replace_with_flexible_indent(&input.old_string, &input.new_string, &snapshot))?;
|
||||
|
||||
if diff.edits.is_empty() {
|
||||
return None;
|
||||
@@ -214,409 +177,41 @@ impl Tool for EditFileTool {
|
||||
}
|
||||
})?;
|
||||
|
||||
return Err(err);
|
||||
return Err(err)
|
||||
};
|
||||
|
||||
let snapshot = cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx)
|
||||
});
|
||||
let snapshot = buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.apply_diff(diff, cx);
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.snapshot()
|
||||
});
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), cx)
|
||||
});
|
||||
snapshot
|
||||
})?;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||
.await?;
|
||||
project.update( cx, |project, cx| {
|
||||
project.save_buffer(buffer, cx)
|
||||
})?.await?;
|
||||
|
||||
let new_text = snapshot.text();
|
||||
let diff_str = cx
|
||||
.background_spawn({
|
||||
let old_text = old_text.clone();
|
||||
let new_text = new_text.clone();
|
||||
async move { language::unified_diff(&old_text, &new_text) }
|
||||
})
|
||||
.await;
|
||||
let diff_str = cx.background_spawn(async move {
|
||||
let new_text = snapshot.text();
|
||||
language::unified_diff(&old_text, &new_text)
|
||||
}).await;
|
||||
|
||||
if let Some(card) = card_clone {
|
||||
card.update(cx, |card, cx| {
|
||||
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"Edited {}:\n\n```diff\n{}\n```",
|
||||
input.path.display(),
|
||||
diff_str
|
||||
))
|
||||
});
|
||||
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
|
||||
|
||||
ToolResult {
|
||||
output: task,
|
||||
card: card.map(AnyToolCard::from),
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditFileToolCard {
|
||||
path: PathBuf,
|
||||
editor: Entity<Editor>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
project: Entity<Project>,
|
||||
diff_task: Option<Task<Result<()>>>,
|
||||
preview_expanded: bool,
|
||||
full_height_expanded: bool,
|
||||
editor_unique_id: EntityId,
|
||||
}
|
||||
|
||||
impl EditFileToolCard {
|
||||
fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
|
||||
let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: true,
|
||||
},
|
||||
multibuffer.clone(),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.disable_scrolling(cx);
|
||||
editor.disable_expand_excerpt_buttons(cx);
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor
|
||||
});
|
||||
Self {
|
||||
editor_unique_id: editor.entity_id(),
|
||||
path,
|
||||
project,
|
||||
editor,
|
||||
multibuffer,
|
||||
diff_task: None,
|
||||
preview_expanded: true,
|
||||
full_height_expanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_diff(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
self.diff_task = Some(cx.spawn(async move |this, cx| {
|
||||
let buffer = build_buffer(new_text, path.clone(), &language_registry, cx).await?;
|
||||
let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.multibuffer.update(cx, |multibuffer, cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let diff = buffer_diff.read(cx);
|
||||
let diff_hunk_ranges = diff
|
||||
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
let (_, is_newly_added) = multibuffer.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer,
|
||||
diff_hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
);
|
||||
debug_assert!(is_newly_added);
|
||||
multibuffer.add_diff(buffer_diff, cx);
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolCard for EditFileToolCard {
|
||||
fn render(
|
||||
&mut self,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let failed = matches!(status, ToolUseStatus::Error(_));
|
||||
|
||||
let path_label_button = h_flex()
|
||||
.id(("edit-tool-path-label-button", self.editor_unique_id))
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.px_1()
|
||||
.gap_0p5()
|
||||
.cursor_pointer()
|
||||
.rounded_sm()
|
||||
.opacity(0.8)
|
||||
.hover(|label| {
|
||||
label
|
||||
.opacity(1.)
|
||||
.bg(cx.theme().colors().element_hover.opacity(0.5))
|
||||
})
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Icon::new(IconName::Pencil)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(rems(0.8125))
|
||||
.child(self.path.display().to_string())
|
||||
.ml_1p5()
|
||||
.mr_0p5(),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Ignored),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let path = self.path.clone();
|
||||
let workspace = workspace.clone();
|
||||
move |_, window, cx| {
|
||||
workspace
|
||||
.update(cx, {
|
||||
|workspace, cx| {
|
||||
let Some(project_path) =
|
||||
workspace.project().read(cx).find_project_path(&path, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let open_task =
|
||||
workspace.open_path(project_path, None, true, window, cx);
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let item = open_task.await?;
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
active_editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
language::Point::new(0, 0),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.into_any_element();
|
||||
|
||||
let codeblock_header_bg = cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_background
|
||||
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
|
||||
|
||||
let codeblock_header = h_flex()
|
||||
.flex_none()
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.rounded_t_md()
|
||||
.when(!failed, |header| header.bg(codeblock_header_bg))
|
||||
.child(path_label_button)
|
||||
.map(|container| {
|
||||
if failed {
|
||||
container.child(
|
||||
Icon::new(IconName::Close)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
} else {
|
||||
container.child(
|
||||
Disclosure::new(
|
||||
("edit-file-disclosure", self.editor_unique_id),
|
||||
self.preview_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener(
|
||||
move |this, _event, _window, _cx| {
|
||||
this.preview_expanded = !this.preview_expanded;
|
||||
},
|
||||
)),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let editor = self.editor.update(cx, |editor, cx| {
|
||||
editor.render(window, cx).into_any_element()
|
||||
});
|
||||
|
||||
let (full_height_icon, full_height_tooltip_label) = if self.full_height_expanded {
|
||||
(IconName::ChevronUp, "Collapse Code Block")
|
||||
} else {
|
||||
(IconName::ChevronDown, "Expand Code Block")
|
||||
};
|
||||
|
||||
let gradient_overlay = div()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h_2_5()
|
||||
.rounded_b_lg()
|
||||
.bg(gpui::linear_gradient(
|
||||
0.,
|
||||
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
|
||||
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
|
||||
));
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
|
||||
v_flex()
|
||||
.mb_2()
|
||||
.border_1()
|
||||
.when(failed, |card| card.border_dashed())
|
||||
.border_color(border_color)
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.child(codeblock_header)
|
||||
.when(!failed && self.preview_expanded, |card| {
|
||||
card.child(
|
||||
v_flex()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.map(|editor_container| {
|
||||
if self.full_height_expanded {
|
||||
editor_container.h_full()
|
||||
} else {
|
||||
editor_container.max_h_64()
|
||||
}
|
||||
})
|
||||
.child(div().pl_1().child(editor))
|
||||
.when(!self.full_height_expanded, |editor_container| {
|
||||
editor_container.child(gradient_overlay)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(!failed && self.preview_expanded, |card| {
|
||||
card.child(
|
||||
h_flex()
|
||||
.id(("edit-tool-card-inner-hflex", self.editor_unique_id))
|
||||
.flex_none()
|
||||
.cursor_pointer()
|
||||
.h_5()
|
||||
.justify_center()
|
||||
.rounded_b_md()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
|
||||
.child(
|
||||
Icon::new(full_height_icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.tooltip(Tooltip::text(full_height_tooltip_label))
|
||||
.on_click(cx.listener(move |this, _event, _window, _cx| {
|
||||
this.full_height_expanded = !this.full_height_expanded;
|
||||
})),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_buffer(
|
||||
mut text: String,
|
||||
path: Arc<Path>,
|
||||
language_registry: &Arc<language::LanguageRegistry>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<Buffer>> {
|
||||
let line_ending = LineEnding::detect(&text);
|
||||
LineEnding::normalize(&mut text);
|
||||
let text = Rope::from(text);
|
||||
let language = cx
|
||||
.update(|_cx| language_registry.language_for_file_path(&path))?
|
||||
.await
|
||||
.ok();
|
||||
let buffer = cx.new(|cx| {
|
||||
let buffer = TextBuffer::new_normalized(
|
||||
0,
|
||||
cx.entity_id().as_non_zero_u64().into(),
|
||||
line_ending,
|
||||
text,
|
||||
);
|
||||
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
|
||||
buffer.set_language(language, cx);
|
||||
buffer
|
||||
})?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
async fn build_buffer_diff(
|
||||
mut old_text: String,
|
||||
buffer: &Entity<Buffer>,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<BufferDiff>> {
|
||||
LineEnding::normalize(&mut old_text);
|
||||
|
||||
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
|
||||
|
||||
let base_buffer = cx
|
||||
.update(|cx| {
|
||||
Buffer::build_snapshot(
|
||||
old_text.clone().into(),
|
||||
buffer.language().cloned(),
|
||||
Some(language_registry.clone()),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
let diff_snapshot = cx
|
||||
.update(|cx| {
|
||||
BufferDiffSnapshot::new_with_base_buffer(
|
||||
buffer.text.clone(),
|
||||
Some(old_text.into()),
|
||||
base_buffer,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer.text, cx);
|
||||
diff.set_snapshot(diff_snapshot, &buffer.text, cx);
|
||||
diff
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task};
|
||||
use gpui::{App, AppContext as _, Entity, Task};
|
||||
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
@@ -145,7 +145,6 @@ impl Tool for FetchTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<FetchToolInput>(input) {
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::OffsetRangeExt;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{
|
||||
@@ -96,7 +96,6 @@ impl Tool for GrepTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
@@ -406,7 +405,7 @@ mod tests {
|
||||
) -> String {
|
||||
let tool = Arc::new(GrepTool);
|
||||
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
||||
let task = cx.update(|cx| tool.run(input, &[], project, action_log, None, cx));
|
||||
let task = cx.update(|cx| tool.run(input, &[], project, action_log, cx));
|
||||
|
||||
match task.output.await {
|
||||
Ok(result) => result,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -76,7 +76,6 @@ impl Tool for ListDirectoryTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -89,7 +89,6 @@ impl Tool for MovePathTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<MovePathToolInput>(input) {
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use chrono::{Local, Utc};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -59,7 +59,6 @@ impl Tool for NowTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
_cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input: NowToolInput = match serde_json::from_value(input) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -52,7 +52,6 @@ impl Tool for OpenTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input: OpenToolInput = match serde_json::from_value(input) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -70,7 +70,6 @@ impl Tool for PathSearchTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
|
||||
use gpui::{App, Entity, Task};
|
||||
use indoc::formatdoc;
|
||||
use itertools::Itertools;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
@@ -88,7 +87,6 @@ impl Tool for ReadFileTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
|
||||
@@ -136,7 +134,7 @@ impl Tool for ReadFileTool {
|
||||
})?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.track_buffer(buffer, cx);
|
||||
log.buffer_read(buffer, cx);
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
@@ -149,7 +147,7 @@ impl Tool for ReadFileTool {
|
||||
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.track_buffer(buffer, cx);
|
||||
log.buffer_read(buffer, cx);
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
@@ -195,7 +193,7 @@ mod test {
|
||||
"path": "root/nonexistent_file.txt"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -225,7 +223,7 @@ mod test {
|
||||
"path": "root/small_file.txt"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -255,7 +253,7 @@ mod test {
|
||||
"path": "root/large_file.rs"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log.clone(), None, cx)
|
||||
.run(input, &[], project.clone(), action_log.clone(), cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -279,7 +277,7 @@ mod test {
|
||||
"offset": 1
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -325,7 +323,7 @@ mod test {
|
||||
"end_line": 4
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{self, Buffer, ToPointUtf16};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -87,7 +87,6 @@ impl Tool for RenameTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<RenameToolInput>(input) {
|
||||
@@ -107,7 +106,7 @@ impl Tool for RenameTool {
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.track_buffer(buffer.clone(), cx);
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
let position = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
|
||||
use gpui::{App, AsyncApp, Entity, Task};
|
||||
use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -121,7 +121,6 @@ impl Tool for SymbolInfoTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {
|
||||
@@ -141,7 +140,7 @@ impl Tool for SymbolInfoTool {
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.track_buffer(buffer.clone(), cx);
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
let position = {
|
||||
|
||||
@@ -3,7 +3,7 @@ use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::io::BufReader;
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt, FutureExt};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -78,7 +78,6 @@ impl Tool for TerminalTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input: TerminalToolInput = match serde_json::from_value(input) {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -50,7 +50,6 @@ impl Tool for ThinkingTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
_cx: &mut App,
|
||||
) -> ToolResult {
|
||||
// This tool just "thinks out loud" and doesn't perform any actions.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
mod tool_call_card_header;
|
||||
|
||||
pub use tool_call_card_header::*;
|
||||
@@ -1,102 +0,0 @@
|
||||
use gpui::{Animation, AnimationExt, App, IntoElement, pulsating_between};
|
||||
use std::time::Duration;
|
||||
use ui::{Tooltip, prelude::*};
|
||||
|
||||
/// A reusable header component for tool call cards.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ToolCallCardHeader {
|
||||
icon: IconName,
|
||||
primary_text: SharedString,
|
||||
secondary_text: Option<SharedString>,
|
||||
is_loading: bool,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl ToolCallCardHeader {
|
||||
pub fn new(icon: IconName, primary_text: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
icon,
|
||||
primary_text: primary_text.into(),
|
||||
secondary_text: None,
|
||||
is_loading: false,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_secondary_text(mut self, text: impl Into<SharedString>) -> Self {
|
||||
self.secondary_text = Some(text.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn loading(mut self) -> Self {
|
||||
self.is_loading = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_error(mut self, error: impl Into<String>) -> Self {
|
||||
self.error = Some(error.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ToolCallCardHeader {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let font_size = rems(0.8125);
|
||||
let secondary_text = self.secondary_text;
|
||||
|
||||
h_flex()
|
||||
.id("tool-label-container")
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.opacity(0.8)
|
||||
.child(
|
||||
h_flex().h(window.line_height()).justify_center().child(
|
||||
Icon::new(self.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h(window.line_height())
|
||||
.gap_1p5()
|
||||
.text_size(font_size)
|
||||
.map(|this| {
|
||||
if let Some(error) = &self.error {
|
||||
this.child(format!("{} failed", self.primary_text)).child(
|
||||
IconButton::new("error_info", IconName::Warning)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Warning)
|
||||
.tooltip(Tooltip::text(error.clone())),
|
||||
)
|
||||
} else {
|
||||
this.child(self.primary_text.clone())
|
||||
}
|
||||
})
|
||||
.when_some(secondary_text, |this, secondary_text| {
|
||||
this.child(
|
||||
div()
|
||||
.size(px(3.))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text),
|
||||
)
|
||||
.child(div().text_size(font_size).child(secondary_text.clone()))
|
||||
})
|
||||
.with_animation(
|
||||
"loading-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
move |this, delta| {
|
||||
if self.is_loading {
|
||||
this.opacity(delta)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use crate::schema::json_schema_for;
|
||||
use crate::ui::ToolCallCardHeader;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use futures::{Future, FutureExt, TryFutureExt};
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
|
||||
Animation, AnimationExt, App, AppContext, Context, Entity, IntoElement, Task, Window,
|
||||
pulsating_between,
|
||||
};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -14,7 +14,6 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::{IconName, Tooltip, prelude::*};
|
||||
use web_search::WebSearchRegistry;
|
||||
use workspace::Workspace;
|
||||
use zed_llm_client::{WebSearchCitation, WebSearchResponse};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -48,7 +47,7 @@ impl Tool for WebSearchTool {
|
||||
}
|
||||
|
||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
||||
"Searching the Web".to_string()
|
||||
"Web Search".to_string()
|
||||
}
|
||||
|
||||
fn run(
|
||||
@@ -57,7 +56,6 @@ impl Tool for WebSearchTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<WebSearchToolInput>(input) {
|
||||
@@ -115,33 +113,63 @@ impl ToolCard for WebSearchToolCard {
|
||||
&mut self,
|
||||
_status: &ToolUseStatus,
|
||||
_window: &mut Window,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let header = match self.response.as_ref() {
|
||||
Some(Ok(response)) => {
|
||||
let text: SharedString = if response.citations.len() == 1 {
|
||||
"1 result".into()
|
||||
} else {
|
||||
format!("{} results", response.citations.len()).into()
|
||||
};
|
||||
ToolCallCardHeader::new(IconName::Globe, "Searched the Web")
|
||||
.with_secondary_text(text)
|
||||
}
|
||||
Some(Err(error)) => {
|
||||
ToolCallCardHeader::new(IconName::Globe, "Web Search").with_error(error.to_string())
|
||||
}
|
||||
None => ToolCallCardHeader::new(IconName::Globe, "Searching the Web").loading(),
|
||||
};
|
||||
let header = h_flex()
|
||||
.id("tool-label-container")
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
Icon::new(IconName::Globe)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(match self.response.as_ref() {
|
||||
Some(Ok(response)) => {
|
||||
let text: SharedString = if response.citations.len() == 1 {
|
||||
"1 result".into()
|
||||
} else {
|
||||
format!("{} results", response.citations.len()).into()
|
||||
};
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Label::new("Searched the Web").size(LabelSize::Small))
|
||||
.child(
|
||||
div()
|
||||
.size(px(3.))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text),
|
||||
)
|
||||
.child(Label::new(text).size(LabelSize::Small))
|
||||
.into_any_element()
|
||||
}
|
||||
Some(Err(error)) => div()
|
||||
.id("web-search-error")
|
||||
.child(Label::new("Web Search failed").size(LabelSize::Small))
|
||||
.tooltip(Tooltip::text(error.to_string()))
|
||||
.into_any_element(),
|
||||
|
||||
None => Label::new("Searching the Web…")
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"web-search-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.alpha(delta),
|
||||
)
|
||||
.into_any_element(),
|
||||
})
|
||||
.into_any();
|
||||
|
||||
let content =
|
||||
self.response.as_ref().and_then(|response| match response {
|
||||
Ok(response) => {
|
||||
Some(
|
||||
v_flex()
|
||||
.overflow_hidden()
|
||||
.ml_1p5()
|
||||
.pl(px(5.))
|
||||
.pl_1p5()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.gap_1()
|
||||
@@ -181,7 +209,7 @@ impl ToolCard for WebSearchToolCard {
|
||||
Err(_) => None,
|
||||
});
|
||||
|
||||
v_flex().mb_3().gap_1().child(header).children(content)
|
||||
v_flex().my_2().gap_1().child(header).children(content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,13 +253,8 @@ impl Component for WebSearchTool {
|
||||
div()
|
||||
.size_full()
|
||||
.child(in_progress_search.update(cx, |tool, cx| {
|
||||
tool.render(
|
||||
&ToolUseStatus::Pending,
|
||||
window,
|
||||
WeakEntity::new_invalid(),
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
tool.render(&ToolUseStatus::Pending, window, cx)
|
||||
.into_any_element()
|
||||
}))
|
||||
.into_any_element(),
|
||||
),
|
||||
@@ -240,13 +263,8 @@ impl Component for WebSearchTool {
|
||||
div()
|
||||
.size_full()
|
||||
.child(successful_search.update(cx, |tool, cx| {
|
||||
tool.render(
|
||||
&ToolUseStatus::Finished("".into()),
|
||||
window,
|
||||
WeakEntity::new_invalid(),
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
tool.render(&ToolUseStatus::Finished("".into()), window, cx)
|
||||
.into_any_element()
|
||||
}))
|
||||
.into_any_element(),
|
||||
),
|
||||
@@ -255,13 +273,8 @@ impl Component for WebSearchTool {
|
||||
div()
|
||||
.size_full()
|
||||
.child(error_search.update(cx, |tool, cx| {
|
||||
tool.render(
|
||||
&ToolUseStatus::Error("".into()),
|
||||
window,
|
||||
WeakEntity::new_invalid(),
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
tool.render(&ToolUseStatus::Error("".into()), window, cx)
|
||||
.into_any_element()
|
||||
}))
|
||||
.into_any_element(),
|
||||
),
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::TelemetrySettings;
|
||||
use anyhow::Result;
|
||||
use clock::SystemClock;
|
||||
use futures::channel::mpsc;
|
||||
use futures::{Future, FutureExt, StreamExt};
|
||||
use futures::{Future, StreamExt};
|
||||
use gpui::{App, AppContext as _, BackgroundExecutor, Task};
|
||||
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
|
||||
use parking_lot::Mutex;
|
||||
@@ -290,10 +290,6 @@ impl Telemetry {
|
||||
paths::logs_dir().join("telemetry.log")
|
||||
}
|
||||
|
||||
pub fn has_checksum_seed(&self) -> bool {
|
||||
ZED_CLIENT_CHECKSUM_SEED.is_some()
|
||||
}
|
||||
|
||||
pub fn start(
|
||||
self: &Arc<Self>,
|
||||
system_id: Option<String>,
|
||||
@@ -434,7 +430,7 @@ impl Telemetry {
|
||||
let executor = self.executor.clone();
|
||||
state.flush_events_task = Some(self.executor.spawn(async move {
|
||||
executor.timer(FLUSH_INTERVAL).await;
|
||||
this.flush_events().detach();
|
||||
this.flush_events();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -460,7 +456,7 @@ impl Telemetry {
|
||||
|
||||
if state.installation_id.is_some() && state.events_queue.len() >= state.max_queue_size {
|
||||
drop(state);
|
||||
self.flush_events().detach();
|
||||
self.flush_events();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,59 +499,60 @@ impl Telemetry {
|
||||
.body(json_bytes.into())?)
|
||||
}
|
||||
|
||||
pub fn flush_events(self: &Arc<Self>) -> Task<()> {
|
||||
pub fn flush_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
state.first_event_date_time = None;
|
||||
let mut events = mem::take(&mut state.events_queue);
|
||||
state.flush_events_task.take();
|
||||
drop(state);
|
||||
if events.is_empty() {
|
||||
return Task::ready(());
|
||||
return;
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
self.executor.spawn(
|
||||
async move {
|
||||
let mut json_bytes = Vec::new();
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let mut json_bytes = Vec::new();
|
||||
|
||||
if let Some(file) = &mut this.state.lock().log_file {
|
||||
for event in &mut events {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, event)?;
|
||||
file.write_all(&json_bytes)?;
|
||||
file.write_all(b"\n")?;
|
||||
if let Some(file) = &mut this.state.lock().log_file {
|
||||
for event in &mut events {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, event)?;
|
||||
file.write_all(&json_bytes)?;
|
||||
file.write_all(b"\n")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let request_body = {
|
||||
let state = this.state.lock();
|
||||
let request_body = {
|
||||
let state = this.state.lock();
|
||||
|
||||
EventRequestBody {
|
||||
system_id: state.system_id.as_deref().map(Into::into),
|
||||
installation_id: state.installation_id.as_deref().map(Into::into),
|
||||
session_id: state.session_id.clone(),
|
||||
metrics_id: state.metrics_id.as_deref().map(Into::into),
|
||||
is_staff: state.is_staff,
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name.clone(),
|
||||
os_version: state.os_version.clone(),
|
||||
architecture: state.architecture.to_string(),
|
||||
EventRequestBody {
|
||||
system_id: state.system_id.as_deref().map(Into::into),
|
||||
installation_id: state.installation_id.as_deref().map(Into::into),
|
||||
session_id: state.session_id.clone(),
|
||||
metrics_id: state.metrics_id.as_deref().map(Into::into),
|
||||
is_staff: state.is_staff,
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name.clone(),
|
||||
os_version: state.os_version.clone(),
|
||||
architecture: state.architecture.to_string(),
|
||||
|
||||
release_channel: state.release_channel.map(Into::into),
|
||||
events,
|
||||
release_channel: state.release_channel.map(Into::into),
|
||||
events,
|
||||
}
|
||||
};
|
||||
|
||||
let request = this.build_request(json_bytes, request_body)?;
|
||||
let response = this.http_client.send(request).await?;
|
||||
if response.status() != 200 {
|
||||
log::error!("Failed to send events: HTTP {:?}", response.status());
|
||||
}
|
||||
};
|
||||
|
||||
let request = this.build_request(json_bytes, request_body)?;
|
||||
let response = this.http_client.send(request).await?;
|
||||
if response.status() != 200 {
|
||||
log::error!("Failed to send events: HTTP {:?}", response.status());
|
||||
anyhow::Ok(())
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.map(|_| ()),
|
||||
)
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -492,8 +492,7 @@ CREATE TABLE IF NOT EXISTS billing_customers (
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER NOT NULL REFERENCES users (id),
|
||||
has_overdue_invoices BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
stripe_customer_id TEXT NOT NULL,
|
||||
trial_started_at TIMESTAMP
|
||||
stripe_customer_id TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "uix_billing_customers_on_user_id" ON billing_customers (user_id);
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
alter table billing_customers
|
||||
add column trial_started_at timestamp without time zone;
|
||||
@@ -1,2 +0,0 @@
|
||||
alter table project_repositories
|
||||
add column head_commit_details varchar;
|
||||
@@ -287,7 +287,7 @@ async fn create_billing_subscription(
|
||||
}
|
||||
}
|
||||
|
||||
let customer_id = if let Some(existing_customer) = &existing_billing_customer {
|
||||
let customer_id = if let Some(existing_customer) = existing_billing_customer {
|
||||
CustomerId::from_str(&existing_customer.stripe_customer_id)
|
||||
.context("failed to parse customer ID")?
|
||||
} else {
|
||||
@@ -320,15 +320,6 @@ async fn create_billing_subscription(
|
||||
.await?
|
||||
}
|
||||
Some(ProductCode::ZedProTrial) => {
|
||||
if let Some(existing_billing_customer) = &existing_billing_customer {
|
||||
if existing_billing_customer.trial_started_at.is_some() {
|
||||
return Err(Error::http(
|
||||
StatusCode::FORBIDDEN,
|
||||
"user already used free trial".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
stripe_billing
|
||||
.checkout_with_zed_pro_trial(
|
||||
app.config.zed_pro_price_id()?,
|
||||
@@ -826,24 +817,6 @@ async fn handle_customer_subscription_event(
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("billing customer not found"))?;
|
||||
|
||||
if let Some(SubscriptionKind::ZedProTrial) = subscription_kind {
|
||||
if subscription.status == SubscriptionStatus::Trialing {
|
||||
let current_period_start =
|
||||
DateTime::from_timestamp(subscription.current_period_start, 0)
|
||||
.ok_or_else(|| anyhow!("No trial subscription period start"))?;
|
||||
|
||||
app.db
|
||||
.update_billing_customer(
|
||||
billing_customer.id,
|
||||
&UpdateBillingCustomerParams {
|
||||
trial_started_at: ActiveValue::set(Some(current_period_start.naive_utc())),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let was_canceled_due_to_payment_failure = subscription.status == SubscriptionStatus::Canceled
|
||||
&& subscription
|
||||
.cancellation_details
|
||||
@@ -870,28 +843,6 @@ async fn handle_customer_subscription_event(
|
||||
.get_billing_subscription_by_stripe_subscription_id(&subscription.id)
|
||||
.await?
|
||||
{
|
||||
let llm_db = app
|
||||
.llm_db
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("LLM DB not initialized"))?;
|
||||
|
||||
let new_period_start_at =
|
||||
chrono::DateTime::from_timestamp(subscription.current_period_start, 0)
|
||||
.ok_or_else(|| anyhow!("No subscription period start"))?;
|
||||
let new_period_end_at =
|
||||
chrono::DateTime::from_timestamp(subscription.current_period_end, 0)
|
||||
.ok_or_else(|| anyhow!("No subscription period end"))?;
|
||||
|
||||
llm_db
|
||||
.transfer_existing_subscription_usage(
|
||||
billing_customer.user_id,
|
||||
&existing_subscription,
|
||||
subscription_kind,
|
||||
new_period_start_at,
|
||||
new_period_end_at,
|
||||
)
|
||||
.await?;
|
||||
|
||||
app.db
|
||||
.update_billing_subscription(
|
||||
existing_subscription.id,
|
||||
|
||||
@@ -516,7 +516,6 @@ pub async fn post_events(
|
||||
if let Some(kinesis_client) = app.kinesis_client.clone() {
|
||||
if let Some(stream) = app.config.kinesis_stream.clone() {
|
||||
let mut request = kinesis_client.put_records().stream_name(stream);
|
||||
let mut has_records = false;
|
||||
for row in for_snowflake(
|
||||
request_body.clone(),
|
||||
first_event_at,
|
||||
@@ -531,12 +530,9 @@ pub async fn post_events(
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
has_records = true;
|
||||
}
|
||||
}
|
||||
if has_records {
|
||||
request.send().await.log_err();
|
||||
}
|
||||
request.send().await.log_err();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -559,7 +555,7 @@ fn for_snowflake(
|
||||
country_code: Option<String>,
|
||||
checksum_matched: bool,
|
||||
) -> impl Iterator<Item = SnowflakeRow> {
|
||||
body.events.into_iter().filter_map(move |event| {
|
||||
body.events.into_iter().flat_map(move |event| {
|
||||
let timestamp =
|
||||
first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
|
||||
// We will need to double check, but I believe all of the events that
|
||||
@@ -748,11 +744,9 @@ fn for_snowflake(
|
||||
// NOTE: most amplitude user properties are read out of our event_properties
|
||||
// dictionary. See https://app.amplitude.com/data/zed/Zed/sources/detail/production/falcon%3A159998
|
||||
// for how that is configured.
|
||||
let user_properties = body.is_staff.map(|is_staff| {
|
||||
serde_json::json!({
|
||||
"is_staff": is_staff,
|
||||
})
|
||||
});
|
||||
let user_properties = Some(serde_json::json!({
|
||||
"is_staff": body.is_staff,
|
||||
}));
|
||||
|
||||
Some(SnowflakeRow {
|
||||
time: timestamp,
|
||||
|
||||
@@ -11,7 +11,6 @@ pub struct UpdateBillingCustomerParams {
|
||||
pub user_id: ActiveValue<UserId>,
|
||||
pub stripe_customer_id: ActiveValue<String>,
|
||||
pub has_overdue_invoices: ActiveValue<bool>,
|
||||
pub trial_started_at: ActiveValue<Option<DateTime>>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
@@ -46,8 +45,7 @@ impl Database {
|
||||
user_id: params.user_id.clone(),
|
||||
stripe_customer_id: params.stripe_customer_id.clone(),
|
||||
has_overdue_invoices: params.has_overdue_invoices.clone(),
|
||||
trial_started_at: params.trial_started_at.clone(),
|
||||
created_at: ActiveValue::not_set(),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
@@ -10,7 +10,6 @@ pub struct Model {
|
||||
pub user_id: UserId,
|
||||
pub stripe_customer_id: String,
|
||||
pub has_overdue_invoices: bool,
|
||||
pub trial_started_at: Option<DateTime>,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,87 +1,8 @@
|
||||
use chrono::Timelike;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::{UserId, billing_subscription};
|
||||
use crate::db::UserId;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn convert_chrono_to_time(datetime: DateTimeUtc) -> anyhow::Result<PrimitiveDateTime> {
|
||||
use chrono::{Datelike as _, Timelike as _};
|
||||
|
||||
let date = time::Date::from_calendar_date(
|
||||
datetime.year(),
|
||||
time::Month::try_from(datetime.month() as u8).unwrap(),
|
||||
datetime.day() as u8,
|
||||
)?;
|
||||
|
||||
let time = time::Time::from_hms_nano(
|
||||
datetime.hour() as u8,
|
||||
datetime.minute() as u8,
|
||||
datetime.second() as u8,
|
||||
datetime.nanosecond(),
|
||||
)?;
|
||||
|
||||
Ok(PrimitiveDateTime::new(date, time))
|
||||
}
|
||||
|
||||
impl LlmDatabase {
|
||||
pub async fn create_subscription_usage(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
period_start_at: DateTimeUtc,
|
||||
period_end_at: DateTimeUtc,
|
||||
plan: SubscriptionKind,
|
||||
model_requests: i32,
|
||||
edit_predictions: i32,
|
||||
) -> Result<subscription_usage::Model> {
|
||||
self.transaction(|tx| async move {
|
||||
self.create_subscription_usage_in_tx(
|
||||
user_id,
|
||||
period_start_at,
|
||||
period_end_at,
|
||||
plan,
|
||||
model_requests,
|
||||
edit_predictions,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_subscription_usage_in_tx(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
period_start_at: DateTimeUtc,
|
||||
period_end_at: DateTimeUtc,
|
||||
plan: SubscriptionKind,
|
||||
model_requests: i32,
|
||||
edit_predictions: i32,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<subscription_usage::Model> {
|
||||
// Clear out the nanoseconds so that these timestamps are comparable with Unix timestamps.
|
||||
let period_start_at = period_start_at.with_nanosecond(0).unwrap();
|
||||
let period_end_at = period_end_at.with_nanosecond(0).unwrap();
|
||||
|
||||
let period_start_at = convert_chrono_to_time(period_start_at)?;
|
||||
let period_end_at = convert_chrono_to_time(period_end_at)?;
|
||||
|
||||
Ok(
|
||||
subscription_usage::Entity::insert(subscription_usage::ActiveModel {
|
||||
id: ActiveValue::not_set(),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
period_start_at: ActiveValue::set(period_start_at),
|
||||
period_end_at: ActiveValue::set(period_end_at),
|
||||
plan: ActiveValue::set(plan),
|
||||
model_requests: ActiveValue::set(model_requests),
|
||||
edit_predictions: ActiveValue::set(edit_predictions),
|
||||
})
|
||||
.exec_with_returning(tx)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_subscription_usage_for_period(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
@@ -89,77 +10,12 @@ impl LlmDatabase {
|
||||
period_end_at: DateTimeUtc,
|
||||
) -> Result<Option<subscription_usage::Model>> {
|
||||
self.transaction(|tx| async move {
|
||||
self.get_subscription_usage_for_period_in_tx(
|
||||
user_id,
|
||||
period_start_at,
|
||||
period_end_at,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_subscription_usage_for_period_in_tx(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
period_start_at: DateTimeUtc,
|
||||
period_end_at: DateTimeUtc,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<subscription_usage::Model>> {
|
||||
Ok(subscription_usage::Entity::find()
|
||||
.filter(subscription_usage::Column::UserId.eq(user_id))
|
||||
.filter(subscription_usage::Column::PeriodStartAt.eq(period_start_at))
|
||||
.filter(subscription_usage::Column::PeriodEndAt.eq(period_end_at))
|
||||
.one(tx)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn transfer_existing_subscription_usage(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
existing_subscription: &billing_subscription::Model,
|
||||
new_subscription_kind: Option<SubscriptionKind>,
|
||||
new_period_start_at: DateTimeUtc,
|
||||
new_period_end_at: DateTimeUtc,
|
||||
) -> Result<Option<subscription_usage::Model>> {
|
||||
self.transaction(|tx| async move {
|
||||
match existing_subscription.kind {
|
||||
Some(SubscriptionKind::ZedProTrial) => {
|
||||
let trial_period_start_at = existing_subscription
|
||||
.current_period_start_at()
|
||||
.ok_or_else(|| anyhow!("No trial subscription period start"))?;
|
||||
let trial_period_end_at = existing_subscription
|
||||
.current_period_end_at()
|
||||
.ok_or_else(|| anyhow!("No trial subscription period end"))?;
|
||||
|
||||
let existing_usage = self
|
||||
.get_subscription_usage_for_period_in_tx(
|
||||
user_id,
|
||||
trial_period_start_at,
|
||||
trial_period_end_at,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
if let Some(existing_usage) = existing_usage {
|
||||
return Ok(Some(
|
||||
self.create_subscription_usage_in_tx(
|
||||
user_id,
|
||||
new_period_start_at,
|
||||
new_period_end_at,
|
||||
new_subscription_kind.unwrap_or(existing_usage.plan),
|
||||
existing_usage.model_requests,
|
||||
existing_usage.edit_predictions,
|
||||
&tx,
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
Ok(subscription_usage::Entity::find()
|
||||
.filter(subscription_usage::Column::UserId.eq(user_id))
|
||||
.filter(subscription_usage::Column::PeriodStartAt.eq(period_start_at))
|
||||
.filter(subscription_usage::Column::PeriodEndAt.eq(period_end_at))
|
||||
.one(&*tx)
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
mod provider_tests;
|
||||
mod subscription_usage_tests;
|
||||
|
||||
use gpui::BackgroundExecutor;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::{UserId, billing_subscription};
|
||||
use crate::llm::db::LlmDatabase;
|
||||
use crate::test_llm_db;
|
||||
|
||||
test_llm_db!(
|
||||
test_transfer_existing_subscription_usage,
|
||||
test_transfer_existing_subscription_usage_postgres
|
||||
);
|
||||
|
||||
async fn test_transfer_existing_subscription_usage(db: &mut LlmDatabase) {
|
||||
let user_id = UserId(1);
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
let trial_period_start_at = now - Duration::days(14);
|
||||
let trial_period_end_at = now;
|
||||
|
||||
let new_period_start_at = now;
|
||||
let new_period_end_at = now + Duration::days(30);
|
||||
|
||||
let existing_subscription = billing_subscription::Model {
|
||||
kind: Some(SubscriptionKind::ZedProTrial),
|
||||
stripe_current_period_start: Some(trial_period_start_at.timestamp()),
|
||||
stripe_current_period_end: Some(trial_period_end_at.timestamp()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let existing_usage = db
|
||||
.create_subscription_usage(
|
||||
user_id,
|
||||
trial_period_start_at,
|
||||
trial_period_end_at,
|
||||
SubscriptionKind::ZedProTrial,
|
||||
25,
|
||||
1_000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let transferred_usage = db
|
||||
.transfer_existing_subscription_usage(
|
||||
user_id,
|
||||
&existing_subscription,
|
||||
Some(SubscriptionKind::ZedPro),
|
||||
new_period_start_at,
|
||||
new_period_end_at,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
transferred_usage.is_some(),
|
||||
"subscription usage not transferred successfully"
|
||||
);
|
||||
let transferred_usage = transferred_usage.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
transferred_usage.model_requests,
|
||||
existing_usage.model_requests
|
||||
);
|
||||
assert_eq!(
|
||||
transferred_usage.edit_predictions,
|
||||
existing_usage.edit_predictions
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::Cents;
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::{billing_subscription, user};
|
||||
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use crate::{Config, db::billing_preference};
|
||||
@@ -33,8 +32,6 @@ pub struct LlmTokenClaims {
|
||||
pub plan: Plan,
|
||||
#[serde(default)]
|
||||
pub subscription_period: Option<(NaiveDateTime, NaiveDateTime)>,
|
||||
#[serde(default)]
|
||||
pub can_use_web_search_tool: bool,
|
||||
}
|
||||
|
||||
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
|
||||
@@ -46,6 +43,7 @@ impl LlmTokenClaims {
|
||||
billing_preferences: Option<billing_preference::Model>,
|
||||
feature_flags: &Vec<String>,
|
||||
has_legacy_llm_subscription: bool,
|
||||
plan: rpc::proto::Plan,
|
||||
subscription: Option<billing_subscription::Model>,
|
||||
system_id: Option<String>,
|
||||
config: &Config,
|
||||
@@ -72,7 +70,6 @@ impl LlmTokenClaims {
|
||||
bypass_account_age_check: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == "bypass-account-age-check"),
|
||||
can_use_web_search_tool: feature_flags.iter().any(|flag| flag == "assistant2"),
|
||||
has_llm_subscription: has_legacy_llm_subscription,
|
||||
max_monthly_spend_in_cents: billing_preferences
|
||||
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {
|
||||
@@ -81,14 +78,11 @@ impl LlmTokenClaims {
|
||||
custom_llm_monthly_allowance_in_cents: user
|
||||
.custom_llm_monthly_allowance_in_cents
|
||||
.map(|allowance| allowance as u32),
|
||||
plan: subscription
|
||||
.as_ref()
|
||||
.and_then(|subscription| subscription.kind)
|
||||
.map_or(Plan::Free, |kind| match kind {
|
||||
SubscriptionKind::ZedFree => Plan::Free,
|
||||
SubscriptionKind::ZedPro => Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
|
||||
}),
|
||||
plan: match plan {
|
||||
rpc::proto::Plan::Free => Plan::Free,
|
||||
rpc::proto::Plan::ZedPro => Plan::ZedPro,
|
||||
rpc::proto::Plan::ZedProTrial => Plan::ZedProTrial,
|
||||
},
|
||||
subscription_period: maybe!({
|
||||
let subscription = subscription?;
|
||||
let period_start_at = subscription.current_period_start_at()?;
|
||||
|
||||
@@ -4147,6 +4147,7 @@ async fn get_llm_api_token(
|
||||
billing_preferences,
|
||||
&flags,
|
||||
has_legacy_llm_subscription,
|
||||
session.current_plan(&db).await?,
|
||||
billing_subscription,
|
||||
session.system_id.clone(),
|
||||
&session.app_state.config,
|
||||
|
||||
@@ -416,16 +416,9 @@ impl StripeBilling {
|
||||
let mut params = stripe::CreateCheckoutSession::new();
|
||||
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
|
||||
trial_period_days: Some(14),
|
||||
trial_settings: Some(stripe::CreateCheckoutSessionSubscriptionDataTrialSettings {
|
||||
end_behavior: stripe::CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior {
|
||||
missing_payment_method: stripe::CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod::Pause,
|
||||
}
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
|
||||
params.payment_method_collection =
|
||||
Some(stripe::CheckoutSessionPaymentMethodCollection::IfRequired);
|
||||
params.customer = Some(customer_id);
|
||||
params.client_reference_id = Some(github_login);
|
||||
params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
|
||||
|
||||
@@ -6,18 +6,17 @@ use collab_ui::{
|
||||
channel_view::ChannelView,
|
||||
notifications::project_shared_notification::ProjectSharedNotification,
|
||||
};
|
||||
use editor::{Editor, MultiBuffer, PathKey};
|
||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use gpui::{
|
||||
AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, TestAppContext,
|
||||
VisualContext, VisualTestContext, point,
|
||||
VisualTestContext, point,
|
||||
};
|
||||
use language::Capability;
|
||||
use project::WorktreeSettings;
|
||||
use rpc::proto::PeerId;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use text::{Point, ToPoint};
|
||||
use util::{path, test::sample_text};
|
||||
use util::path;
|
||||
use workspace::{SplitDirection, Workspace, item::ItemHandle as _};
|
||||
|
||||
use super::TestClient;
|
||||
@@ -296,20 +295,8 @@ async fn test_basic_following(
|
||||
.unwrap()
|
||||
});
|
||||
let mut result = MultiBuffer::new(Capability::ReadWrite);
|
||||
result.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer_a1, cx),
|
||||
buffer_a1,
|
||||
[Point::row_range(1..2)],
|
||||
1,
|
||||
cx,
|
||||
);
|
||||
result.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer_a2, cx),
|
||||
buffer_a2,
|
||||
[Point::row_range(5..6)],
|
||||
1,
|
||||
cx,
|
||||
);
|
||||
result.push_excerpts(buffer_a1, [ExcerptRange::new(0..3)], cx);
|
||||
result.push_excerpts(buffer_a2, [ExcerptRange::new(4..7)], cx);
|
||||
result
|
||||
});
|
||||
let multibuffer_editor_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
@@ -2083,83 +2070,6 @@ async fn share_workspace(
|
||||
.await
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_test_workspace(cx_a).await;
|
||||
join_channel(channel, &client_a, cx_a).await.unwrap();
|
||||
share_workspace(&workspace, cx_a).await.unwrap();
|
||||
let buffer = workspace.update(cx_a, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.create_local_buffer(&sample_text(26, 5, 'a'), None, cx)
|
||||
})
|
||||
});
|
||||
let multibuffer = cx_a.new(|cx| {
|
||||
let mut mb = MultiBuffer::new(Capability::ReadWrite);
|
||||
mb.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer.clone(),
|
||||
[Point::row_range(1..1), Point::row_range(5..5)],
|
||||
1,
|
||||
cx,
|
||||
);
|
||||
mb
|
||||
});
|
||||
let snapshot = buffer.update(cx_a, |buffer, _| buffer.snapshot());
|
||||
let editor: Entity<Editor> = cx_a.new_window_entity(|window, cx| {
|
||||
Editor::for_multibuffer(
|
||||
multibuffer.clone(),
|
||||
Some(workspace.read(cx).project().clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
workspace.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
|
||||
});
|
||||
editor.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_ranges([Point::row_range(4..4)]);
|
||||
})
|
||||
});
|
||||
let positions = editor.update(cx_a, |editor, _| {
|
||||
editor
|
||||
.selections
|
||||
.disjoint_anchor_ranges()
|
||||
.map(|range| range.start.text_anchor.to_point(&snapshot))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
multibuffer.update(cx_a, |multibuffer, cx| {
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer,
|
||||
[Point::row_range(1..5)],
|
||||
1,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let (workspace_b, cx_b) = client_b.join_workspace(channel, cx_b).await;
|
||||
cx_b.run_until_parked();
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let new_positions = editor_b.update(cx_b, |editor, _| {
|
||||
editor
|
||||
.selections
|
||||
.disjoint_anchor_ranges()
|
||||
.map(|range| range.start.text_anchor.to_point(&snapshot))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(positions, new_positions);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_following_to_channel_notes_other_workspace(
|
||||
cx_a: &mut TestAppContext,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use gpui::{App, Entity, Task};
|
||||
use icons::IconName;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -77,7 +77,6 @@ impl Tool for ContextServerTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
|
||||
|
||||
@@ -411,12 +411,12 @@ impl RunningState {
|
||||
.log_err();
|
||||
|
||||
if let Some(thread_id) = thread_id {
|
||||
this.select_thread(*thread_id, window, cx);
|
||||
this.select_thread(*thread_id, cx);
|
||||
}
|
||||
}
|
||||
SessionEvent::Threads => {
|
||||
let threads = this.session.update(cx, |this, cx| this.threads(cx));
|
||||
this.select_current_thread(&threads, window, cx);
|
||||
this.select_current_thread(&threads, cx);
|
||||
}
|
||||
SessionEvent::CapabilitiesLoaded => {
|
||||
let capabilities = this.capabilities(cx);
|
||||
@@ -731,7 +731,6 @@ impl RunningState {
|
||||
pub fn select_current_thread(
|
||||
&mut self,
|
||||
threads: &Vec<(Thread, ThreadStatus)>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let selected_thread = self
|
||||
@@ -744,7 +743,7 @@ impl RunningState {
|
||||
};
|
||||
|
||||
if Some(ThreadId(selected_thread.id)) != self.thread_id {
|
||||
self.select_thread(ThreadId(selected_thread.id), window, cx);
|
||||
self.select_thread(ThreadId(selected_thread.id), cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,7 +756,7 @@ impl RunningState {
|
||||
.map(|id| self.session().read(cx).thread_status(id))
|
||||
}
|
||||
|
||||
fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
|
||||
if self.thread_id.is_some_and(|id| id == thread_id) {
|
||||
return;
|
||||
}
|
||||
@@ -765,7 +764,8 @@ impl RunningState {
|
||||
self.thread_id = Some(thread_id);
|
||||
|
||||
self.stack_frame_list
|
||||
.update(cx, |list, cx| list.schedule_refresh(true, window, cx));
|
||||
.update(cx, |list, cx| list.refresh(cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -917,9 +917,9 @@ impl RunningState {
|
||||
for (thread, _) in threads {
|
||||
let state = state.clone();
|
||||
let thread_id = thread.id;
|
||||
this = this.entry(thread.name, None, move |window, cx| {
|
||||
this = this.entry(thread.name, None, move |_, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.select_thread(ThreadId(thread_id), window, cx);
|
||||
state.select_thread(ThreadId(thread_id), cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use dap::StackFrameId;
|
||||
@@ -29,11 +28,11 @@ pub struct StackFrameList {
|
||||
_subscription: Subscription,
|
||||
session: Entity<Session>,
|
||||
state: WeakEntity<RunningState>,
|
||||
invalidate: bool,
|
||||
entries: Vec<StackFrameEntry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
selected_stack_frame_id: Option<StackFrameId>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -69,17 +68,14 @@ impl StackFrameList {
|
||||
);
|
||||
|
||||
let _subscription =
|
||||
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
|
||||
SessionEvent::Threads => {
|
||||
this.schedule_refresh(false, window, cx);
|
||||
}
|
||||
SessionEvent::Stopped(..) | SessionEvent::StackTrace => {
|
||||
this.schedule_refresh(true, window, cx);
|
||||
cx.subscribe_in(&session, window, |this, _, event, _, cx| match event {
|
||||
SessionEvent::Stopped(_) | SessionEvent::StackTrace | SessionEvent::Threads => {
|
||||
this.refresh(cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
Self {
|
||||
scrollbar_state: ScrollbarState::new(list.clone()),
|
||||
list,
|
||||
session,
|
||||
@@ -87,12 +83,10 @@ impl StackFrameList {
|
||||
focus_handle,
|
||||
state,
|
||||
_subscription,
|
||||
invalidate: true,
|
||||
entries: Default::default(),
|
||||
selected_stack_frame_id: None,
|
||||
_refresh_task: Task::ready(()),
|
||||
};
|
||||
this.schedule_refresh(true, window, cx);
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -142,32 +136,10 @@ impl StackFrameList {
|
||||
self.selected_stack_frame_id
|
||||
}
|
||||
|
||||
pub(super) fn schedule_refresh(
|
||||
&mut self,
|
||||
select_first: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(20);
|
||||
|
||||
self._refresh_task = cx.spawn_in(window, async move |this, cx| {
|
||||
let debounce = this
|
||||
.update(cx, |this, cx| {
|
||||
let new_stack_frames = this.stack_frames(cx);
|
||||
new_stack_frames.is_empty() && !this.entries.is_empty()
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
if debounce {
|
||||
cx.background_executor().timer(REFRESH_DEBOUNCE).await;
|
||||
}
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.build_entries(select_first, window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
pub(super) fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||
self.invalidate = true;
|
||||
self.entries.clear();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn build_entries(
|
||||
@@ -543,7 +515,13 @@ impl StackFrameList {
|
||||
}
|
||||
|
||||
impl Render for StackFrameList {
|
||||
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 {
|
||||
if self.invalidate {
|
||||
self.build_entries(self.entries.is_empty(), window, cx);
|
||||
self.invalidate = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.p_1()
|
||||
|
||||
@@ -152,7 +152,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
|
||||
cx.run_until_parked();
|
||||
|
||||
// select first thread
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
@@ -162,7 +162,6 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -331,7 +330,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
cx.run_until_parked();
|
||||
|
||||
// select first thread
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
@@ -341,7 +340,6 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -706,7 +704,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
cx.run_until_parked();
|
||||
|
||||
// select first thread
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
@@ -716,7 +714,6 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -760,7 +760,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
|
||||
|
||||
// The mutated view may contain more than the reference view as
|
||||
// we don't currently shrink excerpts when diagnostics were removed.
|
||||
let mut ref_iter = reference_excerpts.lines().filter(|line| *line != "§ -----");
|
||||
let mut ref_iter = reference_excerpts.lines();
|
||||
let mut next_ref_line = ref_iter.next();
|
||||
let mut skipped_block = false;
|
||||
|
||||
@@ -768,7 +768,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
|
||||
if let Some(ref_line) = next_ref_line {
|
||||
if mut_line == ref_line {
|
||||
next_ref_line = ref_iter.next();
|
||||
} else if mut_line.contains('§') && mut_line != "§ -----" {
|
||||
} else if mut_line.contains('§') {
|
||||
skipped_block = true;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,8 +27,8 @@ use util::ResultExt;
|
||||
|
||||
use crate::hover_popover::{hover_markdown_style, open_markdown_url};
|
||||
use crate::{
|
||||
CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
|
||||
EditorStyle, ResolvedTasks,
|
||||
CodeActionProvider, CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle,
|
||||
ResolvedTasks,
|
||||
actions::{ConfirmCodeAction, ConfirmCompletion},
|
||||
split_words, styled_runs_for_code_label,
|
||||
};
|
||||
@@ -657,63 +657,6 @@ impl CompletionsMenu {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sort_matches(matches: &mut Vec<SortableMatch<'_>>, query: Option<&str>) {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum MatchTier<'a> {
|
||||
WordStartMatch {
|
||||
sort_score_int: Reverse<i32>,
|
||||
sort_snippet: Reverse<i32>,
|
||||
sort_text: Option<&'a str>,
|
||||
sort_key: (usize, &'a str),
|
||||
},
|
||||
OtherMatch {
|
||||
sort_score: Reverse<OrderedFloat<f64>>,
|
||||
},
|
||||
}
|
||||
|
||||
// Our goal here is to intelligently sort completion suggestions. We want to
|
||||
// balance the raw fuzzy match score with hints from the language server
|
||||
//
|
||||
// We first primary sort using fuzzy score by putting matches into two buckets
|
||||
// strong one and weak one. Among these buckets matches are then compared by
|
||||
// various criteria like snippet, LSP hints, kind, label text etc.
|
||||
//
|
||||
const FUZZY_THRESHOLD: f64 = 0.1317;
|
||||
|
||||
let query_start_lower = query
|
||||
.and_then(|q| q.chars().next())
|
||||
.and_then(|c| c.to_lowercase().next());
|
||||
|
||||
matches.sort_unstable_by_key(|mat| {
|
||||
let score = mat.string_match.score;
|
||||
|
||||
let is_other_match = query_start_lower
|
||||
.map(|query_char| {
|
||||
!split_words(&mat.string_match.string).any(|word| {
|
||||
word.chars()
|
||||
.next()
|
||||
.and_then(|c| c.to_lowercase().next())
|
||||
.map_or(false, |word_char| word_char == query_char)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_other_match {
|
||||
let sort_score = Reverse(OrderedFloat(score));
|
||||
MatchTier::OtherMatch { sort_score }
|
||||
} else {
|
||||
let sort_score_int = Reverse(if score >= FUZZY_THRESHOLD { 1 } else { 0 });
|
||||
let sort_snippet = Reverse(if mat.is_snippet { 1 } else { 0 });
|
||||
MatchTier::WordStartMatch {
|
||||
sort_score_int,
|
||||
sort_snippet,
|
||||
sort_text: mat.sort_text,
|
||||
sort_key: mat.sort_key,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
|
||||
let mut matches = if let Some(query) = query {
|
||||
fuzzy::match_strings(
|
||||
@@ -738,45 +681,85 @@ impl CompletionsMenu {
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut additional_matches = Vec::new();
|
||||
// Deprioritize all candidates where the query's start does not match the start of any word in the candidate
|
||||
if let Some(query) = query {
|
||||
if let Some(query_start) = query.chars().next() {
|
||||
let (primary, secondary) = matches.into_iter().partition(|string_match| {
|
||||
split_words(&string_match.string).any(|word| {
|
||||
// Check that the first codepoint of the word as lowercase matches the first
|
||||
// codepoint of the query as lowercase
|
||||
word.chars()
|
||||
.flat_map(|codepoint| codepoint.to_lowercase())
|
||||
.zip(query_start.to_lowercase())
|
||||
.all(|(word_cp, query_cp)| word_cp == query_cp)
|
||||
})
|
||||
});
|
||||
matches = primary;
|
||||
additional_matches = secondary;
|
||||
}
|
||||
}
|
||||
|
||||
let completions = self.completions.borrow_mut();
|
||||
if self.sort_completions {
|
||||
let completions = self.completions.borrow();
|
||||
matches.sort_unstable_by_key(|mat| {
|
||||
// We do want to strike a balance here between what the language server tells us
|
||||
// to sort by (the sort_text) and what are "obvious" good matches (i.e. when you type
|
||||
// `Creat` and there is a local variable called `CreateComponent`).
|
||||
// So what we do is: we bucket all matches into two buckets
|
||||
// - Strong matches
|
||||
// - Weak matches
|
||||
// Strong matches are the ones with a high fuzzy-matcher score (the "obvious" matches)
|
||||
// and the Weak matches are the rest.
|
||||
//
|
||||
// For the strong matches, we sort by our fuzzy-finder score first and for the weak
|
||||
// matches, we prefer language-server sort_text first.
|
||||
//
|
||||
// The thinking behind that: we want to show strong matches first in order of relevance(fuzzy score).
|
||||
// Rest of the matches(weak) can be sorted as language-server expects.
|
||||
|
||||
let mut sortable_items: Vec<SortableMatch<'_>> = matches
|
||||
.into_iter()
|
||||
.map(|string_match| {
|
||||
let completion = &completions[string_match.candidate_id];
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum MatchScore<'a> {
|
||||
Strong {
|
||||
score: Reverse<OrderedFloat<f64>>,
|
||||
sort_text: Option<&'a str>,
|
||||
sort_key: (usize, &'a str),
|
||||
},
|
||||
Weak {
|
||||
sort_text: Option<&'a str>,
|
||||
score: Reverse<OrderedFloat<f64>>,
|
||||
sort_key: (usize, &'a str),
|
||||
},
|
||||
}
|
||||
|
||||
let is_snippet = matches!(
|
||||
&completion.source,
|
||||
CompletionSource::Lsp { lsp_completion, .. }
|
||||
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
|
||||
);
|
||||
let completion = &completions[mat.candidate_id];
|
||||
let sort_key = completion.sort_key();
|
||||
let sort_text =
|
||||
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
|
||||
lsp_completion.sort_text.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let score = Reverse(OrderedFloat(mat.score));
|
||||
|
||||
let sort_text =
|
||||
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
|
||||
lsp_completion.sort_text.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let sort_key = completion.sort_key();
|
||||
|
||||
SortableMatch {
|
||||
string_match,
|
||||
is_snippet,
|
||||
if mat.score >= 0.2 {
|
||||
MatchScore::Strong {
|
||||
score,
|
||||
sort_text,
|
||||
sort_key,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self::sort_matches(&mut sortable_items, query);
|
||||
|
||||
matches = sortable_items
|
||||
.into_iter()
|
||||
.map(|sortable| sortable.string_match)
|
||||
.collect();
|
||||
} else {
|
||||
MatchScore::Weak {
|
||||
sort_text,
|
||||
score,
|
||||
sort_key,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
drop(completions);
|
||||
|
||||
matches.extend(additional_matches);
|
||||
|
||||
*self.entries.borrow_mut() = matches;
|
||||
self.selected_item = 0;
|
||||
@@ -785,14 +768,6 @@ impl CompletionsMenu {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SortableMatch<'a> {
|
||||
pub string_match: StringMatch,
|
||||
pub is_snippet: bool,
|
||||
pub sort_text: Option<&'a str>,
|
||||
pub sort_key: (usize, &'a str),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AvailableCodeAction {
|
||||
pub excerpt_id: ExcerptId,
|
||||
|
||||
@@ -808,14 +808,10 @@ impl DisplaySnapshot {
|
||||
// used by line_mode selections and tries to match vim behavior
|
||||
pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
|
||||
let new_start = MultiBufferPoint::new(range.start.row, 0);
|
||||
let new_end = if range.end.column > 0 {
|
||||
MultiBufferPoint::new(
|
||||
range.end.row,
|
||||
self.buffer_snapshot.line_len(MultiBufferRow(range.end.row)),
|
||||
)
|
||||
} else {
|
||||
range.end
|
||||
};
|
||||
let new_end = MultiBufferPoint::new(
|
||||
range.end.row,
|
||||
self.buffer_snapshot.line_len(MultiBufferRow(range.end.row)),
|
||||
);
|
||||
|
||||
new_start..new_end
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ pub mod scroll;
|
||||
mod selections_collection;
|
||||
pub mod tasks;
|
||||
|
||||
#[cfg(test)]
|
||||
mod code_completion_tests;
|
||||
#[cfg(test)]
|
||||
mod editor_tests;
|
||||
#[cfg(test)]
|
||||
@@ -269,12 +267,6 @@ enum DocumentHighlightWrite {}
|
||||
enum InputComposition {}
|
||||
enum SelectedTextHighlight {}
|
||||
|
||||
pub enum ConflictsOuter {}
|
||||
pub enum ConflictsOurs {}
|
||||
pub enum ConflictsTheirs {}
|
||||
pub enum ConflictsOursMarker {}
|
||||
pub enum ConflictsTheirsMarker {}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Navigated {
|
||||
Yes,
|
||||
@@ -433,8 +425,6 @@ pub enum EditorMode {
|
||||
scale_ui_elements_with_buffer_font_size: bool,
|
||||
/// When set to `true`, the editor will render a background for the active line.
|
||||
show_active_line_background: bool,
|
||||
/// When set to `true`, the editor's height will be determined by its content.
|
||||
sized_by_content: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -443,7 +433,6 @@ impl EditorMode {
|
||||
Self::Full {
|
||||
scale_ui_elements_with_buffer_font_size: true,
|
||||
show_active_line_background: true,
|
||||
sized_by_content: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -703,10 +692,6 @@ pub trait Addon: 'static {
|
||||
}
|
||||
|
||||
fn to_any(&self) -> &dyn std::any::Any;
|
||||
|
||||
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of caret positions, registered when the editor was edited.
|
||||
@@ -801,8 +786,6 @@ pub struct Editor {
|
||||
show_breadcrumbs: bool,
|
||||
show_gutter: bool,
|
||||
show_scrollbars: bool,
|
||||
disable_scrolling: bool,
|
||||
disable_expand_excerpt_buttons: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
use_relative_line_numbers: Option<bool>,
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
@@ -1098,27 +1081,11 @@ impl SelectionHistory {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RowHighlightOptions {
|
||||
pub autoscroll: bool,
|
||||
pub include_gutter: bool,
|
||||
}
|
||||
|
||||
impl Default for RowHighlightOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
autoscroll: Default::default(),
|
||||
include_gutter: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RowHighlight {
|
||||
index: usize,
|
||||
range: Range<Anchor>,
|
||||
color: Hsla,
|
||||
options: RowHighlightOptions,
|
||||
type_id: TypeId,
|
||||
should_autoscroll: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -1594,13 +1561,11 @@ impl Editor {
|
||||
blink_manager: blink_manager.clone(),
|
||||
show_local_selections: true,
|
||||
show_scrollbars: true,
|
||||
disable_scrolling: true,
|
||||
mode,
|
||||
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
||||
show_gutter: mode.is_full(),
|
||||
show_line_numbers: None,
|
||||
use_relative_line_numbers: None,
|
||||
disable_expand_excerpt_buttons: false,
|
||||
show_git_diff_gutter: None,
|
||||
show_code_actions: None,
|
||||
show_runnables: None,
|
||||
@@ -3143,13 +3108,6 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn has_non_empty_selection(&self, cx: &mut App) -> bool {
|
||||
self.selections
|
||||
.all_adjusted(cx)
|
||||
.iter()
|
||||
.any(|selection| !selection.is_empty())
|
||||
}
|
||||
|
||||
pub fn has_pending_nonempty_selection(&self) -> bool {
|
||||
let pending_nonempty_selection = match self.selections.pending_anchor() {
|
||||
Some(Selection { start, end, .. }) => start != end,
|
||||
@@ -5975,10 +5933,7 @@ impl Editor {
|
||||
self.highlight_rows::<EditPredictionPreview>(
|
||||
target..target,
|
||||
cx.theme().colors().editor_highlighted_line_background,
|
||||
RowHighlightOptions {
|
||||
autoscroll: true,
|
||||
..Default::default()
|
||||
},
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
@@ -11834,7 +11789,6 @@ impl Editor {
|
||||
fn select_next_match_ranges(
|
||||
this: &mut Editor,
|
||||
range: Range<usize>,
|
||||
reversed: bool,
|
||||
replace_newest: bool,
|
||||
auto_scroll: Option<Autoscroll>,
|
||||
window: &mut Window,
|
||||
@@ -11845,11 +11799,7 @@ impl Editor {
|
||||
if replace_newest {
|
||||
s.delete(s.newest_anchor().id);
|
||||
}
|
||||
if reversed {
|
||||
s.insert_range(range.end..range.start);
|
||||
} else {
|
||||
s.insert_range(range);
|
||||
}
|
||||
s.insert_range(range.clone());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11900,7 +11850,6 @@ impl Editor {
|
||||
select_next_match_ranges(
|
||||
self,
|
||||
next_selected_range,
|
||||
last_selection.reversed,
|
||||
replace_newest,
|
||||
autoscroll,
|
||||
window,
|
||||
@@ -11959,7 +11908,6 @@ impl Editor {
|
||||
select_next_match_ranges(
|
||||
self,
|
||||
selection.start..selection.end,
|
||||
selection.reversed,
|
||||
replace_newest,
|
||||
autoscroll,
|
||||
window,
|
||||
@@ -12127,11 +12075,7 @@ impl Editor {
|
||||
if action.replace_newest {
|
||||
s.delete(s.newest_anchor().id);
|
||||
}
|
||||
if last_selection.reversed {
|
||||
s.insert_range(next_selected_range.end..next_selected_range.start);
|
||||
} else {
|
||||
s.insert_range(next_selected_range);
|
||||
}
|
||||
s.insert_range(next_selected_range);
|
||||
});
|
||||
} else {
|
||||
select_prev_state.done = true;
|
||||
@@ -13496,7 +13440,7 @@ impl Editor {
|
||||
start..end,
|
||||
highlight_color
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
|
||||
Default::default(),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
|
||||
@@ -16193,21 +16137,11 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn disable_scrolling(&mut self, cx: &mut Context<Self>) {
|
||||
self.disable_scrolling = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context<Self>) {
|
||||
self.show_line_numbers = Some(show_line_numbers);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn disable_expand_excerpt_buttons(&mut self, cx: &mut Context<Self>) {
|
||||
self.disable_expand_excerpt_buttons = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context<Self>) {
|
||||
self.show_git_diff_gutter = Some(show_git_diff_gutter);
|
||||
cx.notify();
|
||||
@@ -16822,7 +16756,7 @@ impl Editor {
|
||||
&mut self,
|
||||
range: Range<Anchor>,
|
||||
color: Hsla,
|
||||
options: RowHighlightOptions,
|
||||
should_autoscroll: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
@@ -16854,7 +16788,7 @@ impl Editor {
|
||||
merged = true;
|
||||
prev_highlight.index = index;
|
||||
prev_highlight.color = color;
|
||||
prev_highlight.options = options;
|
||||
prev_highlight.should_autoscroll = should_autoscroll;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16865,8 +16799,7 @@ impl Editor {
|
||||
range: range.clone(),
|
||||
index,
|
||||
color,
|
||||
options,
|
||||
type_id: TypeId::of::<T>(),
|
||||
should_autoscroll,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -16972,15 +16905,7 @@ impl Editor {
|
||||
used_highlight_orders.entry(row).or_insert(highlight.index);
|
||||
if highlight.index >= *used_index {
|
||||
*used_index = highlight.index;
|
||||
unique_rows.insert(
|
||||
DisplayRow(row),
|
||||
LineHighlight {
|
||||
include_gutter: highlight.options.include_gutter,
|
||||
border: None,
|
||||
background: highlight.color.into(),
|
||||
type_id: Some(highlight.type_id),
|
||||
},
|
||||
);
|
||||
unique_rows.insert(DisplayRow(row), highlight.color.into());
|
||||
}
|
||||
}
|
||||
unique_rows
|
||||
@@ -16996,7 +16921,7 @@ impl Editor {
|
||||
.values()
|
||||
.flat_map(|highlighted_rows| highlighted_rows.iter())
|
||||
.filter_map(|highlight| {
|
||||
if highlight.options.autoscroll {
|
||||
if highlight.should_autoscroll {
|
||||
Some(highlight.range.start.to_display_point(snapshot).row())
|
||||
} else {
|
||||
None
|
||||
@@ -17471,19 +17396,13 @@ impl Editor {
|
||||
});
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
}
|
||||
multi_buffer::Event::ExcerptsRemoved {
|
||||
ids,
|
||||
removed_buffer_ids,
|
||||
} => {
|
||||
multi_buffer::Event::ExcerptsRemoved { ids } => {
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
|
||||
let buffer = self.buffer.read(cx);
|
||||
self.registered_buffers
|
||||
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
cx.emit(EditorEvent::ExcerptsRemoved {
|
||||
ids: ids.clone(),
|
||||
removed_buffer_ids: removed_buffer_ids.clone(),
|
||||
})
|
||||
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
|
||||
}
|
||||
multi_buffer::Event::ExcerptsEdited {
|
||||
excerpt_ids,
|
||||
@@ -18291,13 +18210,6 @@ impl Editor {
|
||||
.and_then(|item| item.to_any().downcast_ref::<T>())
|
||||
}
|
||||
|
||||
pub fn addon_mut<T: Addon>(&mut self) -> Option<&mut T> {
|
||||
let type_id = std::any::TypeId::of::<T>();
|
||||
self.addons
|
||||
.get_mut(&type_id)
|
||||
.and_then(|item| item.to_any_mut()?.downcast_mut::<T>())
|
||||
}
|
||||
|
||||
fn character_size(&self, window: &mut Window) -> gpui::Size<Pixels> {
|
||||
let text_layout_details = self.text_layout_details(window);
|
||||
let style = &text_layout_details.editor_style;
|
||||
@@ -19811,7 +19723,6 @@ pub enum EditorEvent {
|
||||
},
|
||||
ExcerptsRemoved {
|
||||
ids: Vec<ExcerptId>,
|
||||
removed_buffer_ids: Vec<BufferId>,
|
||||
},
|
||||
BufferFoldToggled {
|
||||
ids: Vec<ExcerptId>,
|
||||
@@ -20752,8 +20663,24 @@ impl Render for MissingEditPredictionKeybindingTooltip {
|
||||
pub struct LineHighlight {
|
||||
pub background: Background,
|
||||
pub border: Option<gpui::Hsla>,
|
||||
pub include_gutter: bool,
|
||||
pub type_id: Option<TypeId>,
|
||||
}
|
||||
|
||||
impl From<Hsla> for LineHighlight {
|
||||
fn from(hsla: Hsla) -> Self {
|
||||
Self {
|
||||
background: hsla.into(),
|
||||
border: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Background> for LineHighlight {
|
||||
fn from(background: Background) -> Self {
|
||||
Self {
|
||||
background,
|
||||
border: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_diff_hunk_controls(
|
||||
|
||||
@@ -5825,13 +5825,6 @@ async fn test_select_next(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||||
|
||||
// Test selection direction should be preserved
|
||||
cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -6018,25 +6011,6 @@ let «fooˇ» = 2;
|
||||
let foo = 2;
|
||||
let foo = «2ˇ»;"#,
|
||||
);
|
||||
|
||||
// Test last selection direction should be preserved
|
||||
cx.set_state(
|
||||
r#"let foo = 2;
|
||||
let foo = 2;
|
||||
let «fooˇ» = 2;
|
||||
let «ˇfoo» = 2;
|
||||
let foo = 2;"#,
|
||||
);
|
||||
|
||||
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state(
|
||||
r#"let foo = 2;
|
||||
let foo = 2;
|
||||
let «fooˇ» = 2;
|
||||
let «ˇfoo» = 2;
|
||||
let «ˇfoo» = 2;"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -6164,26 +6138,25 @@ async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
|
||||
|
||||
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
|
||||
.unwrap();
|
||||
// selection direction is preserved
|
||||
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
|
||||
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
|
||||
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -10446,7 +10419,6 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte
|
||||
EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: false,
|
||||
},
|
||||
multi_buffer.clone(),
|
||||
Some(project.clone()),
|
||||
@@ -10732,7 +10704,7 @@ async fn test_completion(cx: &mut TestAppContext) {
|
||||
.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state("editor.clobberˇ");
|
||||
cx.assert_editor_state("editor.closeˇ");
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
}
|
||||
@@ -11294,6 +11266,76 @@ async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion_sort(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
cx.lsp
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "Range".into(),
|
||||
sort_text: Some("a".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "r".into(),
|
||||
sort_text: Some("b".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "ret".into(),
|
||||
sort_text: Some("c".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "return".into(),
|
||||
sort_text: Some("d".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "slice".into(),
|
||||
sort_text: Some("d".into()),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.set_state("rˇ");
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(
|
||||
&ShowCompletions {
|
||||
trigger: Some("r".into()),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu),
|
||||
&["r", "ret", "Range", "return"]
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_as_is_completions(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -12657,22 +12699,19 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
|
||||
// Insert some excerpts.
|
||||
leader.update(cx, |leader, cx| {
|
||||
leader.buffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
|
||||
let excerpt_ids = multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
vec![
|
||||
Point::row_range(0..3),
|
||||
Point::row_range(1..6),
|
||||
Point::row_range(12..15),
|
||||
[
|
||||
ExcerptRange::new(1..6),
|
||||
ExcerptRange::new(12..15),
|
||||
ExcerptRange::new(0..3),
|
||||
],
|
||||
0,
|
||||
cx,
|
||||
);
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
|
||||
multibuffer.insert_excerpts_after(
|
||||
excerpt_ids[0],
|
||||
buffer_2.clone(),
|
||||
vec![Point::row_range(0..6), Point::row_range(8..12)],
|
||||
0,
|
||||
[ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -14022,7 +14061,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu),
|
||||
&["bg-blue", "bg-red", "bg-yellow"]
|
||||
&["bg-red", "bg-blue", "bg-yellow"]
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
|
||||
ChunkRendererContext, ChunkReplacement, ConflictsOurs, ConflictsOursMarker, ConflictsOuter,
|
||||
ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId,
|
||||
ChunkRendererContext, ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId,
|
||||
DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
|
||||
EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
|
||||
FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
|
||||
@@ -2184,10 +2183,6 @@ impl EditorElement {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Vec<Option<(AnyElement, gpui::Point<Pixels>)>> {
|
||||
if self.editor.read(cx).disable_expand_excerpt_buttons {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let editor_font_size = self.style.text.font_size.to_pixels(window.rem_size()) * 1.2;
|
||||
|
||||
let scroll_top = scroll_position.y * line_height;
|
||||
@@ -4041,7 +4036,6 @@ impl EditorElement {
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||||
highlighted_rows: &BTreeMap<DisplayRow, LineHighlight>,
|
||||
editor: Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -4070,22 +4064,6 @@ impl EditorElement {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if highlighted_rows
|
||||
.get(&display_row_range.start)
|
||||
.and_then(|highlight| highlight.type_id)
|
||||
.is_some_and(|type_id| {
|
||||
[
|
||||
TypeId::of::<ConflictsOuter>(),
|
||||
TypeId::of::<ConflictsOursMarker>(),
|
||||
TypeId::of::<ConflictsOurs>(),
|
||||
TypeId::of::<ConflictsTheirs>(),
|
||||
TypeId::of::<ConflictsTheirsMarker>(),
|
||||
]
|
||||
.contains(&type_id)
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let row_ix = (display_row_range.start - row_range.start).0 as usize;
|
||||
if row_infos[row_ix].diff_status.is_none() {
|
||||
continue;
|
||||
@@ -4280,21 +4258,14 @@ impl EditorElement {
|
||||
highlight_row_end: DisplayRow,
|
||||
highlight: crate::LineHighlight,
|
||||
edges| {
|
||||
let mut origin_x = layout.hitbox.left();
|
||||
let mut width = layout.hitbox.size.width;
|
||||
if !highlight.include_gutter {
|
||||
origin_x += layout.gutter_hitbox.size.width;
|
||||
width -= layout.gutter_hitbox.size.width;
|
||||
}
|
||||
|
||||
let origin = point(
|
||||
origin_x,
|
||||
layout.hitbox.origin.x,
|
||||
layout.hitbox.origin.y
|
||||
+ (highlight_row_start.as_f32() - scroll_top)
|
||||
* layout.position_map.line_height,
|
||||
);
|
||||
let size = size(
|
||||
width,
|
||||
layout.hitbox.size.width,
|
||||
layout.position_map.line_height
|
||||
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
|
||||
);
|
||||
@@ -5541,9 +5512,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
|
||||
if !self.editor.read(cx).disable_scrolling {
|
||||
self.paint_scroll_wheel_listener(layout, window, cx);
|
||||
}
|
||||
self.paint_scroll_wheel_listener(layout, window, cx);
|
||||
|
||||
window.on_mouse_event({
|
||||
let position_map = layout.position_map.clone();
|
||||
@@ -6594,21 +6563,10 @@ impl Element for EditorElement {
|
||||
},
|
||||
)
|
||||
}
|
||||
EditorMode::Full {
|
||||
sized_by_content, ..
|
||||
} => {
|
||||
EditorMode::Full { .. } => {
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
if sized_by_content {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let line_height =
|
||||
self.style.text.line_height_in_pixels(window.rem_size());
|
||||
let scroll_height =
|
||||
(snapshot.max_point().row().next_row().0 as f32) * line_height;
|
||||
style.size.height = scroll_height.into();
|
||||
} else {
|
||||
style.size.height = relative(1.).into();
|
||||
}
|
||||
style.size.height = relative(1.).into();
|
||||
window.request_layout(style, None, cx)
|
||||
}
|
||||
};
|
||||
@@ -6831,16 +6789,10 @@ impl Element for EditorElement {
|
||||
} else {
|
||||
background_color.opacity(0.36)
|
||||
}),
|
||||
include_gutter: true,
|
||||
type_id: None,
|
||||
};
|
||||
|
||||
let filled_highlight = LineHighlight {
|
||||
background: solid_background(background_color.opacity(hunk_opacity)),
|
||||
border: None,
|
||||
include_gutter: true,
|
||||
type_id: None,
|
||||
};
|
||||
let filled_highlight =
|
||||
solid_background(background_color.opacity(hunk_opacity)).into();
|
||||
|
||||
let background = if Self::diff_hunk_hollow(diff_status, cx) {
|
||||
hollow_highlight
|
||||
@@ -7599,7 +7551,6 @@ impl Element for EditorElement {
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&display_hunks,
|
||||
&highlighted_rows,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -99,40 +99,25 @@ impl FollowableItem for Editor {
|
||||
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
|
||||
} else {
|
||||
multibuffer = MultiBuffer::new(project.read(cx).capability());
|
||||
let mut sorted_excerpts = state.excerpts.clone();
|
||||
sorted_excerpts.sort_by_key(|e| e.id);
|
||||
let mut sorted_excerpts = sorted_excerpts.into_iter().peekable();
|
||||
|
||||
while let Some(excerpt) = sorted_excerpts.next() {
|
||||
let mut excerpts = state.excerpts.into_iter().peekable();
|
||||
while let Some(excerpt) = excerpts.peek() {
|
||||
let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut insert_position = ExcerptId::min();
|
||||
for e in &state.excerpts {
|
||||
if e.id == excerpt.id {
|
||||
break;
|
||||
}
|
||||
if e.id < excerpt.id {
|
||||
insert_position = ExcerptId::from_proto(e.id);
|
||||
}
|
||||
}
|
||||
|
||||
let buffer_excerpts = iter::from_fn(|| {
|
||||
let excerpt = excerpts.peek()?;
|
||||
(excerpt.buffer_id == u64::from(buffer_id))
|
||||
.then(|| excerpts.next().unwrap())
|
||||
});
|
||||
let buffer =
|
||||
buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
|
||||
|
||||
let Some(excerpt) = deserialize_excerpt_range(excerpt) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(buffer) = buffer else { continue };
|
||||
|
||||
multibuffer.insert_excerpts_with_ids_after(
|
||||
insert_position,
|
||||
buffer.clone(),
|
||||
[excerpt],
|
||||
cx,
|
||||
);
|
||||
if let Some(buffer) = buffer {
|
||||
multibuffer.push_excerpts(
|
||||
buffer.clone(),
|
||||
buffer_excerpts.filter_map(deserialize_excerpt_range),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -217,26 +202,25 @@ impl FollowableItem for Editor {
|
||||
primary_end: Some(serialize_text_anchor(&range.primary.end)),
|
||||
})
|
||||
.collect();
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
|
||||
Some(proto::view::Variant::Editor(proto::view::Editor {
|
||||
singleton: buffer.is_singleton(),
|
||||
title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
|
||||
excerpts,
|
||||
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor, &snapshot)),
|
||||
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)),
|
||||
scroll_x: scroll_anchor.offset.x,
|
||||
scroll_y: scroll_anchor.offset.y,
|
||||
selections: self
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.map(|s| serialize_selection(s, &snapshot))
|
||||
.map(serialize_selection)
|
||||
.collect(),
|
||||
pending_selection: self
|
||||
.selections
|
||||
.pending_anchor()
|
||||
.as_ref()
|
||||
.map(|s| serialize_selection(s, &snapshot)),
|
||||
.map(serialize_selection),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -288,34 +272,31 @@ impl FollowableItem for Editor {
|
||||
}
|
||||
true
|
||||
}
|
||||
EditorEvent::ExcerptsRemoved { ids, .. } => {
|
||||
EditorEvent::ExcerptsRemoved { ids } => {
|
||||
update
|
||||
.deleted_excerpts
|
||||
.extend(ids.iter().map(ExcerptId::to_proto));
|
||||
true
|
||||
}
|
||||
EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let scroll_anchor = self.scroll_manager.anchor();
|
||||
update.scroll_top_anchor =
|
||||
Some(serialize_anchor(&scroll_anchor.anchor, &snapshot));
|
||||
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
|
||||
update.scroll_x = scroll_anchor.offset.x;
|
||||
update.scroll_y = scroll_anchor.offset.y;
|
||||
true
|
||||
}
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
update.selections = self
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.map(|s| serialize_selection(s, &snapshot))
|
||||
.map(serialize_selection)
|
||||
.collect();
|
||||
update.pending_selection = self
|
||||
.selections
|
||||
.pending_anchor()
|
||||
.as_ref()
|
||||
.map(|s| serialize_selection(s, &snapshot));
|
||||
.map(serialize_selection);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
@@ -415,7 +396,12 @@ async fn update_editor_from_message(
|
||||
[excerpt]
|
||||
.into_iter()
|
||||
.chain(adjacent_excerpts)
|
||||
.filter_map(deserialize_excerpt_range),
|
||||
.filter_map(|excerpt| {
|
||||
Some((
|
||||
ExcerptId::from_proto(excerpt.id),
|
||||
deserialize_excerpt_range(excerpt)?,
|
||||
))
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -492,28 +478,23 @@ fn serialize_excerpt(
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_selection(
|
||||
selection: &Selection<Anchor>,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
) -> proto::Selection {
|
||||
fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
|
||||
proto::Selection {
|
||||
id: selection.id as u64,
|
||||
start: Some(serialize_anchor(&selection.start, &buffer)),
|
||||
end: Some(serialize_anchor(&selection.end, &buffer)),
|
||||
start: Some(serialize_anchor(&selection.start)),
|
||||
end: Some(serialize_anchor(&selection.end)),
|
||||
reversed: selection.reversed,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_anchor(anchor: &Anchor, buffer: &MultiBufferSnapshot) -> proto::EditorAnchor {
|
||||
fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor {
|
||||
proto::EditorAnchor {
|
||||
excerpt_id: buffer.latest_excerpt_id(anchor.excerpt_id).to_proto(),
|
||||
excerpt_id: anchor.excerpt_id.to_proto(),
|
||||
anchor: Some(serialize_text_anchor(&anchor.text_anchor)),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_excerpt_range(
|
||||
excerpt: proto::Excerpt,
|
||||
) -> Option<(ExcerptId, ExcerptRange<language::Anchor>)> {
|
||||
fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option<ExcerptRange<language::Anchor>> {
|
||||
let context = {
|
||||
let start = language::proto::deserialize_anchor(excerpt.context_start?)?;
|
||||
let end = language::proto::deserialize_anchor(excerpt.context_end?)?;
|
||||
@@ -528,10 +509,7 @@ fn deserialize_excerpt_range(
|
||||
Some(start..end)
|
||||
})
|
||||
.unwrap_or_else(|| context.clone());
|
||||
Some((
|
||||
ExcerptId::from_proto(excerpt.id),
|
||||
ExcerptRange { context, primary },
|
||||
))
|
||||
Some(ExcerptRange { context, primary })
|
||||
}
|
||||
|
||||
fn deserialize_selection(
|
||||
|
||||
@@ -199,9 +199,6 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
|
||||
lines[row.0 as usize].push_str("§ ");
|
||||
lines[row.0 as usize].push_str(block_lines[0].trim_end());
|
||||
for i in 1..height as usize {
|
||||
if row.0 as usize + i >= lines.len() {
|
||||
lines.push("".to_string());
|
||||
};
|
||||
lines[row.0 as usize + i].push_str("§ ");
|
||||
lines[row.0 as usize + i].push_str(block_lines[i].trim_end());
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@ agent.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
assistant_tools.workspace = true
|
||||
async-trait.workspace = true
|
||||
async-watch.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
client.workspace = true
|
||||
@@ -34,11 +32,9 @@ node_runtime.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
regex.workspace = true
|
||||
release_channel.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
shellexpand.workspace = true
|
||||
smol.workspace = true
|
||||
@@ -48,6 +44,7 @@ unindent.workspace = true
|
||||
util.workspace = true
|
||||
uuid = { version = "1.6", features = ["v4"] }
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "eval"
|
||||
path = "src/eval.rs"
|
||||
|
||||
3
crates/eval/examples/add_arp_protocol_support/base.toml
Normal file
3
crates/eval/examples/add_arp_protocol_support/base.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/GyulyVGC/sniffnet.git"
|
||||
revision = "cfb5b6519bd7838f279e5be9d360445aaffaa647"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,16 @@
|
||||
1. **Protocol Enumeration:** Ensure the `Protocol` enum includes the `ARP` variant and is integrated in `Protocol::ALL`.
|
||||
2. **Packet Analysis Logic:**
|
||||
- Properly detect ARP packets within `analyze_headers` and `analyze_network_header`.
|
||||
- Appropriately extract ARP sender/target IPs based on the protocol (IPv4 or IPv6).
|
||||
- Track and store ARP operations (Request, Reply) using the `ArpType` enum.
|
||||
3. **Display & User Interface:**
|
||||
- Accurately represent ARP packet types in the UI (`connection_details_page.rs`) alongside ICMP types.
|
||||
- Skip displaying service information for ARP packets in line with ICMP behavior.
|
||||
4. **Data Struct Enhancements:**
|
||||
- Update `InfoAddressPortPair` to store and count ARP operation types.
|
||||
- Ensure filtering and presentation logic uses ARP data correctly.
|
||||
5. **Default Behaviors:**
|
||||
- Set default `protocol` in `PacketFiltersFields` to `ARP` for consistency.
|
||||
6. **Testing:**
|
||||
- Update unit tests for `Protocol::ALL` and `get_service` to account for ARP behavior.
|
||||
- Confirm that ARP protocol toggling works properly in the GUI protocol filter handling.
|
||||
1
crates/eval/examples/add_arp_protocol_support/prompt.md
Normal file
1
crates/eval/examples/add_arp_protocol_support/prompt.md
Normal file
@@ -0,0 +1 @@
|
||||
Add full support for the Address Resolution Protocol (ARP) in the packet sniffer. This includes recognizing ARP packets during packet analysis, displaying ARP operation types in the UI, and updating data structures to track ARP-specific metadata. Integrate ARP into the protocol filtering system, update all relevant UI logic to ensure it handles ARP packets similarly to ICMP, and ensure proper test coverage for all new functionality. Update `Protocol::ALL` to include ARP and skip service detection for ARP packets, as they don’t use ports. Finally, ensure the `connection_details_page` displays the ARP operation types with counts, using a `pretty_print_types` method similar to ICMP types.
|
||||
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/swc-project/swc.git"
|
||||
revision = "787d5fabf410fafe6595ec00c197181b27578cb1"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,6 @@
|
||||
1. The `parse` and `parse_sync` functions must support both `Buffer` and `String` inputs for the `src` parameter, using the `Either` type from `napi` to avoid breaking existing string-based usage while adding buffer support.
|
||||
2. A helper function `stringify` must handle conversion of `Either<Buffer, String>` to a unified `String` representation internally, ensuring consistent UTF-8 decoding for buffers and direct string passthrough.
|
||||
3. The TypeScript binding declarations (`binding.d.ts`) must reflect the updated parameter types for `parse` and `parse_sync` to accept `Buffer | string`, ensuring compatibility with JavaScript/TypeScript callers.
|
||||
4. Unit tests must validate both buffer and string input paths for asynchronous (`parse`) and synchronous (`parse_sync`) APIs, ensuring parity in functionality and output correctness.
|
||||
5. The `filename` parameter must remain optional but use `FileName::Real` when provided and fall back to `FileName::Anon` if omitted, preserving existing file resolution logic.
|
||||
6. No regressions in error handling, abort signal support, or serialization/deserialization of `ParseOptions` during the refactor.
|
||||
@@ -0,0 +1 @@
|
||||
I need to extend the SWC parsing APIs to support both `Buffer` and `string` inputs for the source code. Please update the `parse` and `parse_sync` functions to accept `Either<Buffer, String>` instead of just `String`. Add a helper function to convert the `Either` type into a UTF-8 string, using `String::from_utf8_lossy` for buffers to handle invalid characters gracefully. Ensure the TypeScript definitions in `binding.d.ts` reflect the new parameter types. Include unit tests for both buffer and string inputs in `api_test.js`, verifying that asynchronous and synchronous parsing produce identical results regardless of input type. Maintain backward compatibility with existing string-based calls and ensure the `filename` fallback logic remains unchanged. Simplify the `src` handling to avoid code duplication between async/sync paths.
|
||||
@@ -0,0 +1,4 @@
|
||||
url = "https://github.com/dani-garcia/vaultwarden.git"
|
||||
revision = "3a1f1bae002bebf26ce3a38b879c1ba26529af1e"
|
||||
language_extension = "rs"
|
||||
allow_preexisting_diagnostics = true
|
||||
@@ -0,0 +1,6 @@
|
||||
1. Refactors the `register_verification_email` logic to generate the JWT verification token earlier in the control flow, reducing duplication and improving readability.
|
||||
2. Improves conditional logic for sending verification emails by only querying the database when mail should be sent, reducing unnecessary operations.
|
||||
3. Refines the user existence check to specifically filter for users that have a `private_key`, adding stricter criteria before skipping email sending.
|
||||
4. Preserves existing timing attack mitigation by retaining randomized sleep behavior when user exists but an email is not sent.
|
||||
5. Ensures the email is sent only if appropriate, preserving previous behavior while streamlining logic and improving maintainability.
|
||||
6. Removes redundant code paths and unnecessary reassignments, improving clarity without affecting functionality.
|
||||
@@ -0,0 +1 @@
|
||||
I want to refactor the `register_verification_email` function to streamline how verification emails are handled. Currently, the code checks if a user exists and then sends an email or returns early. I’d like to move the JWT token generation to the top of the function to avoid duplication. Then, if mail sending is enabled, the code should check for the user, but only send the verification email if the user exists and has a `private_key` (otherwise it should send the email). Keep the random sleep logic for timing mitigation in the branch where no email is sent. Remove the old duplicated token generation logic and any redundant conditionals, while ensuring the core behavior and response flow stays the same.
|
||||
3
crates/eval/examples/exif_rotation_support/base.toml
Normal file
3
crates/eval/examples/exif_rotation_support/base.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/qarmin/czkawka.git"
|
||||
revision = "db164d3698198dd46653b1c3bb0384f8a9e38fab"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,7 @@
|
||||
1. **EXIF-based Rotation Handling**: Introduces image orientation correction using EXIF metadata by parsing orientation tags and applying corresponding image transformations (e.g., flip, rotate). This improves accuracy for displaying and analyzing images with embedded rotation data.
|
||||
2. **New Dependencies and Parsing Logic**: Adds `nom-exif`, `iso6709parse`, and related dependencies for reading EXIF metadata, and implements robust parsing logic using `MediaParser`, `ExifIter`, and orientation matching for clean integration.
|
||||
3. **Expanded `common_image.rs` Logic**: Refactors image loading in `get_dynamic_image_from_path` to automatically apply EXIF-based orientation corrections, adding new helper methods (`get_rotation_from_exif`) and an `ExifOrientation` enum to encapsulate the rotation logic clearly and maintainably.
|
||||
4. **Versioning and Compatibility Updates**: Updates minimum Rust version to 1.80.0 across all packages and workflows, ensuring compatibility with newly introduced crates and language features.
|
||||
5. **Internal Versioning Sync**: Increments `CACHE_IMAGE_VERSION` to ensure cache invalidation reflects new image processing logic, preventing mismatches due to transformed image data.
|
||||
6. **Dependency Management and Cargo.toml Additions**: Adds new crate dependencies to `Cargo.toml` files where necessary (`czkawka_core`, `Cargo.lock`) and aligns versions to reflect new EXIF parsing functionality.
|
||||
7. **GUI State Initialization Adjustment**: Modifies GUI default tab state from `SimilarImages` to `DuplicateFiles`—likely for improved UX or alignment with application focus.
|
||||
1
crates/eval/examples/exif_rotation_support/prompt.md
Normal file
1
crates/eval/examples/exif_rotation_support/prompt.md
Normal file
@@ -0,0 +1 @@
|
||||
I'd like to implement support for automatic image orientation correction based on EXIF metadata in our Rust project. Specifically, I want to use the `nom-exif` crate to read EXIF orientation tags and adjust the image accordingly (e.g., flip horizontally, rotate 90° CW, etc.) when loading it in `get_dynamic_image_from_path`. Please integrate the EXIF parsing flow using `MediaParser`, `ExifIter`, and match the orientation codes 1–8 to a custom `ExifOrientation` enum. Ensure that these transformations are applied directly to the `DynamicImage` output when applicable. Also, bump the `CACHE_IMAGE_VERSION` to invalidate any outdated cached formats and update the Rust version across the codebase to `1.80.0` to support the latest dependencies. Make any required changes to Cargo.toml and lockfiles, and default the GUI to open the Duplicate Files tab instead of Similar Images for consistency.
|
||||
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/zed-industries/zed.git"
|
||||
revision = "38fcadf9481d018543c65f36ac3bafeba190179b"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,2 @@
|
||||
1. The changes must replace the previous output returned by `FindReplaceFileTool` with the new `ToolResult` struct. The struct should contain an `output` field that is the same as the string we were returning before, and a new `card` field that contains a view for the card
|
||||
2. The card should be a view that displays a diff. Each line in the diff should be colored according to whether it was added, removed or unchanged.
|
||||
@@ -0,0 +1,3 @@
|
||||
Look at the `find_replace_file_tool.rs`. I want to implement a card for it. The card should implement the `Render` trait.
|
||||
|
||||
The card should show a diff. It should be a beautifully presented diff. The card "box" should look like what we show for markdown codeblocks (look at `MarkdownElement`). I want to see a red background for lines that were deleted and a green background for lines that were added. We should have a div per diff line.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user