Compare commits
1 Commits
rework-eva
...
thomas/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c080d3d60d |
77
.github/workflows/eval.yml
vendored
77
.github/workflows/eval.yml
vendored
@@ -1,77 +0,0 @@
|
||||
name: Run Agent Eval
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
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
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4912,7 +4912,6 @@ dependencies = [
|
||||
"language_models",
|
||||
"languages",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"project",
|
||||
"prompt_store",
|
||||
@@ -14228,7 +14227,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"hex",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"proto",
|
||||
"schemars",
|
||||
"serde",
|
||||
|
||||
@@ -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 |
@@ -670,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| {
|
||||
@@ -1030,7 +1010,6 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
ThreadEvent::CheckpointChanged => cx.notify(),
|
||||
ThreadEvent::ReceivedTextChunk => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1093,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()
|
||||
{
|
||||
@@ -1569,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)
|
||||
@@ -1734,6 +1703,7 @@ impl ActiveThread {
|
||||
} else {
|
||||
div()
|
||||
.min_h_6()
|
||||
.text_ui(cx)
|
||||
.child(self.render_message_content(
|
||||
message_id,
|
||||
rendered_message,
|
||||
@@ -1792,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().py_2().px_2p5().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()
|
||||
@@ -1856,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();
|
||||
@@ -1878,7 +1861,8 @@ impl ActiveThread {
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(div().p_2().children(message_content)),
|
||||
),
|
||||
Role::Assistant => v_flex()
|
||||
.id(("message-container", ix))
|
||||
@@ -2117,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 {
|
||||
@@ -2144,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({
|
||||
@@ -3344,15 +3308,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();
|
||||
@@ -3379,7 +3343,6 @@ pub(crate) fn open_context(
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
AssistantContext::Image(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1951,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,13 +1,8 @@
|
||||
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 gpui::{App, Entity, SharedString};
|
||||
use language::{Buffer, File};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::{ProjectEntryId, ProjectPath, Worktree};
|
||||
use prompt_store::UserPromptId;
|
||||
use rope::Point;
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,51 +136,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
|
||||
// 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 +177,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 +200,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 +210,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 +259,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,9 +6,8 @@ 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 gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
|
||||
use language::{Buffer, File};
|
||||
use project::{Project, ProjectEntryId, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::UserPromptId;
|
||||
use rope::{Point, Rope};
|
||||
@@ -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(())
|
||||
})
|
||||
@@ -209,15 +211,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))?;
|
||||
@@ -289,23 +303,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 +468,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 +502,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 +560,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 +570,6 @@ impl ContextStore {
|
||||
AssistantContext::Rules(RulesContext { prompt_id, .. }) => {
|
||||
self.user_rules.remove(&prompt_id);
|
||||
}
|
||||
AssistantContext::Image(_) => {}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@@ -700,11 +695,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 +713,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(
|
||||
@@ -915,13 +923,13 @@ pub fn refresh_context_store_text(
|
||||
return refresh_symbol_text(context_store, symbol_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Selection(selection_context) => {
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
// TODO: Should refresh if the path has changed, as it's in the text.
|
||||
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 +946,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
|
||||
@@ -1043,27 +1050,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 +1138,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 +1159,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 +1175,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 +1184,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
|
||||
|
||||
@@ -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};
|
||||
@@ -271,7 +271,6 @@ 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();
|
||||
@@ -281,7 +280,6 @@ impl MessageEditor {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let checkpoint = checkpoint.await.ok();
|
||||
refresh_task.await;
|
||||
wait_for_images.await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
@@ -295,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<_>>();
|
||||
|
||||
@@ -377,34 +370,6 @@ 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(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||
}
|
||||
@@ -480,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)
|
||||
|
||||
@@ -16,7 +16,7 @@ use git::repository::DiffType;
|
||||
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,
|
||||
@@ -38,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,
|
||||
@@ -97,7 +97,6 @@ pub struct Message {
|
||||
pub role: Role,
|
||||
pub segments: Vec<MessageSegment>,
|
||||
pub context: String,
|
||||
pub images: Vec<LanguageModelImage>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
@@ -168,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,
|
||||
}
|
||||
}
|
||||
@@ -416,7 +418,6 @@ impl Thread {
|
||||
})
|
||||
.collect(),
|
||||
context: message.context,
|
||||
images: Vec::new(),
|
||||
})
|
||||
.collect(),
|
||||
next_message_id,
|
||||
@@ -749,19 +750,6 @@ 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 {
|
||||
@@ -780,16 +768,15 @@ impl Thread {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AssistantContext::Selection(selection_context) => {
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
log.buffer_added_as_context(
|
||||
selection_context.context_buffer.buffer.clone(),
|
||||
excerpt_context.context_buffer.buffer.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AssistantContext::FetchedUrl(_)
|
||||
| AssistantContext::Thread(_)
|
||||
| AssistantContext::Rules(_)
|
||||
| AssistantContext::Image(_) => {}
|
||||
| AssistantContext::Rules(_) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -830,7 +817,6 @@ impl Thread {
|
||||
role,
|
||||
segments,
|
||||
context: String::new(),
|
||||
images: Vec::new(),
|
||||
});
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageAdded(id));
|
||||
@@ -1054,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) => {
|
||||
@@ -1263,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);
|
||||
@@ -1813,7 +1783,7 @@ impl Thread {
|
||||
thread_data,
|
||||
final_project_snapshot
|
||||
);
|
||||
client.telemetry().flush_events().await;
|
||||
client.telemetry().flush_events();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -1858,7 +1828,7 @@ impl Thread {
|
||||
thread_data,
|
||||
final_project_snapshot
|
||||
);
|
||||
client.telemetry().flush_events().await;
|
||||
client.telemetry().flush_events();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -2114,7 +2084,7 @@ impl Thread {
|
||||
github_login = github_login
|
||||
);
|
||||
|
||||
client.telemetry().flush_events().await;
|
||||
client.telemetry().flush_events();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2232,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
|
||||
|
||||
@@ -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,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,8 +259,7 @@ impl AddedContext {
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
summarizing: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,8 +289,7 @@ impl AddedContext {
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
summarizing: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,12 +300,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 +313,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 +326,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 +343,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 +353,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 +366,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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -22,7 +22,6 @@ mod schema;
|
||||
mod symbol_info_tool;
|
||||
mod terminal_tool;
|
||||
mod thinking_tool;
|
||||
mod ui;
|
||||
mod web_search_tool;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -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,11 +1,13 @@
|
||||
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 gpui::{App, AppContext, Context, Entity, IntoElement, Task, Window};
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, AppContext, Context, Entity, IntoElement, Task, Window,
|
||||
pulsating_between,
|
||||
};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -45,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(
|
||||
@@ -113,30 +115,61 @@ impl ToolCard for WebSearchToolCard {
|
||||
_window: &mut Window,
|
||||
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()
|
||||
@@ -176,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3108,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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ language_model.workspace = true
|
||||
language_models.workspace = true
|
||||
languages = { workspace = true, features = ["load-grammars"] }
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
|
||||
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.
|
||||
@@ -1,2 +1,2 @@
|
||||
- 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 task we were returning before, and a new `card` field that contains a view for the card.
|
||||
- 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.
|
||||
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.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
- The first tool call should be to path search including "find_replace_file_tool.rs" in the string. (*Not* grep, for example, or reading the file based on a guess at the path.) This is because we gave the model a filename and it needs to turn that into a real path.
|
||||
- After obtaining the correct path of "zed/crates/assistant_tools/src/find_replace_file_tool.rs", it should read the contents of that path.
|
||||
- When trying to find information about the Render trait, it should *not* begin with a path search, because it doesn't yet have any information on what path the Render trait might be in.
|
||||
1. The first tool call should be to path search including "find_replace_file_tool.rs" in the string. (*Not* grep, for example, or reading the file based on a guess at the path.) This is because we gave the model a filename and it needs to turn that into a real path.
|
||||
2. After obtaining the correct path of "zed/crates/assistant_tools/src/find_replace_file_tool.rs", it should read the contents of that path.
|
||||
3. When trying to find information about the Render trait, it should *not* begin with a path search, because it doesn't yet have any information on what path the Render trait might be in.
|
||||
|
||||
3
crates/eval/examples/lhs_join_update_callbacks/base.toml
Normal file
3
crates/eval/examples/lhs_join_update_callbacks/base.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/clockworklabs/SpacetimeDB.git"
|
||||
revision = "68d23d4c25548fd74f1bde28a57d8858022b9671"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,6 @@
|
||||
1. A `JOIN` query with conditions on both sides (LHS and RHS) correctly triggers subscription updates when only the LHS table is updated.
|
||||
2. Callback functions (`on_insert`, `on_update`) are invoked exactly once and in the expected order.
|
||||
3. Queries with logically equivalent WHERE conditions (e.g., `x > 0 and x < 5` vs. `0 < x and x < 5`) yield consistent subscription behavior.
|
||||
4. Complex disjoint queries that restrict the RHS via additional constraints (e.g., `u.n != 1`) still properly identify matching LHS updates.
|
||||
5. Type inference and expression normalization correctly handle literals on the left-hand side of binary operations in WHERE clauses.
|
||||
6. Physical execution plans normalize expressions like `3 < l.x` into `l.x > 3` with appropriate operator inversion (`Lt ↔ Gt`, `Lte ↔ Gte`), maintaining logical correctness.
|
||||
12
crates/eval/examples/lhs_join_update_callbacks/prompt.md
Normal file
12
crates/eval/examples/lhs_join_update_callbacks/prompt.md
Normal file
@@ -0,0 +1,12 @@
|
||||
Add a new test case to validate join subscription updates when the **LHS table is updated**, and ensure correct invocation of reducer callbacks. The test should:
|
||||
|
||||
- Subscribe to a join query with a filter involving fields from both tables (e.g., `SELECT p.* FROM pk_u32 p JOIN unique_u32 u ON p.n = u.n WHERE u.data > 0 AND u.data < 5`).
|
||||
- Insert rows into both LHS (`pk_u32`) and RHS (`unique_u32`) that satisfy the join condition.
|
||||
- Verify the initial subscription callback is triggered via `on_insert`.
|
||||
- Update the LHS (`pk_u32`) such that the row remains part of the join result.
|
||||
- Validate that:
|
||||
- `on_update` is invoked correctly.
|
||||
- An immediate follow-up update back to the original value also triggers `on_update`.
|
||||
- Repeat the above with disjoint filters (e.g., `u.n != 1`) and confirm behavior remains correct.
|
||||
|
||||
Also, ensure that literal-first SQL expressions like `3 < x` are correctly interpreted and inverted in the physical execution plan (converted to `x > 3`) and behave identically during query evaluation and execution.
|
||||
3
crates/eval/examples/libdevice_symbol_reexport/base.toml
Normal file
3
crates/eval/examples/libdevice_symbol_reexport/base.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/Rust-GPU/Rust-CUDA.git"
|
||||
revision = "728013419b6c4c80e099a42413574c36a9aff9c7"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,4 @@
|
||||
1. **Reexports `LIBDEVICE_BITCODE` for cleaner dependency usage:** The `LIBDEVICE_BITCODE` symbol from the `cust_raw::nvvm_sys` crate is now reexported via the `nvvm` crate. This allows consuming crates to access the symbol directly from `nvvm`, abstracting away the internal structure and reducing tight coupling to `cust_raw`.
|
||||
2. **Simplifies dependency graph and usage of NVVM internals:** By removing the direct dependency on `cust_raw` from `rustc_codegen_nvvm`, the changes streamline the crate's external interface, reducing maintenance overhead and improving modularity. Consumers now only need to rely on the higher-level `nvvm` crate.
|
||||
3. **Improves code readability and encapsulation:** The change makes the source cleaner by reducing low-level, verbose paths like `nvvm_sys::LIBDEVICE_BITCODE` to a concise `LIBDEVICE_BITCODE`, enhancing readability and reinforcing a layered architecture.
|
||||
4. **Maintains existing functionality:** The code logic remains unchanged in behavior—`LIBDEVICE_BITCODE` is still used in the same way, ensuring that the refactor is safe, non-breaking, and purely organizational.
|
||||
1
crates/eval/examples/libdevice_symbol_reexport/prompt.md
Normal file
1
crates/eval/examples/libdevice_symbol_reexport/prompt.md
Normal file
@@ -0,0 +1 @@
|
||||
I'd like to improve the modularity and encapsulation of the NVVM codegen setup. Please refactor the code to reexport `LIBDEVICE_BITCODE` from the `nvvm` crate instead of accessing it directly from `cust_raw::nvvm_sys`. This involves updating the `nvvm` crate to publicly reexport the symbol, and then modifying `rustc_codegen_nvvm` to use the reexported path. Additionally, remove the direct dependency on `cust_raw` from `rustc_codegen_nvvm/Cargo.toml` and clean up any redundant `use` statements that reference `cust_raw` directly. The goal is to simplify usage of `nvvm_sys` internals by encapsulating them within `nvvm`, making the codebase more maintainable without changing its behavior.
|
||||
3
crates/eval/examples/license_management/base.toml
Normal file
3
crates/eval/examples/license_management/base.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/SAP-samples/abap-cheat-sheets.git"
|
||||
revision = "262c0472eeb03e05ff8235767356a328d97850e6"
|
||||
language_extension = "rs"
|
||||
3
crates/eval/examples/license_management/diff_criteria.md
Normal file
3
crates/eval/examples/license_management/diff_criteria.md
Normal file
@@ -0,0 +1,3 @@
|
||||
1. The file `.reuse/dep5` has been deleted. This file previously contained copyright and licensing information in Debian's copyright format, including details about API usage with SAP products, copyright notice (2022 SAP SE or affiliates), and Apache-2.0 license information.
|
||||
2. A new file `REUSE.toml` has been created with similar copyright and licensing information but in a different format. It includes the package name, supplier information, download location, and the same detailed disclaimer about API usage with SAP products that was in the deleted file.
|
||||
3. The new `REUSE.toml` file also contains annotations specifying that the copyright text and Apache-2.0 license apply to all files (`path = "**"`) with aggregate precedence, effectively maintaining the same licensing terms but in a different configuration format.
|
||||
17
crates/eval/examples/license_management/prompt.md
Normal file
17
crates/eval/examples/license_management/prompt.md
Normal file
@@ -0,0 +1,17 @@
|
||||
I need to switch our license stuff from the old .reuse/dep5 file to the new REUSE.toml format. basically same info, just different format. here's what's in the old file:
|
||||
|
||||
project name: abap-cheat-sheets
|
||||
contact: daniel reger's email
|
||||
repo link
|
||||
that long SAP API disclaimer
|
||||
copyright: SAP + contributors, 2022
|
||||
license: Apache-2.0
|
||||
need to:
|
||||
|
||||
delete the old .reuse/dep5 file
|
||||
make a new REUSE.toml with:
|
||||
same project info (name, contact, repo)
|
||||
same exact API disclaimer text
|
||||
SPDX-style copyright & license fields
|
||||
apply to all files (** glob) with aggregate precedence
|
||||
not changing any actual license terms, just updating the format. can you give me the exact REUSE.toml file we need?
|
||||
4
crates/eval/examples/metal_i64_support/base.toml
Normal file
4
crates/eval/examples/metal_i64_support/base.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
url = "https://github.com/huggingface/candle.git"
|
||||
revision = "3164a19a5dc18f5e0f7a063ae85a0cfd289e98f1"
|
||||
language_extension = "rs"
|
||||
allow_preexisting_diagnostics = true
|
||||
4
crates/eval/examples/metal_i64_support/diff_criteria.md
Normal file
4
crates/eval/examples/metal_i64_support/diff_criteria.md
Normal file
@@ -0,0 +1,4 @@
|
||||
1. The changes improve the configurability of the `TextGeneration` struct and its initialization by refactoring generation parameters (`temperature`, `top_p`) to use non-optional types with default values, simplifying their use throughout the codebase.
|
||||
2. The argument parser is updated to enhance usability: `verbose_prompt` is renamed to a more general `verbose` flag, several arguments are given default values (e.g., `temperature`, `top_p`, `sample_len`), and optional arguments like `cache_path` and `weight_path` are now properly handled with conditional logic and fallbacks.
|
||||
3. The code loading the model configuration is updated to support deserializing from a JSON config file using Serde, and the `Config` struct is extended with a new `rope_ratio` field with a default value via a helper function, improving flexibility for different model setups.
|
||||
4. Import statements and general code layout are cleaned up for clarity and consistency, including reorganizing imports and removing unnecessary unwraps or panics, while maintaining the same core functionality of the text generation pipeline.
|
||||
1
crates/eval/examples/metal_i64_support/prompt.md
Normal file
1
crates/eval/examples/metal_i64_support/prompt.md
Normal file
@@ -0,0 +1 @@
|
||||
I'd like to improve the configurability and usability of the text generation script for the CodeGeeX4-9B model. Please refactor the argument parsing to set more user-friendly defaults where possible, especially for generation parameters like temperature and top-p, and change fields like verbose_prompt to a more general verbose flag. Simplify the handling of optional paths like cache or weight paths, making them truly optional with fallbacks. I also want the model config to support deserialization from a JSON file instead of relying on hardcoded defaults, including support for a rope_ratio parameter with a sensible default. Lastly, please clean up the code for consistency—such as import ordering—and ensure everything aligns with these improvements without changing the overall functionality.
|
||||
3
crates/eval/examples/metrics_data_size_updates/base.toml
Normal file
3
crates/eval/examples/metrics_data_size_updates/base.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/clockworklabs/SpacetimeDB.git"
|
||||
revision = "13dfb031351c3adf308c74b2a085ca15aa797db1"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,6 @@
|
||||
1. The function `report_data_size` has been refactored to have a more accessible visibility by changing from `pub(super)` to `pub` in the `CommittedState` struct, making it usable outside of its previous scope.
|
||||
2. The `record_tx_metrics` function has been modified to remove the previously commented-out code that invoked `report_data_size` from `committed_state`. The intention to possibly inline this functionality or refactor the metrics updates is noted.
|
||||
3. A new function `update_data_size_metrics` has been introduced in the `RelationalDB` struct to simplify calling the `report_data_size` method, enhancing clarity and direct usage within the database context.
|
||||
4. The `storage_monitor` function has been renamed and refactored to `metric_reporter`, which is tasked with collecting disk usage statistics and invoking `update_data_size_metrics` for database state updates.
|
||||
5. Various asynchronous operations involving time intervals for disk usage measurement and reporting have been restructured for improved metric collection, reducing unnecessary operations and improving clarity.
|
||||
6. Comments and TODOs are placed where further improvements, such as adding heap usage metrics, are possible, guiding future enhancements.
|
||||
1
crates/eval/examples/metrics_data_size_updates/prompt.md
Normal file
1
crates/eval/examples/metrics_data_size_updates/prompt.md
Normal file
@@ -0,0 +1 @@
|
||||
I'd like to refactor and improve the metric collection system in the database layer. Specifically, please refactor the `report_data_size` function to make it publicly accessible by changing its visibility from `pub(super)` to `pub`. Then, remove the commented-out `report_data_size` invocation from `record_tx_metrics` in the `datastore.rs` file and ensure that metric collection is more streamlined. Add a new function in the `RelationalDB` struct named `update_data_size_metrics` to simplify invoking `report_data_size` and enhance its usage across the code. Finally, refactor the `storage_monitor` function by renaming it to `metric_reporter`, and ensure that it periodically collects disk usage statistics and updates data size metrics. Additionally, leave a TODO in the code for adding heap usage metrics in the future. Please ensure that these changes maintain the core functionality while improving the overall organization and clarity of the code.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Pull Request: https://github.com/zed-industries/zed/pull/27934
|
||||
url = "https://github.com/zed-industries/zed.git"
|
||||
revision = "889bc13b7dd61c67d894cee8a6cdd87f56c6c45b"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,3 @@
|
||||
1. The changes must add internal state to track whether the user is providing feedback comments and to store the comment editor instance. This includes adding new fields to the ActiveThread struct and initializing them appropriately when the thread is created.
|
||||
2. When a user selects negative feedback, the application should show a UI for submitting additional comments. This includes rendering a short prompt, a multi-line text editor, and submit/cancel buttons. The editor should only be created when first needed, and the UI should be dismissed and cleaned up after submission or cancellation.
|
||||
3. On submit, the system must report the negative feedback as before, and if the comment field is not empty, it must also log the comment as a separate telemetry event. Positive feedback handling should remain unchanged and bypass the comment UI entirely.
|
||||
@@ -0,0 +1 @@
|
||||
Add support for optional user comments when the thumbs down is given on a thread. Comments should be submitted along with the reaction and logged if provided. Make sure the UI highlights the ui icon when the user clicks it.
|
||||
@@ -0,0 +1,3 @@
|
||||
1. The first tool call should perform a regex search for terms related to "negative feedback." Since no specific file path or code snippet was provided, regex is necessary to locate relevant content. Once the matching files are found, their contents should be read.
|
||||
2. Only the `zed/crates/agent/src/active_thread.rs` file needs to be edited. All logic related to reactions and negative comments should be contained within this file.
|
||||
3. A comment box should appear only when the negative reaction is clicked. The positive reaction behavior should remain unchanged.
|
||||
3
crates/eval/examples/never_type_workaround/base.toml
Normal file
3
crates/eval/examples/never_type_workaround/base.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/bevyengine/bevy.git"
|
||||
revision = "ac52cca033b351cc966cd3d40eb99ffbefbdb104"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,5 @@
|
||||
1. Introduces a stable-Rust-compatible workaround for the unstable `!` (never) type by implementing a custom `Never` alias based on a trait (`FnRet`) and function signature (`fn() -> !`), mimicking the behavior of the `never_say_never` crate without an external dependency.
|
||||
2. Adds trait impls that enable Bevy systems and commands to accept `Never` as an output type, ensuring compatibility with panicking closures or intentionally non-returning functions like `todo!()` or `panic!()`.
|
||||
3. Updates internal wrappers (`InfallibleSystemWrapper`, `InfallibleObserverWrapper`) and trait bounds across observer and schedule systems to support this workaround by allowing `Never` as a valid output type while maintaining existing fallible/infallible behavior.
|
||||
4. Adds robust regression test coverage to ensure these `Never`-based trait implementations compile and function as expected, specifically targeting closures and functions that use `todo!()` or diverge without returning.
|
||||
5. Ensures this workaround does not compromise stability guarantees by isolating `Never` usage to internal APIs and clearly documenting the risks and rationale in the new `never.rs` module.
|
||||
1
crates/eval/examples/never_type_workaround/prompt.md
Normal file
1
crates/eval/examples/never_type_workaround/prompt.md
Normal file
@@ -0,0 +1 @@
|
||||
I'd like to add stable Rust support for handling the `!` (never) type in Bevy's ECS systems, in light of changes introduced in the Rust 2024 edition around never type fallback inference. Please create a new internal module (e.g., `never.rs`) that provides a type alias `Never` using a workaround based on a trait and `fn() -> !` to simulate the behavior of the unstable `!` type. Update the necessary traits and system wrappers (such as `HandleError`, `IntoScheduleConfigs`, and `IntoObserverSystem`) to accept `Never` as a valid output type, ensuring that closures or systems using `todo!()` or panics can still compile and behave correctly. Add a set of regression tests that exercise this compatibility by queuing and scheduling systems and commands with `todo!()` as their body, ensuring trait impls are resolved properly. Make sure to document this hack in the new module with a clear explanation of why it's being used and the risks involved.
|
||||
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/alacritty/alacritty.git"
|
||||
revision = "c9c41e637ac49f3cd67cf0362c596ae9d947f896"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,6 @@
|
||||
1. **Field Renaming and Semantic Clarification**: The `hold` field in `Options`, `TerminalOptions`, and `PtyOptions` has been renamed to `drain_on_exit` across the codebase. This improves semantic clarity by distinguishing between two distinct behaviors: draining output before exit versus holding the terminal open after exit.
|
||||
2. **Behavioral Shift in Exit Logic**: The logic previously controlled by `hold` now uses `drain_on_exit`, ensuring the child process’s output is drained upon termination, but the window may still close unless explicitly held open via external means. Exit handling in `event_loop.rs` has been updated to reflect this behavioral distinction.
|
||||
3. **Config and Struct Updates**: All related structs (`UiConfig`, `Window`, `WindowContext`, `EventLoop`) have been updated to reflect the new `drain_on_exit` naming. This ensures consistent naming and avoids legacy confusion.
|
||||
4. **UI Window Behavior**: A new `hold` field has been added to the `Window` struct to manage whether the terminal window should remain open on exit, separating UI behavior from terminal process behavior.
|
||||
5. **Exit Control Improvements**: When a user closes a window manually (`CloseRequested` event), `hold` is explicitly set to `false` to allow proper shutdown, ensuring manual control supersedes configuration-based persistence.
|
||||
6. **Documentation and Changelog Updates**: The CHANGELOG entry for version `0.25.0-dev` documents the replacement of `hold` with `drain_on_exit`, providing visibility into this breaking change and its rationale (terminal holding should now be handled externally).
|
||||
@@ -0,0 +1 @@
|
||||
I'd like to rename and refactor the `hold` behavior in the Alacritty terminal codebase to better reflect its actual use and separate terminal process handling from window behavior. Please rename the `hold` field in all relevant config structs (`Options`, `TerminalOptions`, `PtyOptions`) to `drain_on_exit` to make it clear that the terminal should drain its output before exiting, not necessarily hold the window open. Update all associated logic and struct initializations accordingly. Additionally, add a new `hold` field specifically to the `Window` struct to control whether the terminal window should remain open after the terminal process exits. Ensure that when a user explicitly closes the window (e.g., via `WindowEvent::CloseRequested`), this `hold` flag is set to false to allow normal shutdown. Update any logic that previously depended on `hold` to use the appropriate new field, and include a changelog entry explaining this semantic split and why the change was made.
|
||||
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/lancedb/lancedb.git"
|
||||
revision = "698f329598bcfa8a5bf0feedfdd4344a4cdc7e4d"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,6 @@
|
||||
1. The `restore` method is updated across Python and Rust components of LanceDB to accept an optional `version` argument, enabling more flexible restoration of historical table versions.
|
||||
2. Python async bindings in `_lancedb.pyi` and `table.py` are updated to reflect the new method signature `restore(version: Optional[int] = None)`, aligning type hints and implementations.
|
||||
3. The remote table interface in `remote/table.py` includes a corresponding `restore` method, bridging the sync API to the async backend with version support.
|
||||
4. The Rust FFI layer (`table.rs`) is modified to accept the optional `version` argument, with logic that performs a `checkout(version)` if specified, before proceeding to `restore()`, improving control over the restore flow.
|
||||
5. The `RemoteTable` implementation in `remote/table.rs` now constructs and sends a versioned restore request via HTTP, enabling client-side version-specific restoration even in cloud deployments.
|
||||
6. Docstrings and comments are added or expanded to explain the behavior of the `restore` function, particularly the no-op case when restoring the latest version, enhancing code maintainability and developer understanding.
|
||||
@@ -0,0 +1 @@
|
||||
I'd like to update the `restore` method in LanceDB to support restoring to a specific historical version of a table. Please modify all relevant files to add an optional `version` parameter to `restore`, defaulting to `None`. When `version` is provided, the implementation should perform a checkout to that version before executing the restore. If `version` is not specified, it should restore the currently checked-out version. Update the Python async bindings (`_lancedb.pyi`, `table.py`, and `remote/table.py`) to reflect the new method signature and behavior. In the Rust FFI layer (`python/src/table.rs`), modify the `restore` function to accept and correctly handle the optional version argument. For the cloud-backed `RemoteTable` in Rust (`rust/lancedb/src/remote/table.rs`), ensure that the version is included in the HTTP request body during a restore operation. Add or update docstrings and comments as needed to clarify how restore behaves with and without the `version` argument.
|
||||
3
crates/eval/examples/time_detail_merge_update/base.toml
Normal file
3
crates/eval/examples/time_detail_merge_update/base.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/tikv/tikv.git"
|
||||
revision = "be74cadcdd6608e5788d0c2a6784c456b4ce84e6"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,5 @@
|
||||
1. **Function Modification**: The `write_time_detail` function has been refactored into `merge_time_detail` to modify the behavior of merging time details instead of overwriting them. The `merge_time_detail` function now adds new values to the existing ones, preserving the data and allowing for cumulative updates, which ensures more accurate tracking of time metrics.
|
||||
2. **Usage of New Function**: All instances where `write_time_detail` was called have been updated to use `merge_time_detail`, including in the `src/coprocessor/endpoint.rs`, `src/server/service/kv.rs`, `src/storage/txn/tracker.rs`, and test files. The modification ensures consistency across the codebase by merging time details rather than replacing them.
|
||||
3. **Test Coverage**: A new test, `test_select_time_details`, has been added in `tests/integrations/coprocessor/test_select.rs` to validate the proper functioning of time detail merging. The test checks that the `process_wall_time_ns` field is not zero, ensuring the correct time metrics are being tracked and merged.
|
||||
4. **Backward Compatibility**: The changes do not affect any external functionality or break compatibility. The merging of time details is backward-compatible, as it preserves existing values and adds new ones, which makes the system more flexible for future extensions.
|
||||
5. **Code Consistency**: The naming convention and function signature have been aligned with existing code practices, making the codebase more consistent and easier to maintain.
|
||||
1
crates/eval/examples/time_detail_merge_update/prompt.md
Normal file
1
crates/eval/examples/time_detail_merge_update/prompt.md
Normal file
@@ -0,0 +1 @@
|
||||
I want to refactor the existing time detail handling in the codebase. Specifically, I'd like to replace the `write_time_detail` function with a new `merge_time_detail` function, which will add new time details to the existing ones rather than overwriting them. This change should be applied across the codebase wherever `write_time_detail` is used, including in `src/coprocessor/endpoint.rs`, `src/server/service/kv.rs`, `src/storage/txn/tracker.rs`, and any related test cases. Please ensure that all occurrences of the old function are updated to use the new one. Additionally, add a test to validate that the `process_wall_time_ns` is correctly merged and is not zero, which will ensure the merging is functioning as intended. Make sure these changes preserve backward compatibility and do not introduce any regressions in functionality.
|
||||
3
crates/eval/examples/tool_response_handling/base.toml
Normal file
3
crates/eval/examples/tool_response_handling/base.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
url = "https://github.com/block/goose.git"
|
||||
revision = "d7308457fe3f1b9c7253de45b2f81ddc4f005fe5"
|
||||
language_extension = "rs"
|
||||
@@ -0,0 +1,3 @@
|
||||
1. All Goose packages (`goose`, `goose-bench`, `goose-cli`, `goose-mcp`, `goose-server`) were updated from version `1.0.17` to `1.0.18` in `Cargo.lock`. These updates ensure compatibility and consistency across related packages.
|
||||
2. The `goose-app` version in `ui/desktop/package-lock.json` was also updated to `1.0.18`, maintaining alignment with the backend and shared libraries.
|
||||
3. In `App.tsx`, the `useConfig` hook was destructured to directly use `addExtension` instead of the older `addExtensionToConfig` function. All occurrences of the old function name were updated, including inside effects and async calls, to use the new unified method. This change simplifies extension handling logic while preserving current behavior.
|
||||
1
crates/eval/examples/tool_response_handling/prompt.md
Normal file
1
crates/eval/examples/tool_response_handling/prompt.md
Normal file
@@ -0,0 +1 @@
|
||||
Upgrade all Goose-related packages and apps from version 1.0.17 to 1.0.18 throughout the codebase. This includes updating version references in Cargo.lock, package-lock.json, and source files where applicable. In addition, streamline the addExtension logic in App.tsx by removing the outdated addExtensionToConfig references and replacing them with the new unified addExtension function. Ensure that all function dependencies and hooks reflect this updated usage. The goal is to improve maintainability and consistency across the codebase without introducing any functional changes.
|
||||
@@ -0,0 +1,4 @@
|
||||
url = "https://github.com/firecracker-microvm/firecracker.git"
|
||||
revision = "5eaa6e08e350cd38c8102848913a096312e59097"
|
||||
language_extension = "rs"
|
||||
allow_preexisting_diagnostics = true
|
||||
@@ -0,0 +1,5 @@
|
||||
1. The changes remove unnecessary generic type parameters from the `FileEngine`, `AsyncFileEngine`, and related structures by directly using the `PendingRequest` type, simplifying type signatures and improving code clarity.
|
||||
2. Error handling is unified through the replacement of `UserDataError` with `RequestError` that specifically carries `PendingRequest` information, ensuring consistent error propagation with request context.
|
||||
3. The `WrappedUserData` struct is renamed to `WrappedRequest` and directly embeds `PendingRequest`, aligning terminology with the virtio block device’s request lifecycle and improving traceability.
|
||||
4. Test code is updated to use `PendingRequest::default()` instead of placeholder `()` types, ensuring type consistency and proper request initialization in all scenarios.
|
||||
5. Code organization is improved by consolidating imports (e.g., merging `IO_URING_NUM_ENTRIES` and `PendingRequest` imports) and removing redundant type parameters across async/sync I/O implementations.
|
||||
@@ -0,0 +1 @@
|
||||
Refactor the virtio block device’s I/O handling to eliminate generic type parameters from file engine structures, replacing them with the concrete `PendingRequest` type. Update the `AsyncFileEngine` and `FileEngine` implementations to directly handle `PendingRequest` in all operations, ensuring error types like `RequestError` propagate this request context. Rename `UserDataError`/`UserDataOk` to `RequestError`/`RequestOk` and adjust their internals to store `PendingRequest` instead of a generic `user_data`. Simplify imports (e.g., consolidate `io_uring` imports) and modify test code to initialize `PendingRequest` properly with default values where needed. Maintain all existing async/sync I/O functionality, including dirty memory tracking and request completion logic.
|
||||
@@ -3,37 +3,34 @@ mod ids;
|
||||
mod tool_metrics;
|
||||
|
||||
pub(crate) use example::*;
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub(crate) use tool_metrics::*;
|
||||
|
||||
use ::fs::RealFs;
|
||||
use anyhow::{Result, anyhow};
|
||||
use clap::Parser;
|
||||
use client::{Client, ProxySettings, UserStore};
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::HashSet;
|
||||
use extension::ExtensionHostProxy;
|
||||
use futures::future;
|
||||
use futures::{StreamExt, future};
|
||||
use gpui::http_client::{Uri, read_proxy_from_env};
|
||||
use gpui::{App, AppContext, Application, AsyncApp, Entity, SemanticVersion, UpdateGlobal};
|
||||
use gpui_tokio::Tokio;
|
||||
use language::{Diagnostic, LanguageRegistry};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelRegistry, LanguageModelRequest, TokenUsage,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{ConfiguredModel, LanguageModel, LanguageModelRegistry};
|
||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||
use project::Project;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use project::{DiagnosticSummary, Project};
|
||||
use prompt_store::PromptBuilder;
|
||||
use release_channel::AppVersion;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::collections::VecDeque;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::usize;
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub const RUNS_DIR: &str = "./crates/eval/runs";
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "eval", disable_version_flag = true)]
|
||||
struct Args {
|
||||
@@ -45,9 +42,13 @@ struct Args {
|
||||
model: String,
|
||||
#[arg(long, value_delimiter = ',', default_value = "rs,ts")]
|
||||
languages: Vec<String>,
|
||||
/// How many times to run each example.
|
||||
/// How many times to run each example. Note that this is currently not very efficient as N
|
||||
/// worktrees will be created for the examples.
|
||||
#[arg(long, default_value = "1")]
|
||||
repetitions: usize,
|
||||
repetitions: u32,
|
||||
/// How many times to run the judge on each example run.
|
||||
#[arg(long, default_value = "3")]
|
||||
judge_repetitions: u32,
|
||||
/// Maximum number of examples to run concurrently.
|
||||
#[arg(long, default_value = "10")]
|
||||
concurrency: usize,
|
||||
@@ -56,36 +57,8 @@ struct Args {
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let system_id = ids::get_or_create_id(&ids::eval_system_id_path()).ok();
|
||||
let installation_id = ids::get_or_create_id(&ids::eval_installation_id_path()).ok();
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
let run_timestamp = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S");
|
||||
let run_id = match env::var("GITHUB_RUN_ID") {
|
||||
Ok(run_id) => format!("github/{}", run_id),
|
||||
Err(_) => format!("local/{}", run_timestamp),
|
||||
};
|
||||
|
||||
let root_dir = Path::new(std::env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap();
|
||||
let eval_crate_dir = root_dir.join("crates/eval");
|
||||
let repos_dir = eval_crate_dir.join("repos");
|
||||
let worktrees_dir = eval_crate_dir.join("worktrees");
|
||||
let examples_dir = eval_crate_dir.join("examples");
|
||||
let runs_dir = eval_crate_dir.join("runs");
|
||||
let run_dir = runs_dir.join(format!("{}", run_timestamp));
|
||||
std::fs::create_dir_all(&run_dir).unwrap();
|
||||
std::fs::create_dir_all(&repos_dir).unwrap();
|
||||
std::fs::create_dir_all(&worktrees_dir).unwrap();
|
||||
std::fs::create_dir_all(&examples_dir).unwrap();
|
||||
std::fs::create_dir_all(&paths::config_dir()).unwrap();
|
||||
|
||||
let zed_commit_sha = commit_sha_for_path(root_dir);
|
||||
let zed_branch_name = git_branch_for_path(root_dir);
|
||||
let args = Args::parse();
|
||||
let all_available_examples = list_all_examples(&examples_dir).unwrap();
|
||||
let all_available_examples = list_all_examples().unwrap();
|
||||
|
||||
let example_paths = all_available_examples
|
||||
.iter()
|
||||
@@ -110,20 +83,14 @@ fn main() {
|
||||
app.run(move |cx| {
|
||||
let app_state = init(cx);
|
||||
|
||||
let telemetry = app_state.client.telemetry();
|
||||
telemetry.start(system_id, installation_id, session_id, cx);
|
||||
let system_id = ids::get_or_create_id(&ids::eval_system_id_path()).ok();
|
||||
let installation_id = ids::get_or_create_id(&ids::eval_installation_id_path()).ok();
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").map_or(false, |value| value == "1")
|
||||
&& telemetry.has_checksum_seed();
|
||||
if enable_telemetry {
|
||||
println!("Telemetry enabled");
|
||||
telemetry::event!(
|
||||
"Agent Eval Started",
|
||||
zed_commit_sha = zed_commit_sha,
|
||||
zed_branch_name = zed_branch_name,
|
||||
run_id = run_id,
|
||||
);
|
||||
}
|
||||
app_state
|
||||
.client
|
||||
.telemetry()
|
||||
.start(system_id, installation_id, session_id, cx);
|
||||
|
||||
let mut cumulative_tool_metrics = ToolMetrics::default();
|
||||
|
||||
@@ -147,6 +114,15 @@ fn main() {
|
||||
cx.spawn(async move |cx| {
|
||||
authenticate_task.await.unwrap();
|
||||
|
||||
std::fs::create_dir_all(REPOS_DIR)?;
|
||||
std::fs::create_dir_all(WORKTREES_DIR)?;
|
||||
|
||||
let run_dir = Path::new(RUNS_DIR).join(format!(
|
||||
"{}",
|
||||
chrono::Local::now().format("%Y-%m-%d_%H-%M-%S")
|
||||
));
|
||||
std::fs::create_dir_all(&run_dir)?;
|
||||
|
||||
let mut examples = Vec::new();
|
||||
|
||||
const COLORS: [&str; 12] = [
|
||||
@@ -164,15 +140,11 @@ fn main() {
|
||||
"\x1b[96m", // Bright Cyan
|
||||
];
|
||||
|
||||
let mut max_name_width = 0;
|
||||
let mut skipped = Vec::new();
|
||||
|
||||
for example_path in &example_paths {
|
||||
let example = Example::load_from_directory(
|
||||
example_path,
|
||||
&run_dir,
|
||||
&worktrees_dir,
|
||||
&repos_dir,
|
||||
)?;
|
||||
let example = Example::load_from_directory(example_path, &run_dir)?;
|
||||
|
||||
if !example
|
||||
.base
|
||||
@@ -184,7 +156,20 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
examples.extend(example.repeat(args.repetitions));
|
||||
// TODO: This creates a worktree per repetition. Ideally these examples should
|
||||
// either be run sequentially on the same worktree, or reuse worktrees when there
|
||||
// are more examples to run than the concurrency limit.
|
||||
for repetition_number in 0..args.repetitions {
|
||||
let mut example = example.clone();
|
||||
example.set_repetition_number(repetition_number);
|
||||
|
||||
let name_len = example.name.len();
|
||||
if name_len > max_name_width {
|
||||
max_name_width = example.name.len();
|
||||
}
|
||||
|
||||
examples.push(example);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Skipped examples: {}\n", skipped.join(", "));
|
||||
@@ -197,11 +182,6 @@ fn main() {
|
||||
let mut repo_urls = HashSet::default();
|
||||
let mut clone_tasks = Vec::new();
|
||||
|
||||
let max_name_width = examples
|
||||
.iter()
|
||||
.map(|e| e.repetition_name().len())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
for (i, example) in examples.iter_mut().enumerate() {
|
||||
let color = COLORS[i % COLORS.len()].to_string();
|
||||
example.set_log_prefix_style(&color, max_name_width);
|
||||
@@ -209,12 +189,12 @@ fn main() {
|
||||
println!(
|
||||
"{}Logging to: {}",
|
||||
example.log_prefix,
|
||||
example.run_directory_path().display()
|
||||
example.example_output_directory().display()
|
||||
);
|
||||
|
||||
let repo_url = example.base.url.clone();
|
||||
if repo_urls.insert(repo_url.clone()) {
|
||||
let repo_path = example.repo_path.clone();
|
||||
let repo_path = repo_path_for_url(&repo_url);
|
||||
|
||||
if !repo_path.join(".git").is_dir() {
|
||||
println!(
|
||||
@@ -255,35 +235,42 @@ fn main() {
|
||||
future::join_all(clone_tasks).await;
|
||||
|
||||
for example in examples.iter_mut() {
|
||||
example.fetch().await?;
|
||||
example.setup().await?;
|
||||
}
|
||||
|
||||
let examples = Arc::new(Mutex::new(VecDeque::from(examples)));
|
||||
let results_by_example_name = Arc::new(Mutex::new(HashMap::default()));
|
||||
let judge_repetitions = args.judge_repetitions;
|
||||
let concurrency = args.concurrency;
|
||||
|
||||
future::join_all((0..args.concurrency).map(|_| {
|
||||
let tasks = examples.iter().map(|example| {
|
||||
let app_state = app_state.clone();
|
||||
let model = model.clone();
|
||||
let zed_commit_sha = zed_commit_sha.clone();
|
||||
let zed_branch_name = zed_branch_name.clone();
|
||||
let run_id = run_id.clone();
|
||||
let examples = examples.clone();
|
||||
let results = results_by_example_name.clone();
|
||||
let example = example.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
loop {
|
||||
let Some(mut example) = examples.lock().pop_front() else {
|
||||
break;
|
||||
};
|
||||
let result = example.evaluate();
|
||||
results
|
||||
.lock()
|
||||
.entry(example.name.clone())
|
||||
.or_insert(Vec::new())
|
||||
.push((example.clone(), result));
|
||||
let result = async {
|
||||
let run_output = cx
|
||||
.update(|cx| example.run(model.clone(), app_state.clone(), cx))?
|
||||
.await?;
|
||||
let judge_tasks = (0..judge_repetitions).map(|round| {
|
||||
run_judge_repetition(
|
||||
example.clone(),
|
||||
model.clone(),
|
||||
&run_output,
|
||||
round,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let judge_outputs = future::join_all(judge_tasks).await;
|
||||
anyhow::Ok((run_output, judge_outputs))
|
||||
}
|
||||
.await;
|
||||
(example, result)
|
||||
})
|
||||
}))
|
||||
.await;
|
||||
});
|
||||
|
||||
let results = futures::stream::iter(tasks)
|
||||
.buffer_unordered(concurrency)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
println!("\n\n");
|
||||
print_header("EVAL RESULTS");
|
||||
@@ -292,64 +279,59 @@ fn main() {
|
||||
let mut thread_scores = Vec::new();
|
||||
let mut error_count = 0;
|
||||
|
||||
for (example_name, results) in results_by_example_name.lock().iter_mut() {
|
||||
print_header(&example_name);
|
||||
for (example, result) in results {
|
||||
print_header(&example.name);
|
||||
|
||||
results.sort_unstable_by_key(|(example, _)| example.repetition);
|
||||
let mut example_cumulative_tool_metrics = ToolMetrics::default();
|
||||
match result {
|
||||
Err(err) => {
|
||||
println!("💥 {}{:?}", example.log_prefix, err);
|
||||
error_count += 1;
|
||||
}
|
||||
Ok((run_output, judge_results)) => {
|
||||
cumulative_tool_metrics.merge(&run_output.tool_metrics);
|
||||
|
||||
println!("┌───────┬──────┬────────┐");
|
||||
println!("│ Round │ Diff │ Thread │");
|
||||
println!("├───────┼──────┼────────┤");
|
||||
for (example, result) in results {
|
||||
let run_dir_path = example.run_directory_path();
|
||||
let relative_run_dir_path = run_dir_path.strip_prefix(root_dir).unwrap();
|
||||
|
||||
match result {
|
||||
Err(err) => {
|
||||
println!(
|
||||
"|{:^7}│{:^6}│{:^8}│ {:?}{}",
|
||||
example.repetition,
|
||||
"N/A",
|
||||
"N/A",
|
||||
err,
|
||||
relative_run_dir_path.display()
|
||||
);
|
||||
error_count += 1;
|
||||
}
|
||||
Ok((run_output, judge_result)) => {
|
||||
cumulative_tool_metrics.merge(&run_output.tool_metrics);
|
||||
example_cumulative_tool_metrics.merge(&run_output.tool_metrics);
|
||||
println!("┌───────┬──────┬────────┐");
|
||||
println!("│ Judge │ Diff │ Thread │");
|
||||
println!("├───────┼──────┼────────┤");
|
||||
|
||||
for (i, judge_result) in judge_results.iter().enumerate() {
|
||||
match judge_result {
|
||||
Ok(judge_output) => {
|
||||
diff_scores.push(judge_output.diff.score());
|
||||
thread_scores.push(judge_output.thread.score());
|
||||
let diff_score = judge_output.diff.score;
|
||||
diff_scores.push(diff_score);
|
||||
|
||||
let thread_display = if let Some(thread) = &judge_output.thread
|
||||
{
|
||||
let thread_score = thread.score;
|
||||
thread_scores.push(thread_score);
|
||||
format!("{}", thread_score)
|
||||
} else {
|
||||
"N/A".to_string()
|
||||
};
|
||||
|
||||
println!(
|
||||
"|{:^7}│{:^6}│{:^8}│ {}",
|
||||
example.repetition,
|
||||
format!("{}%", judge_output.diff.score()),
|
||||
format!("{}%", judge_output.thread.score()),
|
||||
relative_run_dir_path.display()
|
||||
"|{:^7}│{:^6}│{:^8}│",
|
||||
i + 1,
|
||||
diff_score,
|
||||
thread_display
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
println!(
|
||||
"|{:^7}│{:^6}│{:^8}│{:?}│ {}",
|
||||
example.repetition,
|
||||
"N/A",
|
||||
"N/A",
|
||||
err,
|
||||
relative_run_dir_path.display()
|
||||
);
|
||||
println!("|{:^7}│{:^6}│{:^8}│{:?}", i + 1, "N/A", "N/A", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("└───────┴──────┴────────┘");
|
||||
|
||||
println!("{}", run_output.tool_metrics);
|
||||
}
|
||||
}
|
||||
|
||||
println!("└───────┴──────┴────────┘");
|
||||
println!("{}", example_cumulative_tool_metrics);
|
||||
println!(
|
||||
"{} > {}",
|
||||
" ".repeat(max_name_width),
|
||||
example.example_output_directory().display()
|
||||
);
|
||||
}
|
||||
|
||||
let diff_score_count = diff_scores.len();
|
||||
@@ -363,21 +345,31 @@ fn main() {
|
||||
println!("\n{error_count} examples failed to run!");
|
||||
}
|
||||
|
||||
println!("\nAverage code diff score: {average_diff_score}");
|
||||
if diff_score_count > 0 {
|
||||
println!("\nAverage code diff score: {average_diff_score}");
|
||||
}
|
||||
|
||||
let thread_score_count = thread_scores.len();
|
||||
let average_thread_score = thread_scores
|
||||
.into_iter()
|
||||
.map(|score| score as f32)
|
||||
.sum::<f32>()
|
||||
/ (thread_score_count as f32);
|
||||
|
||||
println!("\nAverage thread score: {average_thread_score}");
|
||||
// We might have gotten no thread scores if we weren't asked to judge the thread.
|
||||
if thread_score_count > 0 {
|
||||
let average_thread_score = thread_scores
|
||||
.into_iter()
|
||||
.map(|score| score as f32)
|
||||
.sum::<f32>()
|
||||
/ (thread_score_count as f32);
|
||||
|
||||
if diff_score_count > 0 {
|
||||
println!("\nAverage thread score: {average_thread_score}");
|
||||
}
|
||||
}
|
||||
|
||||
print_header("CUMULATIVE TOOL METRICS");
|
||||
println!("{}", cumulative_tool_metrics);
|
||||
|
||||
app_state.client.telemetry().flush_events().await;
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
|
||||
app_state.client.telemetry().flush_events();
|
||||
|
||||
cx.update(|cx| cx.quit())
|
||||
})
|
||||
@@ -385,8 +377,8 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
fn list_all_examples(examples_dir: &Path) -> Result<Vec<PathBuf>> {
|
||||
let path = std::fs::canonicalize(examples_dir).unwrap();
|
||||
fn list_all_examples() -> Result<Vec<PathBuf>> {
|
||||
let path = std::fs::canonicalize(EXAMPLES_DIR).unwrap();
|
||||
let entries = std::fs::read_dir(path).unwrap();
|
||||
let mut result_paths = Vec::new();
|
||||
for entry in entries {
|
||||
@@ -399,21 +391,19 @@ fn list_all_examples(examples_dir: &Path) -> Result<Vec<PathBuf>> {
|
||||
Ok(result_paths)
|
||||
}
|
||||
|
||||
/// GPUI application state for the eval binary
|
||||
pub struct EvalAppState {
|
||||
/// Subset of `workspace::AppState` needed by `HeadlessAssistant`, with additional fields.
|
||||
pub struct AgentAppState {
|
||||
pub languages: Arc<LanguageRegistry>,
|
||||
pub client: Arc<Client>,
|
||||
pub user_store: Entity<UserStore>,
|
||||
pub fs: Arc<dyn fs::Fs>,
|
||||
pub node_runtime: NodeRuntime,
|
||||
pub commit_sha: String,
|
||||
pub branch_name: String,
|
||||
pub run_id: String,
|
||||
pub enable_telemetry: bool,
|
||||
|
||||
// Additional fields not present in `workspace::AppState`.
|
||||
pub prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut App) -> Arc<EvalAppState> {
|
||||
pub fn init(cx: &mut App) -> Arc<AgentAppState> {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
gpui_tokio::init(cx);
|
||||
|
||||
@@ -508,7 +498,7 @@ pub fn init(cx: &mut App) -> Arc<EvalAppState> {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Arc::new(EvalAppState {
|
||||
Arc::new(AgentAppState {
|
||||
languages,
|
||||
client,
|
||||
user_store,
|
||||
@@ -542,83 +532,81 @@ pub fn find_model(
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
pub fn commit_sha_for_path(repo_path: &Path) -> String {
|
||||
futures::executor::block_on(run_git(repo_path, &["rev-parse", "HEAD"])).unwrap()
|
||||
pub async fn get_current_commit_id(repo_path: &Path) -> Option<String> {
|
||||
(run_git(repo_path, &["rev-parse", "HEAD"]).await).ok()
|
||||
}
|
||||
|
||||
pub fn git_branch_for_path(repo_path: &Path) -> String {
|
||||
match std::env::var("GITHUB_REF_NAME") {
|
||||
Ok(branch) => branch,
|
||||
Err(_) => {
|
||||
futures::executor::block_on(run_git(repo_path, &["rev-parse", "--abbrev-ref", "HEAD"]))
|
||||
.unwrap_or_else(|_| "unknown".to_string())
|
||||
pub fn get_current_commit_id_sync(repo_path: &Path) -> String {
|
||||
futures::executor::block_on(async {
|
||||
get_current_commit_id(repo_path).await.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_judge_repetition(
|
||||
example: Example,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
run_output: &RunOutput,
|
||||
round: u32,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<JudgeOutput> {
|
||||
let judge_result = example.judge(model.clone(), &run_output, round, cx).await;
|
||||
|
||||
if let Ok(judge_output) = &judge_result {
|
||||
let cohort_id = example
|
||||
.run_directory_path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or(chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string());
|
||||
|
||||
let path = std::path::Path::new(".");
|
||||
let commit_id = get_current_commit_id(path).await.unwrap_or_default();
|
||||
|
||||
if let Some(thread) = &judge_output.thread {
|
||||
telemetry::event!(
|
||||
"Agent Eval Completed",
|
||||
cohort_id = cohort_id,
|
||||
example_name = example.name.clone(),
|
||||
round = round,
|
||||
diff_score = judge_output.diff.score,
|
||||
diff_analysis = judge_output.diff.analysis,
|
||||
thread_score = thread.score,
|
||||
thread_analysis = thread.analysis,
|
||||
tool_metrics = run_output.tool_metrics,
|
||||
response_count = run_output.response_count,
|
||||
token_usage = run_output.token_usage,
|
||||
model = model.telemetry_id(),
|
||||
model_provider = model.provider_id().to_string(),
|
||||
repository_url = example.base.url.clone(),
|
||||
repository_revision = example.base.revision.clone(),
|
||||
diagnostics_before = run_output.diagnostics_before,
|
||||
diagnostics_after = run_output.diagnostics_after,
|
||||
commit_id = commit_id
|
||||
);
|
||||
} else {
|
||||
telemetry::event!(
|
||||
"Agent Eval Completed",
|
||||
cohort_id = cohort_id,
|
||||
example_name = example.name.clone(),
|
||||
round = round,
|
||||
diff_score = judge_output.diff.score,
|
||||
diff_analysis = judge_output.diff.analysis,
|
||||
tool_metrics = run_output.tool_metrics,
|
||||
response_count = run_output.response_count,
|
||||
token_usage = run_output.token_usage,
|
||||
model = model.telemetry_id(),
|
||||
model_provider = model.provider_id().to_string(),
|
||||
repository_url = example.base.url.clone(),
|
||||
repository_revision = example.base.revision.clone(),
|
||||
diagnostics_before = run_output.diagnostics_before,
|
||||
diagnostics_after = run_output.diagnostics_after,
|
||||
commit_id = commit_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Clone)]
|
||||
pub struct Sample {
|
||||
pub repository_diff: String,
|
||||
pub ran_diagnostics_check: bool,
|
||||
pub diagnostic_summary_before: DiagnosticSummary,
|
||||
pub diagnostic_summary_after: DiagnosticSummary,
|
||||
pub diagnostics_before: Option<String>,
|
||||
pub diagnostics_after: Option<String>,
|
||||
pub response_count: usize,
|
||||
pub token_usage: TokenUsage,
|
||||
pub tool_metrics: ToolMetrics,
|
||||
pub last_request: LanguageModelRequest,
|
||||
pub error: Option<SamplingError>,
|
||||
judge_result
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
struct SamplingError {
|
||||
message: String,
|
||||
full_stack: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct Evaluation {
|
||||
metadata: EvaluationMetadata,
|
||||
sample: Sample,
|
||||
diff_evaluation: DiffEvaluation,
|
||||
thread_evaluation: ThreadEvaluation,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct EvaluationMetadata {
|
||||
zed_commit_sha: String,
|
||||
zed_branch_name: String,
|
||||
run_id: String,
|
||||
example_name: String,
|
||||
example_repetition: usize,
|
||||
model: String,
|
||||
model_provider: String,
|
||||
repository_url: String,
|
||||
repository_revision: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
struct DiffEvaluation {
|
||||
assertions: Vec<EvaluatedAssertion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
struct ThreadEvaluation {
|
||||
assertions: Vec<EvaluatedAssertion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
struct EvaluatedAssertion {
|
||||
assertion: Assertion,
|
||||
passed: bool,
|
||||
analysis: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
struct Assertion(String);
|
||||
|
||||
fn print_header(header: &str) {
|
||||
println!("\n========================================");
|
||||
println!("{:^40}", header);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::Result;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use uuid::Uuid;
|
||||
@@ -11,7 +11,6 @@ pub fn get_or_create_id(path: &Path) -> Result<String> {
|
||||
}
|
||||
}
|
||||
let new_id = Uuid::new_v4().to_string();
|
||||
fs::create_dir_all(path.parent().ok_or_else(|| anyhow!("invalid id path"))?)?;
|
||||
fs::write(path, &new_id)?;
|
||||
Ok(new_id)
|
||||
}
|
||||
|
||||
@@ -1,25 +1,48 @@
|
||||
You are an expert software developer. Your task is to evaluate a diff produced by an AI agent in response to a prompt.
|
||||
Here is the prompt and the diff:
|
||||
You are an expert software developer tasked with evaluating the following changes to a codebase:
|
||||
|
||||
<prompt>
|
||||
{{{prompt}}}
|
||||
</prompt>
|
||||
<changes>
|
||||
{{repository_diff}}
|
||||
</changes>
|
||||
|
||||
<diff>
|
||||
{{{repository_diff}}}
|
||||
</diff>
|
||||
|
||||
Evaluate how many of the following criteria were satisfied by the diff:
|
||||
Use the following criteria to score the above changes.
|
||||
|
||||
<criteria>
|
||||
{{criteria}}
|
||||
- There are no changes unrelated to the prompt
|
||||
</criteria>
|
||||
|
||||
Analyze the diff hunk by hunk, and structure your answer in the following XML format:
|
||||
{{#if ran_diagnostics_check}}
|
||||
Take into account the diagnostics before and after applying the change:
|
||||
|
||||
<diagnostics_before>
|
||||
{{#if diagnostics_before}}
|
||||
{{{diagnostics_before}}}
|
||||
{{else}}
|
||||
No diagnostics before applying the edits.
|
||||
{{/if}}
|
||||
</diagnostics_before>
|
||||
|
||||
<diagnostics_after>
|
||||
{{#if diagnostics_after}}
|
||||
{{{diagnostics_after}}}
|
||||
{{else}}
|
||||
No diagnostics after applying the edits.
|
||||
{{/if}}
|
||||
</diagnostics_after>
|
||||
{{else}}
|
||||
No diagnostic checks were performed.
|
||||
{{/if}}
|
||||
|
||||
Based on these criteria, give the test output a score between 0 and 5.
|
||||
The output score should ONLY INCLUDE whole numbers. DO NOT return decimals or floats.
|
||||
|
||||
- 5 means: changes meet all criteria
|
||||
- 0 means: changes don't meet any criteria
|
||||
|
||||
Be suspicious of the changes because they were generated by an LLM.
|
||||
Sometimes the LLM decides to change random code, so if the changes are not mentioned in the criteria, penalize the score.
|
||||
Analyze the diff hunk by hunk and describe how each change meets or fails to meet the criteria.
|
||||
|
||||
```
|
||||
<analysis>{YOUR ANALYSIS HERE}</analysis>
|
||||
<total_criteria>{THE TOTAL NUMBER OF CRITERIA THAT WERE LISTED}</total_criteria>
|
||||
<passing_criteria>{THE NUMBER OF CRITERIA THAT ARE MET BY THE DIFF}</passing_criteria>
|
||||
<score>{YOUR SCORE HERE}</score>
|
||||
```
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
You are an expert software developer. Your task is to evaluate an AI agent's messages and tool calls in this conversation:
|
||||
You are an expert software developer tasked with evaluating an AI agent's messages and tool calls in this conversation:
|
||||
|
||||
<messages>
|
||||
{{{messages}}}
|
||||
</messages>
|
||||
|
||||
You must count how many of the following criteria were satisfied by the messages:
|
||||
Use the following criteria to score the above messages.
|
||||
|
||||
<criteria>
|
||||
{{{criteria}}}
|
||||
{{criteria}}
|
||||
</criteria>
|
||||
|
||||
Analyze the messages one by one, and structure your answer in the following XML format:
|
||||
Based on these criteria, give the messages a score between 0 and 5.
|
||||
The output score should ONLY INCLUDE whole numbers. DO NOT return decimals or floats.
|
||||
|
||||
- 5 means: messages meet all criteria
|
||||
- 0 means: messages don't meet any criteria
|
||||
|
||||
```
|
||||
<analysis>{YOUR ANALYSIS HERE}</analysis>
|
||||
<total_criteria>{THE TOTAL NUMBER OF CRITERIA THAT WERE LISTED}</total_criteria>
|
||||
<passing_criteria>{THE NUMBER OF CRITERIA THAT ARE MET BY THE MESSAGES}</passing_criteria>
|
||||
<score>{YOUR SCORE HERE}</score>
|
||||
```
|
||||
|
||||
@@ -1568,7 +1568,7 @@ mod tests {
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
".git": {},
|
||||
".git":{},
|
||||
"a.txt": "created\n",
|
||||
"b.txt": "really changed\n",
|
||||
"c.txt": "unchanged\n"
|
||||
@@ -1646,87 +1646,4 @@ mod tests {
|
||||
"
|
||||
));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_excerpts_splitting_after_restoring_the_middle_excerpt(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let git_contents = indoc! {r#"
|
||||
#[rustfmt::skip]
|
||||
fn main() {
|
||||
let x = 0.0; // this line will be removed
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
let y = 0.0; // this line will be removed
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
let arr = [
|
||||
0.0, // this line will be removed
|
||||
0.0, // this line will be removed
|
||||
0.0, // this line will be removed
|
||||
0.0, // this line will be removed
|
||||
];
|
||||
}
|
||||
"#};
|
||||
let buffer_contents = indoc! {"
|
||||
#[rustfmt::skip]
|
||||
fn main() {
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
let arr = [
|
||||
];
|
||||
}
|
||||
"};
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
".git": {},
|
||||
"main.rs": buffer_contents,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_git_content_for_repo(
|
||||
Path::new("/a/.git"),
|
||||
&[("main.rs".into(), git_contents.to_owned(), None)],
|
||||
);
|
||||
|
||||
let project = Project::test(fs, [Path::new("/a")], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.focus(&workspace);
|
||||
cx.update(|window, cx| {
|
||||
window.dispatch_action(project_diff::Diff.boxed_clone(), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let item = workspace.update(cx, |workspace, cx| {
|
||||
workspace.active_item_as::<ProjectDiff>(cx).unwrap()
|
||||
});
|
||||
cx.focus(&item);
|
||||
let editor = item.update(cx, |item, _| item.editor.clone());
|
||||
|
||||
let mut cx = EditorTestContext::for_editor_in(editor, cx).await;
|
||||
|
||||
cx.assert_excerpts_with_selections(&format!("[EXCERPT]\nˇ{git_contents}"));
|
||||
|
||||
cx.dispatch_action(editor::actions::GoToHunk);
|
||||
cx.dispatch_action(editor::actions::GoToHunk);
|
||||
cx.dispatch_action(git::Restore);
|
||||
cx.dispatch_action(editor::actions::MoveToBeginning);
|
||||
|
||||
cx.assert_excerpts_with_selections(&format!("[EXCERPT]\nˇ{git_contents}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,8 +402,6 @@ pub enum Model {
|
||||
Gemini25ProExp0325,
|
||||
#[serde(rename = "gemini-2.5-pro-preview-03-25")]
|
||||
Gemini25ProPreview0325,
|
||||
#[serde(rename = "gemini-2.5-flash-preview-04-17")]
|
||||
Gemini25FlashPreview0417,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
@@ -428,7 +426,6 @@ impl Model {
|
||||
Model::Gemini20FlashLite => "gemini-2.0-flash-lite-preview",
|
||||
Model::Gemini25ProExp0325 => "gemini-2.5-pro-exp-03-25",
|
||||
Model::Gemini25ProPreview0325 => "gemini-2.5-pro-preview-03-25",
|
||||
Model::Gemini25FlashPreview0417 => "gemini-2.5-flash-preview-04-17",
|
||||
Model::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
@@ -443,7 +440,6 @@ impl Model {
|
||||
Model::Gemini20FlashLite => "Gemini 2.0 Flash Lite",
|
||||
Model::Gemini25ProExp0325 => "Gemini 2.5 Pro Exp",
|
||||
Model::Gemini25ProPreview0325 => "Gemini 2.5 Pro Preview",
|
||||
Model::Gemini25FlashPreview0417 => "Gemini 2.5 Flash Preview",
|
||||
Self::Custom {
|
||||
name, display_name, ..
|
||||
} => display_name.as_ref().unwrap_or(name),
|
||||
@@ -460,7 +456,6 @@ impl Model {
|
||||
Model::Gemini20FlashLite => 1_000_000,
|
||||
Model::Gemini25ProExp0325 => 1_000_000,
|
||||
Model::Gemini25ProPreview0325 => 1_000_000,
|
||||
Model::Gemini25FlashPreview0417 => 1_000_000,
|
||||
Model::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1497,15 +1497,6 @@ impl Hash for Image {
|
||||
}
|
||||
|
||||
impl Image {
|
||||
/// An empty image containing no data
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
format: ImageFormat::Png,
|
||||
bytes: Vec::new(),
|
||||
id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get this image's ID
|
||||
pub fn id(&self) -> u64 {
|
||||
self.id
|
||||
|
||||
@@ -139,7 +139,6 @@ pub enum IconName {
|
||||
Globe,
|
||||
Hash,
|
||||
HistoryRerun,
|
||||
Image,
|
||||
Indicator,
|
||||
Info,
|
||||
InlayHint,
|
||||
|
||||
@@ -105,7 +105,6 @@ impl CloudModel {
|
||||
| google_ai::Model::Gemini20FlashLite
|
||||
| google_ai::Model::Gemini25ProExp0325
|
||||
| google_ai::Model::Gemini25ProPreview0325
|
||||
| google_ai::Model::Gemini25FlashPreview0417
|
||||
| google_ai::Model::Custom { .. } => {
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
|
||||
}
|
||||
|
||||
@@ -32,14 +32,7 @@ impl std::fmt::Debug for LanguageModelImage {
|
||||
const ANTHROPIC_SIZE_LIMT: f32 = 1568.;
|
||||
|
||||
impl LanguageModelImage {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
source: "".into(),
|
||||
size: size(DevicePixels(0), DevicePixels(0)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_image(data: Arc<Image>, cx: &mut App) -> Task<Option<Self>> {
|
||||
pub fn from_image(data: Image, cx: &mut App) -> Task<Option<Self>> {
|
||||
cx.background_spawn(async move {
|
||||
match data.format() {
|
||||
gpui::ImageFormat::Png
|
||||
@@ -228,16 +221,23 @@ impl LanguageModelRequestMessage {
|
||||
}
|
||||
|
||||
pub fn contents_empty(&self) -> bool {
|
||||
self.content.iter().all(|content| match content {
|
||||
MessageContent::Text(text) => text.chars().all(|c| c.is_whitespace()),
|
||||
MessageContent::Thinking { text, .. } => text.chars().all(|c| c.is_whitespace()),
|
||||
MessageContent::ToolResult(tool_result) => {
|
||||
tool_result.content.chars().all(|c| c.is_whitespace())
|
||||
}
|
||||
MessageContent::RedactedThinking(_)
|
||||
| MessageContent::ToolUse(_)
|
||||
| MessageContent::Image(_) => false,
|
||||
})
|
||||
self.content.is_empty()
|
||||
|| self
|
||||
.content
|
||||
.first()
|
||||
.map(|content| match content {
|
||||
MessageContent::Text(text) => text.chars().all(|c| c.is_whitespace()),
|
||||
MessageContent::Thinking { text, .. } => {
|
||||
text.chars().all(|c| c.is_whitespace())
|
||||
}
|
||||
MessageContent::ToolResult(tool_result) => {
|
||||
tool_result.content.chars().all(|c| c.is_whitespace())
|
||||
}
|
||||
MessageContent::RedactedThinking(_)
|
||||
| MessageContent::ToolUse(_)
|
||||
| MessageContent::Image(_) => true,
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -384,14 +384,7 @@ pub fn into_google(
|
||||
}
|
||||
}
|
||||
language_model::MessageContent::RedactedThinking(_) => None,
|
||||
language_model::MessageContent::Image(image) => {
|
||||
Some(Part::InlineDataPart(google_ai::InlineDataPart {
|
||||
inline_data: google_ai::GenerativeContentBlob {
|
||||
mime_type: "image/png".to_string(),
|
||||
data: image.source.to_string(),
|
||||
},
|
||||
}))
|
||||
}
|
||||
language_model::MessageContent::Image(_) => None,
|
||||
language_model::MessageContent::ToolUse(tool_use) => {
|
||||
Some(Part::FunctionCallPart(google_ai::FunctionCallPart {
|
||||
function_call: google_ai::FunctionCall {
|
||||
|
||||
@@ -129,7 +129,7 @@ impl LspAdapter for RustLspAdapter {
|
||||
})
|
||||
.await;
|
||||
if let Err(err) = result {
|
||||
log::debug!(
|
||||
log::error!(
|
||||
"failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
|
||||
path,
|
||||
err
|
||||
|
||||
@@ -1687,7 +1687,7 @@ impl MultiBuffer {
|
||||
if let Some(last_range) = merged_ranges.last_mut() {
|
||||
debug_assert!(last_range.context.start <= range.context.start);
|
||||
if last_range.context.end >= range.context.start {
|
||||
last_range.context.end = range.context.end.max(last_range.context.end);
|
||||
last_range.context.end = range.context.end;
|
||||
*counts.last_mut().unwrap() += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -1741,106 +1741,106 @@ impl MultiBuffer {
|
||||
excerpts_cursor.next(&());
|
||||
|
||||
loop {
|
||||
let new = new_iter.peek();
|
||||
let existing = if let Some(existing_id) = existing_iter.peek() {
|
||||
let locator = snapshot.excerpt_locator_for_id(*existing_id);
|
||||
excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &());
|
||||
if let Some(excerpt) = excerpts_cursor.item() {
|
||||
if excerpt.buffer_id != buffer_snapshot.remote_id() {
|
||||
to_remove.push(*existing_id);
|
||||
existing_iter.next();
|
||||
continue;
|
||||
}
|
||||
Some((
|
||||
*existing_id,
|
||||
excerpt.range.context.to_point(&buffer_snapshot),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some((last_id, last)) = to_insert.last_mut() {
|
||||
if let Some(new) = new {
|
||||
if last.context.end >= new.context.start {
|
||||
last.context.end = last.context.end.max(new.context.end);
|
||||
excerpt_ids.push(*last_id);
|
||||
new_iter.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some((existing_id, existing_range)) = &existing {
|
||||
if last.context.end >= existing_range.start {
|
||||
last.context.end = last.context.end.max(existing_range.end);
|
||||
to_remove.push(*existing_id);
|
||||
self.snapshot
|
||||
.borrow_mut()
|
||||
.replaced_excerpts
|
||||
.insert(*existing_id, *last_id);
|
||||
existing_iter.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (new, existing) {
|
||||
let (new, existing) = match (new_iter.peek(), existing_iter.peek()) {
|
||||
(Some(new), Some(existing)) => (new, existing),
|
||||
(None, None) => break,
|
||||
(None, Some((existing_id, _))) => {
|
||||
existing_iter.next();
|
||||
(None, Some(_)) => {
|
||||
let existing_id = existing_iter.next().unwrap();
|
||||
if let Some((new_id, last)) = to_insert.last() {
|
||||
let locator = snapshot.excerpt_locator_for_id(existing_id);
|
||||
excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &());
|
||||
if let Some(existing_excerpt) = excerpts_cursor
|
||||
.item()
|
||||
.filter(|e| e.buffer_id == buffer_snapshot.remote_id())
|
||||
{
|
||||
let existing_end = existing_excerpt
|
||||
.range
|
||||
.context
|
||||
.end
|
||||
.to_point(&buffer_snapshot);
|
||||
if existing_end <= last.context.end {
|
||||
self.snapshot
|
||||
.borrow_mut()
|
||||
.replaced_excerpts
|
||||
.insert(existing_id, *new_id);
|
||||
}
|
||||
};
|
||||
}
|
||||
to_remove.push(existing_id);
|
||||
continue;
|
||||
}
|
||||
(Some(_), None) => {
|
||||
added_a_new_excerpt = true;
|
||||
let new_id = next_excerpt_id();
|
||||
excerpt_ids.push(new_id);
|
||||
to_insert.push((new_id, new_iter.next().unwrap()));
|
||||
to_insert.push((next_excerpt_id(), new_iter.next().unwrap()));
|
||||
continue;
|
||||
}
|
||||
(Some(new), Some((_, existing_range))) => {
|
||||
if existing_range.end < new.context.start {
|
||||
let existing_id = existing_iter.next().unwrap();
|
||||
to_remove.push(existing_id);
|
||||
continue;
|
||||
} else if existing_range.start > new.context.end {
|
||||
let new_id = next_excerpt_id();
|
||||
excerpt_ids.push(new_id);
|
||||
to_insert.push((new_id, new_iter.next().unwrap()));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let locator = snapshot.excerpt_locator_for_id(*existing);
|
||||
excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &());
|
||||
let Some(existing_excerpt) = excerpts_cursor
|
||||
.item()
|
||||
.filter(|e| e.buffer_id == buffer_snapshot.remote_id())
|
||||
else {
|
||||
to_remove.push(existing_iter.next().unwrap());
|
||||
to_insert.push((next_excerpt_id(), new_iter.next().unwrap()));
|
||||
continue;
|
||||
};
|
||||
|
||||
if existing_range.start == new.context.start
|
||||
&& existing_range.end == new.context.end
|
||||
{
|
||||
self.insert_excerpts_with_ids_after(
|
||||
insert_after,
|
||||
buffer.clone(),
|
||||
mem::take(&mut to_insert),
|
||||
cx,
|
||||
);
|
||||
insert_after = existing_iter.next().unwrap();
|
||||
excerpt_ids.push(insert_after);
|
||||
new_iter.next();
|
||||
} else {
|
||||
let existing_id = existing_iter.next().unwrap();
|
||||
let new_id = next_excerpt_id();
|
||||
let existing_start = existing_excerpt
|
||||
.range
|
||||
.context
|
||||
.start
|
||||
.to_point(&buffer_snapshot);
|
||||
let existing_end = existing_excerpt
|
||||
.range
|
||||
.context
|
||||
.end
|
||||
.to_point(&buffer_snapshot);
|
||||
|
||||
if existing_end < new.context.start {
|
||||
let existing_id = existing_iter.next().unwrap();
|
||||
if let Some((new_id, last)) = to_insert.last() {
|
||||
if existing_end <= last.context.end {
|
||||
self.snapshot
|
||||
.borrow_mut()
|
||||
.replaced_excerpts
|
||||
.insert(existing_id, new_id);
|
||||
to_remove.push(existing_id);
|
||||
let mut range = new_iter.next().unwrap();
|
||||
range.context.start = range.context.start.min(existing_range.start);
|
||||
range.context.end = range.context.end.max(existing_range.end);
|
||||
excerpt_ids.push(new_id);
|
||||
to_insert.push((new_id, range));
|
||||
.insert(existing_id, *new_id);
|
||||
}
|
||||
}
|
||||
};
|
||||
to_remove.push(existing_id);
|
||||
continue;
|
||||
} else if existing_start > new.context.end {
|
||||
to_insert.push((next_excerpt_id(), new_iter.next().unwrap()));
|
||||
continue;
|
||||
}
|
||||
|
||||
if existing_start == new.context.start && existing_end == new.context.end {
|
||||
excerpt_ids.extend(to_insert.iter().map(|(id, _)| id));
|
||||
self.insert_excerpts_with_ids_after(
|
||||
insert_after,
|
||||
buffer.clone(),
|
||||
mem::take(&mut to_insert),
|
||||
cx,
|
||||
);
|
||||
insert_after = existing_iter.next().unwrap();
|
||||
excerpt_ids.push(insert_after);
|
||||
new_iter.next();
|
||||
} else {
|
||||
let existing_id = existing_iter.next().unwrap();
|
||||
let new_id = next_excerpt_id();
|
||||
self.snapshot
|
||||
.borrow_mut()
|
||||
.replaced_excerpts
|
||||
.insert(existing_id, new_id);
|
||||
to_remove.push(existing_id);
|
||||
let mut range = new_iter.next().unwrap();
|
||||
range.context.start = range.context.start.min(existing_start);
|
||||
range.context.end = range.context.end.max(existing_end);
|
||||
to_insert.push((new_id, range));
|
||||
}
|
||||
}
|
||||
|
||||
excerpt_ids.extend(to_insert.iter().map(|(id, _)| id));
|
||||
self.insert_excerpts_with_ids_after(insert_after, buffer, to_insert, cx);
|
||||
self.remove_excerpts(to_remove, cx);
|
||||
if excerpt_ids.is_empty() {
|
||||
@@ -1849,8 +1849,7 @@ impl MultiBuffer {
|
||||
for excerpt_id in &excerpt_ids {
|
||||
self.paths_by_excerpt.insert(*excerpt_id, path.clone());
|
||||
}
|
||||
self.excerpts_by_path
|
||||
.insert(path, excerpt_ids.iter().dedup().cloned().collect());
|
||||
self.excerpts_by_path.insert(path, excerpt_ids.clone());
|
||||
}
|
||||
|
||||
(excerpt_ids, added_a_new_excerpt)
|
||||
|
||||
@@ -2476,81 +2476,6 @@ impl ReferenceMultibuffer {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
let base_text = "a\n".repeat(100);
|
||||
let buf = cx.update(|cx| cx.new(|cx| Buffer::local(base_text, cx)));
|
||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
fn row_ranges(ranges: &Vec<Range<Point>>) -> Vec<Range<u32>> {
|
||||
ranges
|
||||
.iter()
|
||||
.map(|range| range.start.row..range.end.row)
|
||||
.collect()
|
||||
}
|
||||
|
||||
for _ in 0..operations {
|
||||
let snapshot = buf.update(cx, |buf, _| buf.snapshot());
|
||||
let num_ranges = rng.gen_range(0..=10);
|
||||
let max_row = snapshot.max_point().row;
|
||||
let mut ranges = (0..num_ranges)
|
||||
.map(|_| {
|
||||
let start = rng.gen_range(0..max_row);
|
||||
let end = rng.gen_range(start + 1..max_row + 1);
|
||||
Point::row_range(start..end)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
ranges.sort_by_key(|range| range.start);
|
||||
log::info!("Setting ranges: {:?}", row_ranges(&ranges));
|
||||
let (created, _) = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buf, cx),
|
||||
buf.clone(),
|
||||
ranges.clone(),
|
||||
2,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(created.len(), ranges.len());
|
||||
|
||||
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||||
let mut last_end = None;
|
||||
let mut seen_ranges = Vec::default();
|
||||
|
||||
for (_, buf, range) in snapshot.excerpts() {
|
||||
let start = range.context.start.to_point(&buf);
|
||||
let end = range.context.end.to_point(&buf);
|
||||
seen_ranges.push(start..end);
|
||||
|
||||
if let Some(last_end) = last_end.take() {
|
||||
assert!(
|
||||
start > last_end,
|
||||
"multibuffer has out-of-order ranges: {:?}; {:?} <= {:?}",
|
||||
row_ranges(&seen_ranges),
|
||||
start,
|
||||
last_end
|
||||
)
|
||||
}
|
||||
|
||||
ranges.retain(|range| range.start < start || range.end > end);
|
||||
|
||||
last_end = Some(end)
|
||||
}
|
||||
|
||||
assert!(
|
||||
ranges.is_empty(),
|
||||
"multibuffer {:?} did not include all ranges: {:?}",
|
||||
row_ranges(&seen_ranges),
|
||||
row_ranges(&ranges)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
let operations = env::var("OPERATIONS")
|
||||
|
||||
@@ -401,7 +401,7 @@ pub fn task_file_name() -> &'static str {
|
||||
"tasks.json"
|
||||
}
|
||||
|
||||
/// Returns the relative path to a `debug.json` file within a project.
|
||||
/// Returns the relative path to a `launch.json` file within a project.
|
||||
pub fn local_debug_file_relative_path() -> &'static Path {
|
||||
Path::new(".zed/debug.json")
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Task}
|
||||
use lsp::LanguageServerName;
|
||||
use paths::{
|
||||
EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
|
||||
local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
|
||||
local_vscode_tasks_file_relative_path,
|
||||
local_tasks_file_relative_path, local_vscode_tasks_file_relative_path,
|
||||
};
|
||||
use rpc::{
|
||||
AnyProtoClient, TypedEnvelope,
|
||||
@@ -25,7 +24,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
|
||||
use task::{TaskTemplates, VsCodeTaskFile};
|
||||
use util::{ResultExt, serde::default_true};
|
||||
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
|
||||
|
||||
@@ -574,18 +573,6 @@ impl SettingsObserver {
|
||||
.unwrap(),
|
||||
);
|
||||
(settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
|
||||
} else if path.ends_with(local_vscode_launch_file_relative_path()) {
|
||||
let settings_dir = Arc::<Path>::from(
|
||||
path.ancestors()
|
||||
.nth(
|
||||
local_vscode_tasks_file_relative_path()
|
||||
.components()
|
||||
.count()
|
||||
.saturating_sub(1),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
(settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
|
||||
} else if path.ends_with(EDITORCONFIG_NAME) {
|
||||
let Some(settings_dir) = path.parent().map(Arc::from) else {
|
||||
continue;
|
||||
@@ -631,23 +618,6 @@ impl SettingsObserver {
|
||||
"serializing Zed tasks into JSON, file {abs_path:?}"
|
||||
)
|
||||
})
|
||||
} else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
|
||||
let vscode_tasks =
|
||||
parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
|
||||
.with_context(|| {
|
||||
format!("parsing VSCode debug tasks, file {abs_path:?}")
|
||||
})?;
|
||||
let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"converting VSCode debug tasks into Zed ones, file {abs_path:?}"
|
||||
)
|
||||
})?;
|
||||
serde_json::to_string(&zed_tasks).with_context(|| {
|
||||
format!(
|
||||
"serializing Zed tasks into JSON, file {abs_path:?}"
|
||||
)
|
||||
})
|
||||
} else {
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
@@ -34,4 +34,3 @@ workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -4,7 +4,6 @@ mod debug_format;
|
||||
mod serde_helpers;
|
||||
pub mod static_source;
|
||||
mod task_template;
|
||||
mod vscode_debug_format;
|
||||
mod vscode_format;
|
||||
|
||||
use collections::{HashMap, HashSet, hash_map};
|
||||
@@ -23,7 +22,6 @@ pub use task_template::{
|
||||
DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate,
|
||||
TaskTemplates, TaskType,
|
||||
};
|
||||
pub use vscode_debug_format::VsCodeDebugTaskFile;
|
||||
pub use vscode_format::VsCodeTaskFile;
|
||||
pub use zed_actions::RevealTarget;
|
||||
|
||||
@@ -524,50 +522,3 @@ impl ShellBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type VsCodeEnvVariable = String;
|
||||
type ZedEnvVariable = String;
|
||||
|
||||
struct EnvVariableReplacer {
|
||||
variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>,
|
||||
}
|
||||
|
||||
impl EnvVariableReplacer {
|
||||
fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
|
||||
Self { variables }
|
||||
}
|
||||
// Replaces occurrences of VsCode-specific environment variables with Zed equivalents.
|
||||
fn replace(&self, input: &str) -> String {
|
||||
shellexpand::env_with_context_no_errors(&input, |var: &str| {
|
||||
// Colons denote a default value in case the variable is not set. We want to preserve that default, as otherwise shellexpand will substitute it for us.
|
||||
let colon_position = var.find(':').unwrap_or(var.len());
|
||||
let (left, right) = var.split_at(colon_position);
|
||||
if left == "env" && !right.is_empty() {
|
||||
let variable_name = &right[1..];
|
||||
return Some(format!("${{{variable_name}}}"));
|
||||
}
|
||||
let (variable_name, default) = (left, right);
|
||||
let append_previous_default = |ret: &mut String| {
|
||||
if !default.is_empty() {
|
||||
ret.push_str(default);
|
||||
}
|
||||
};
|
||||
if let Some(substitution) = self.variables.get(variable_name) {
|
||||
// Got a VSCode->Zed hit, perform a substitution
|
||||
let mut name = format!("${{{substitution}");
|
||||
append_previous_default(&mut name);
|
||||
name.push('}');
|
||||
return Some(name);
|
||||
}
|
||||
// This is an unknown variable.
|
||||
// We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect.
|
||||
// If there's a default, we need to return the string verbatim as otherwise shellexpand will apply that default for us.
|
||||
if !default.is_empty() {
|
||||
return Some(format!("${{{var}}}"));
|
||||
}
|
||||
// Else we can just return None and that variable will be left as is.
|
||||
None
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use serde::Deserialize;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{
|
||||
AttachRequest, DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate,
|
||||
EnvVariableReplacer, LaunchRequest, TcpArgumentsTemplate, VariableName,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum Request {
|
||||
Launch,
|
||||
Attach,
|
||||
}
|
||||
|
||||
// TODO support preLaunchTask linkage with other tasks
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VsCodeDebugTaskDefinition {
|
||||
r#type: String,
|
||||
name: String,
|
||||
request: Request,
|
||||
|
||||
#[serde(default)]
|
||||
program: Option<String>,
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
env: HashMap<String, Option<String>>,
|
||||
// TODO envFile?
|
||||
#[serde(default)]
|
||||
cwd: Option<String>,
|
||||
#[serde(default)]
|
||||
port: Option<u16>,
|
||||
#[serde(default)]
|
||||
stop_on_entry: Option<bool>,
|
||||
#[serde(flatten)]
|
||||
other_attributes: HashMap<String, serde_json_lenient::Value>,
|
||||
}
|
||||
|
||||
impl VsCodeDebugTaskDefinition {
|
||||
fn try_to_zed(self, replacer: &EnvVariableReplacer) -> anyhow::Result<DebugTaskTemplate> {
|
||||
let label = replacer.replace(&self.name);
|
||||
// TODO based on grep.app results it seems that vscode supports whitespace-splitting this field (ugh)
|
||||
let definition = DebugTaskDefinition {
|
||||
label,
|
||||
request: match self.request {
|
||||
Request::Launch => {
|
||||
let cwd = self.cwd.map(|cwd| PathBuf::from(replacer.replace(&cwd)));
|
||||
let program = self.program.ok_or_else(|| {
|
||||
anyhow!("vscode debug launch configuration does not define a program")
|
||||
})?;
|
||||
let program = replacer.replace(&program);
|
||||
let args = self
|
||||
.args
|
||||
.into_iter()
|
||||
.map(|arg| replacer.replace(&arg))
|
||||
.collect();
|
||||
DebugRequest::Launch(LaunchRequest { program, cwd, args })
|
||||
}
|
||||
Request::Attach => DebugRequest::Attach(AttachRequest { process_id: None }),
|
||||
},
|
||||
adapter: task_type_to_adapter_name(self.r#type),
|
||||
// TODO host?
|
||||
tcp_connection: self.port.map(|port| TcpArgumentsTemplate {
|
||||
port: Some(port),
|
||||
host: None,
|
||||
timeout: None,
|
||||
}),
|
||||
stop_on_entry: self.stop_on_entry,
|
||||
// TODO
|
||||
initialize_args: None,
|
||||
};
|
||||
let template = DebugTaskTemplate {
|
||||
locator: None,
|
||||
definition,
|
||||
};
|
||||
Ok(template)
|
||||
}
|
||||
}
|
||||
|
||||
/// blah
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VsCodeDebugTaskFile {
|
||||
version: String,
|
||||
configurations: Vec<VsCodeDebugTaskDefinition>,
|
||||
}
|
||||
|
||||
impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(file: VsCodeDebugTaskFile) -> Result<Self, Self::Error> {
|
||||
let replacer = EnvVariableReplacer::new(HashMap::from_iter([
|
||||
(
|
||||
"workspaceFolder".to_owned(),
|
||||
VariableName::WorktreeRoot.to_string(),
|
||||
),
|
||||
// TODO other interesting variables?
|
||||
]));
|
||||
let templates = file
|
||||
.configurations
|
||||
.into_iter()
|
||||
.filter_map(|config| config.try_to_zed(&replacer).log_err())
|
||||
.collect::<Vec<_>>();
|
||||
Ok(DebugTaskFile(templates))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO figure out how to make JsDebugAdapter::ADAPTER_NAME et al available here
|
||||
fn task_type_to_adapter_name(task_type: String) -> String {
|
||||
match task_type.as_str() {
|
||||
"node" => "JavaScript".to_owned(),
|
||||
"go" => "Delve".to_owned(),
|
||||
"php" => "PHP".to_owned(),
|
||||
"cppdbg" | "lldb" => "CodeLLDB".to_owned(),
|
||||
"debugpy" => "Debugpy".to_owned(),
|
||||
_ => task_type,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate, LaunchRequest,
|
||||
TcpArgumentsTemplate,
|
||||
};
|
||||
|
||||
use super::VsCodeDebugTaskFile;
|
||||
|
||||
#[test]
|
||||
fn test_parsing_vscode_launch_json() {
|
||||
let raw = r#"
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug my JS app",
|
||||
"request": "launch",
|
||||
"type": "node",
|
||||
"program": "${workspaceFolder}/xyz.js",
|
||||
"showDevDebugOutput": false,
|
||||
"stopOnEntry": true,
|
||||
"args": ["--foo", "${workspaceFolder}/thing"],
|
||||
"cwd": "${workspaceFolder}/${env:FOO}/sub",
|
||||
"env": {
|
||||
"X": "Y"
|
||||
},
|
||||
"port": 17
|
||||
},
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let parsed: VsCodeDebugTaskFile =
|
||||
serde_json_lenient::from_str(&raw).expect("deserializing launch.json");
|
||||
let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
|
||||
pretty_assertions::assert_eq!(
|
||||
zed,
|
||||
DebugTaskFile(vec![DebugTaskTemplate {
|
||||
locator: None,
|
||||
definition: DebugTaskDefinition {
|
||||
label: "Debug my JS app".into(),
|
||||
adapter: "JavaScript".into(),
|
||||
stop_on_entry: Some(true),
|
||||
initialize_args: None,
|
||||
tcp_connection: Some(TcpArgumentsTemplate {
|
||||
port: Some(17),
|
||||
host: None,
|
||||
timeout: None,
|
||||
}),
|
||||
request: DebugRequest::Launch(LaunchRequest {
|
||||
program: "${ZED_WORKTREE_ROOT}/xyz.js".into(),
|
||||
args: vec!["--foo".into(), "${ZED_WORKTREE_ROOT}/thing".into()],
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}/${FOO}/sub".into()),
|
||||
}),
|
||||
}
|
||||
}])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use collections::HashMap;
|
||||
use serde::Deserialize;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{EnvVariableReplacer, TaskTemplate, TaskTemplates, VariableName};
|
||||
use crate::{TaskTemplate, TaskTemplates, VariableName};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -41,6 +41,48 @@ enum Command {
|
||||
},
|
||||
}
|
||||
|
||||
type VsCodeEnvVariable = String;
|
||||
type ZedEnvVariable = String;
|
||||
|
||||
struct EnvVariableReplacer {
|
||||
variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>,
|
||||
}
|
||||
|
||||
impl EnvVariableReplacer {
|
||||
fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
|
||||
Self { variables }
|
||||
}
|
||||
// Replaces occurrences of VsCode-specific environment variables with Zed equivalents.
|
||||
fn replace(&self, input: &str) -> String {
|
||||
shellexpand::env_with_context_no_errors(&input, |var: &str| {
|
||||
// Colons denote a default value in case the variable is not set. We want to preserve that default, as otherwise shellexpand will substitute it for us.
|
||||
let colon_position = var.find(':').unwrap_or(var.len());
|
||||
let (variable_name, default) = var.split_at(colon_position);
|
||||
let append_previous_default = |ret: &mut String| {
|
||||
if !default.is_empty() {
|
||||
ret.push_str(default);
|
||||
}
|
||||
};
|
||||
if let Some(substitution) = self.variables.get(variable_name) {
|
||||
// Got a VSCode->Zed hit, perform a substitution
|
||||
let mut name = format!("${{{substitution}");
|
||||
append_previous_default(&mut name);
|
||||
name.push('}');
|
||||
return Some(name);
|
||||
}
|
||||
// This is an unknown variable.
|
||||
// We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect.
|
||||
// If there's a default, we need to return the string verbatim as otherwise shellexpand will apply that default for us.
|
||||
if !default.is_empty() {
|
||||
return Some(format!("${{{var}}}"));
|
||||
}
|
||||
// Else we can just return None and that variable will be left as is.
|
||||
None
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl VsCodeTaskDefinition {
|
||||
fn into_zed_format(self, replacer: &EnvVariableReplacer) -> anyhow::Result<TaskTemplate> {
|
||||
if self.other_attributes.contains_key("dependsOn") {
|
||||
|
||||
@@ -15,7 +15,6 @@ pub struct EventRequestBody {
|
||||
pub session_id: Option<String>,
|
||||
pub metrics_id: Option<String>,
|
||||
/// True for Zed staff, otherwise false
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_staff: Option<bool>,
|
||||
/// Zed version number
|
||||
pub app_version: String,
|
||||
|
||||
@@ -604,7 +604,7 @@ fn main() {
|
||||
setting = "keymap",
|
||||
value = BaseKeymap::get_global(cx).to_string()
|
||||
);
|
||||
telemetry.flush_events().detach();
|
||||
telemetry.flush_events();
|
||||
|
||||
let fs = app_state.fs.clone();
|
||||
load_user_themes_in_background(fs.clone(), cx);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user