Compare commits

..

18 Commits

Author SHA1 Message Date
Thomas Mickley-Doyle
fcb71cda2a Update repo for eval data 2025-04-22 16:08:58 -05:00
Thomas Mickley-Doyle
5145a8dfca Add eval example for cli todo app 2025-04-22 11:58:26 -05:00
Max Brunsfeld
36d02de784 Rework eval to support interpretable scores and efficient repetitions (#29197)
### Problem

We want to start continuously tracking our progress on agent evals over
time. As part of this, we'd like the *score* to have a clear,
interpretable meaning. Right now, it's a number from 0 to 5, but it's
not clear what any particular number works. In addition, scores vary
widely from run to run, because the agent's output is deterministic. We
try to stabilize the score using a panel of judges, but the behavior of
the agent itself varies much more widely than the judges' scores for a
given run.

### Solution

* **explicit meanings of scores** - In this PR, we're prescribing the
diff and thread criteria files so that they *must* be unordered lists of
assertions. For both the thread and the diff, rather than providing an
abstract score, the judge's task is simply to count how many of these
assertions are satisfied. A percentage score can be derived from this
number, divided by the total number of assertions.
* **repetitions** - Rather than running each example once, and judging
it N times, we'll **run** the example N times. Right now, I'm just
judging the output once per run, because I believe that with these more
clear scoring criteria, the main source of non-determinism will be the
*agent's* behavior, not the judge's

### Questions

* **accounting for diagnostic errors** - Previously, the judge was asked
to incorporate diagnostics into their abstract scores. Now that the
"score" is determined directly from the criteria, the diagnostic will
not be captured in the score. How should the diagnostics be accounted
for in the eval? One thought is - let's simply count and report the
number of errors remaining after the agent finishes, as a separate field
of the run (along with diff score and thread score). We could consider
normalizing it using the total lines of added code (like errors per 100
lines of code added) in order to give it some semblance of stability
between examples.

* **repetitions** - How many repetitions should we run on CI? Each
repetition takes significant time, but I think running more than one
repetition will make the scores significantly less volatile.

### Todo

* [x] Fix `--concurrency` implementation so that only N tasks are
spawned
* [x] Support `--repetitions` efficiently (re-using the same worktree)
* [x] Restructure judge prompts to count passing criteria, not compute
abstract score
* [x] Report total number of diagnostics in some way
* [x] Format output nicely

Release Notes:

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

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-04-22 14:00:09 +00:00
Danilo Leal
36da97935a agent: Show project name in the Agent notification (#29211)
Release Notes:

- agent: Added the project name in the Agent Panel notification.
2025-04-22 10:41:03 -03:00
Danilo Leal
19b547565d agent: Refine the web search tool call UI (#29190)
This PR refines a bit the web search tool UI by introducing a component
(`ToolCallCardHeader`) that aims to standardize the heading element of
tool calls in the thread.

In terms of next steps, I plan to evolve this component further soon
(e.g., building a full-blown "tool call card" component), and even move
it to a place where I can re-use it in the active_thread as well without
making the `assistant_tools` a dependency of it.

Release Notes:

- N/A
2025-04-22 09:51:57 -03:00
Danilo Leal
109f1d43fc agent: Simplify user message design (#29165)
Mainly removing the "You" label, which didn't add a lot of value. Still
figuring out an issue with font size Markdown rendering before merging
this PR.

Release Notes:

- N/A
2025-04-22 09:51:50 -03:00
Bennet Bo Fenner
a5852d4537 agent: Support inserting selections as context via @selection (#29045)
WIP

Release Notes:

- N/A
2025-04-22 13:56:42 +02:00
Stephan Seidt
10ded0ab75 agent: Add support for google gemini 2.5 flash preview (#29205)
Adds support for the new gemini-2.5-flash-preview-04-17

Release Notes:

- agent: Added support for gemini-2.5-flash-preview
2025-04-22 09:37:12 +00:00
Bennet Bo Fenner
b0b620af56 gemini: Add support for passing images as part of the prompt (#29203)
Release Notes:

- agent: Add support for adding images as context when using Google
Gemini
2025-04-22 09:05:46 +00:00
Bennet Bo Fenner
eca6d5a04e agent: Support pasting images as context (#29177)
https://github.com/user-attachments/assets/d6a27b05-3590-4f40-a820-f6f99f6bd581

Release Notes:

- agent: Added support for pasting images as context

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-04-22 09:01:01 +00:00
Conrad Irwin
3357736aea Fix duplicated multi-buffer excerpts (#29193)
- **add test case**
- **Merge excerpts more aggressively**
- **Randomized test for set_excerpts_for_path**

Closes #ISSUE

Release Notes:

- Fixed duplicted excerpts (and resulting panics)

---------

Co-authored-by: João Marcos <marcospb19@hotmail.com>
2025-04-22 05:25:09 +00:00
Nathan Sobo
458ffaa134 Add new action to run agent eval (#29158)
The old one wasn't linking, and
https://github.com/zed-industries/zed/pull/29081 has a bunch of merge
conflicts. Wanted to start simple/small.

## Todo

* [x] Remove low-signal examples
* [x] Make the eval run on a cron, on main, and on any PR with the
`run-eval` label
* [x] Noise in logs about failure to write settings
    ```
[2025-04-21T20:45:04Z ERROR settings] Failed to write settings to file
"/home/runner/.config/zed/settings.json"
    
       Caused by:
No such file or directory (os error 2) at path
"/home/runner/.config/zed/.tmpLewFEs"
    ```
* [x] `Agentic loop stalled`
(https://github.com/zed-industries/zed/actions/runs/14581044243/job/40897622894)
* [x] Make sure that events are recorded in snowflake
* [ ] Change judge criteria to be more explicit about meanings of scores

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Thomas Mickley-Doyle <tmickleydoyle@gmail.com>
2025-04-21 21:30:21 -07:00
Agus Zubiaga
b14356d1d3 agent: Do not add <using_tool> placeholder (#29194)
Our provider code in `language_models` filters out messages for which
`LanguageModelRequestMessage::contents_empty` returns `false`. This
doesn't seem wrong by itself, but `contents_empty` was returning `false`
for messages whose first segment didn't contain non-whitespace text even
if they contained other non-empty segments. This caused requests to fail
when a message with a tool call didn't contain any preceding text.

Release Notes:

- N/A
2025-04-22 00:41:47 -03:00
Michael Sloan
19ef56ba7c agent: Fix file context renames affecting display + simplify loading code (#29192)
Release Notes:

- N/A
2025-04-22 03:16:46 +00:00
Conrad Irwin
dfbd132d9f Update Split bindings in terminal (#29188)
Closes #29087

Release Notes:

- Changed default bindings for splitting terminals from `ctrl-k
{up,down,left,right}` to `ctrl-alt-{up,down,left,right}`. `ctrl-k` is
used by Readline to cut to the end of the line.
2025-04-21 19:48:18 -06:00
Michael Sloan
2e8ee9b64f agent: Make directory context display update on rename (#29189)
Release Notes:

- N/A
2025-04-22 01:44:31 +00:00
Gaku Kanematsu
c15382c4d8 vim: Add cursor shape settings for each vim mode (#28636)
Closes #4495

Release Notes:

- vim: add cursor shape settings for each vim mode

---

Add cursor shape settings for each vim mode to enable users to specify
them.

Example of `settings.json`:

```json
{
  "vim_mode": true,
  "vim": {
    "cursor_shape": {
      "normal": "hollow",
      "insert": "bar",
      "replace": "block",
      "visual": "underline"
    }
  }
}
```

After this change is applied,

- The cursor shape specified by the user for each mode is used.
- In insert mode, the `vim > cursor_shape > insert` setting takes
precedence over the primary `cursor_shape` setting.
- If `vim > cursor_shape > insert` is not set, the primary
`cursor_shape` will be used in insert mode.
- The cursor shape will remain unchanged before and after this update
when the user does not set the `vim > cursor_shape` setting.

Video:


[screen-record.webm](https://github.com/user-attachments/assets/b87461a1-6b3a-4a77-a607-a340f106def5)
2025-04-21 18:42:04 -06:00
Michael Sloan
70c51b513b agent eval: Default to also running typescript examples (#29185)
Release Notes:

- N/A
2025-04-21 23:59:35 +00:00
127 changed files with 3193 additions and 2719 deletions

77
.github/workflows/eval.yml vendored Normal file
View File

@@ -0,0 +1,77 @@
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, Ive included the cleanup code here as a precaution.
# While its not strictly necessary at this moment, I believe its better to err on the side of caution.
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo

View File

@@ -1,28 +0,0 @@
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

4
Cargo.lock generated
View File

@@ -3042,7 +3042,6 @@ dependencies = [
"strum 0.27.1",
"subtle",
"supermaven_api",
"task",
"telemetry_events",
"text",
"theme",
@@ -4190,7 +4189,6 @@ dependencies = [
"command_palette_hooks",
"dap",
"db",
"debugger_tools",
"editor",
"env_logger 0.11.8",
"feature_flags",
@@ -4200,7 +4198,6 @@ dependencies = [
"language",
"log",
"menu",
"parking_lot",
"picker",
"pretty_assertions",
"project",
@@ -4915,6 +4912,7 @@ dependencies = [
"language_models",
"languages",
"node_runtime",
"parking_lot",
"paths",
"project",
"prompt_store",

1
assets/icons/image.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-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>

After

Width:  |  Height:  |  Size: 372 B

View File

@@ -1028,10 +1028,10 @@
// Using `ctrl-shift-space` in Zed requires disabling the macOS global shortcut.
// System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck)
"ctrl-shift-space": "terminal::ToggleViMode",
"ctrl-k up": "pane::SplitUp",
"ctrl-k down": "pane::SplitDown",
"ctrl-k left": "pane::SplitLeft",
"ctrl-k right": "pane::SplitRight"
"ctrl-alt-up": "pane::SplitUp",
"ctrl-alt-down": "pane::SplitDown",
"ctrl-alt-left": "pane::SplitLeft",
"ctrl-alt-right": "pane::SplitRight"
}
},
{

View File

@@ -1489,7 +1489,12 @@
"use_multiline_find": false,
"use_smartcase_find": false,
"highlight_on_yank_duration": 200,
"custom_digraphs": {}
"custom_digraphs": {},
// Cursor shape for the each mode.
// Specify the mode as the key and the shape as the value.
// The mode can be one of the following: "normal", "replace", "insert", "visual".
// The shape can be one of the following: "block", "bar", "underline", "hollow".
"cursor_shape": {}
},
// The server to connect to. If the environment variable
// ZED_SERVER_URL is set, it will override this setting.

View File

@@ -670,6 +670,26 @@ 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| {
@@ -1010,6 +1030,7 @@ impl ActiveThread {
}
}
ThreadEvent::CheckpointChanged => cx.notify(),
ThreadEvent::ReceivedTextChunk => {}
}
}
@@ -1072,9 +1093,21 @@ 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))
cx.new(|_| {
AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
})
})
.log_err()
{
@@ -1536,9 +1569,7 @@ 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)
@@ -1703,7 +1734,6 @@ impl ActiveThread {
} else {
div()
.min_h_6()
.text_ui(cx)
.child(self.render_message_content(
message_id,
rendered_message,
@@ -1762,35 +1792,18 @@ impl ActiveThread {
.pb_4()
.child(
v_flex()
.bg(colors.editor_background)
.bg(editor_bg_color)
.rounded_lg()
.border_1()
.border_color(colors.border)
.shadow_md()
.child(div().py_2().px_2p5().children(message_content))
.child(
h_flex()
.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),
),
)
.p_1()
.border_t_1()
.border_color(colors.border_variant)
.justify_end()
.child(
h_flex()
.gap_1()
@@ -1843,8 +1856,12 @@ impl ActiveThread {
edit_message_editor.is_none() && allow_editing_message,
|this| {
this.child(
Button::new("edit-message", "Edit")
Button::new("edit-message", "Edit Message")
.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();
@@ -1861,8 +1878,7 @@ impl ActiveThread {
},
),
),
)
.child(div().p_2().children(message_content)),
),
),
Role::Assistant => v_flex()
.id(("message-container", ix))
@@ -2101,11 +2117,13 @@ impl ActiveThread {
.map(|m| m.role)
.unwrap_or(Role::User);
let is_assistant = message_role == Role::Assistant;
let is_assistant_message = message_role == Role::Assistant;
let is_user_message = message_role == Role::User;
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 {
@@ -2126,10 +2144,28 @@ impl ActiveThread {
RenderedMessageSegment::Text(markdown) => {
let markdown_element = MarkdownElement::new(
markdown.clone(),
default_markdown_style(window, cx),
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)
},
);
let markdown_element = if is_assistant {
let markdown_element = if is_assistant_message {
markdown_element.code_block_renderer(
markdown::CodeBlockRenderer::Custom {
render: Arc::new({
@@ -3282,12 +3318,10 @@ pub(crate) fn open_context(
}
}
AssistantContext::Directory(directory_context) => {
let project_path = directory_context.project_path(cx);
let entry_id = directory_context.entry_id;
workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
if let Some(entry) = project.entry_for_path(&project_path, cx) {
cx.emit(project::Event::RevealInProjectPanel(entry.id));
}
workspace.project().update(cx, |_project, cx| {
cx.emit(project::Event::RevealInProjectPanel(entry_id));
})
})
}
@@ -3310,15 +3344,15 @@ pub(crate) fn open_context(
.detach();
}
}
AssistantContext::Excerpt(excerpt_context) => {
if let Some(project_path) = excerpt_context
AssistantContext::Selection(selection_context) => {
if let Some(project_path) = selection_context
.context_buffer
.buffer
.read(cx)
.project_path(cx)
{
let snapshot = excerpt_context.context_buffer.buffer.read(cx).snapshot();
let target_position = excerpt_context.range.start.to_point(&snapshot);
let snapshot = selection_context.context_buffer.buffer.read(cx).snapshot();
let target_position = selection_context.range.start.to_point(&snapshot);
open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
@@ -3345,6 +3379,7 @@ pub(crate) fn open_context(
}),
cx,
),
AssistantContext::Image(_) => {}
}
}

View File

@@ -1951,7 +1951,9 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
.collect::<Vec<_>>();
for (buffer, range) in selection_ranges {
store.add_excerpt(range, buffer, cx).detach_and_log_err(cx);
store
.add_selection(buffer, range, cx)
.detach_and_log_err(cx);
}
})
})

View File

@@ -1,9 +1,14 @@
use std::{ops::Range, path::Path, sync::Arc};
use std::{
ops::Range,
path::{Path, PathBuf},
sync::Arc,
};
use gpui::{App, Entity, SharedString};
use language::{Buffer, File};
use language_model::LanguageModelRequestMessage;
use project::{ProjectPath, Worktree};
use futures::{FutureExt, future::Shared};
use gpui::{App, Entity, SharedString, Task};
use language::Buffer;
use language_model::{LanguageModelImage, LanguageModelRequestMessage};
use project::{ProjectEntryId, ProjectPath, Worktree};
use prompt_store::UserPromptId;
use rope::Point;
use serde::{Deserialize, Serialize};
@@ -28,10 +33,11 @@ pub enum ContextKind {
File,
Directory,
Symbol,
Excerpt,
Selection,
FetchedUrl,
Thread,
Rules,
Image,
}
impl ContextKind {
@@ -40,10 +46,11 @@ impl ContextKind {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::Symbol => IconName::Code,
ContextKind::Excerpt => IconName::Code,
ContextKind::Selection => IconName::Context,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageBubbles,
ContextKind::Rules => RULES_ICON,
ContextKind::Image => IconName::Image,
}
}
}
@@ -55,8 +62,9 @@ pub enum AssistantContext {
Symbol(SymbolContext),
FetchedUrl(FetchedUrlContext),
Thread(ThreadContext),
Excerpt(ExcerptContext),
Selection(SelectionContext),
Rules(RulesContext),
Image(ImageContext),
}
impl AssistantContext {
@@ -67,8 +75,9 @@ impl AssistantContext {
Self::Symbol(symbol) => symbol.id,
Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id,
Self::Excerpt(excerpt) => excerpt.id,
Self::Selection(selection) => selection.id,
Self::Rules(rules) => rules.id,
Self::Image(image) => image.id,
}
}
}
@@ -83,17 +92,25 @@ pub struct FileContext {
pub struct DirectoryContext {
pub id: ContextId,
pub worktree: Entity<Worktree>,
pub path: Arc<Path>,
pub entry_id: ProjectEntryId,
pub last_path: Arc<Path>,
/// Buffers of the files within the directory.
pub context_buffers: Vec<ContextBuffer>,
}
impl DirectoryContext {
pub fn project_path(&self, cx: &App) -> ProjectPath {
ProjectPath {
worktree_id: self.worktree.read(cx).id(),
path: self.path.clone(),
}
pub fn entry<'a>(&self, cx: &'a App) -> Option<&'a project::Entry> {
self.worktree.read(cx).entry_for_id(self.entry_id)
}
pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
let worktree = self.worktree.read(cx);
worktree
.entry_for_id(self.entry_id)
.map(|entry| ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
})
}
}
@@ -128,17 +145,51 @@ 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 thread even if the thread is deleted. Should probably be
// 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 file: Arc<dyn File>,
pub last_full_path: Arc<Path>,
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")
@@ -169,7 +220,7 @@ pub struct ContextSymbolId {
}
#[derive(Debug, Clone)]
pub struct ExcerptContext {
pub struct SelectionContext {
pub id: ContextId,
pub range: Range<Anchor>,
pub line_range: Range<Point>,
@@ -192,7 +243,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 excerpt_context = Vec::new();
let mut selection_context = Vec::new();
let mut fetch_context = Vec::new();
let mut thread_context = Vec::new();
let mut rules_context = Vec::new();
@@ -202,17 +253,18 @@ 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::Excerpt(context) => excerpt_context.push(context),
AssistantContext::Selection(context) => selection_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()
&& excerpt_context.is_empty()
&& selection_context.is_empty()
&& fetch_context.is_empty()
&& thread_context.is_empty()
&& rules_context.is_empty()
@@ -251,13 +303,13 @@ pub fn format_context_as_string<'a>(
result.push_str("</symbols>\n");
}
if !excerpt_context.is_empty() {
result.push_str("<excerpts>\n");
for context in excerpt_context {
if !selection_context.is_empty() {
result.push_str("<selections>\n");
for context in selection_context {
result.push_str(&context.context_buffer.text);
result.push('\n');
}
result.push_str("</excerpts>\n");
result.push_str("</selections>\n");
}
if !fetch_context.is_empty() {

View File

@@ -17,6 +17,7 @@ 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;
@@ -40,6 +41,35 @@ 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,
@@ -49,6 +79,31 @@ 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;
@@ -65,7 +120,7 @@ impl TryFrom<&str> for ContextPickerMode {
}
impl ContextPickerMode {
pub fn mention_prefix(&self) -> &'static str {
pub fn keyword(&self) -> &'static str {
match self {
Self::File => "file",
Self::Symbol => "symbol",
@@ -167,7 +222,13 @@ impl ContextPicker {
.enumerate()
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
let modes = supported_context_picker_modes(&self.thread_store);
let entries = self
.workspace
.upgrade()
.map(|workspace| {
available_context_picker_entries(&self.thread_store, &workspace, cx)
})
.unwrap_or_default();
menu.when(has_recent, |menu| {
menu.custom_row(|_, _| {
@@ -183,15 +244,15 @@ impl ContextPicker {
})
.extend(recent_entries)
.when(has_recent, |menu| menu.separator())
.extend(modes.into_iter().map(|mode| {
.extend(entries.into_iter().map(|entry| {
let context_picker = context_picker.clone();
ContextMenuEntry::new(mode.label())
.icon(mode.icon())
ContextMenuEntry::new(entry.label())
.icon(entry.icon())
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.handler(move |window, cx| {
context_picker.update(cx, |this, cx| this.select_mode(mode, window, cx))
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
})
}))
.keep_open_on_confirm()
@@ -210,74 +271,87 @@ impl ContextPicker {
self.thread_store.is_some()
}
fn select_mode(
fn select_entry(
&mut self,
mode: ContextPickerMode,
entry: ContextPickerEntry,
window: &mut Window,
cx: &mut Context<Self>,
) {
let context_picker = cx.entity().downgrade();
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(),
match entry {
ContextPickerEntry::Mode(mode) => 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::Rules => {
if let Some(thread_store) = self.thread_store.as_ref() {
self.mode = ContextPickerState::Rules(cx.new(|cx| {
RulesContextPicker::new(
thread_store.clone(),
ContextPickerMode::Symbol => {
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
SymbolContextPicker::new(
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();
@@ -451,19 +525,37 @@ enum RecentEntry {
Thread(ThreadContextEntry),
}
fn supported_context_picker_modes(
fn available_context_picker_entries(
thread_store: &Option<WeakEntity<ThreadStore>>,
) -> Vec<ContextPickerMode> {
let mut modes = vec![
ContextPickerMode::File,
ContextPickerMode::Symbol,
ContextPickerMode::Fetch,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<ContextPickerEntry> {
let mut entries = vec![
ContextPickerEntry::Mode(ContextPickerMode::File),
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
];
if thread_store.is_some() {
modes.push(ContextPickerMode::Thread);
modes.push(ContextPickerMode::Rules);
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,
));
}
modes
if thread_store.is_some() {
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
}
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
entries
}
fn recent_context_picker_entries(
@@ -522,6 +614,54 @@ 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,
@@ -541,24 +681,11 @@ 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 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(
let crease = crease_for_mention(
crease_label,
crease_icon_path,
start..end,
placeholder.clone(),
fold_toggle("mention"),
render_trailer,
editor_entity.downgrade(),
);
editor.display_map.update(cx, |display_map, cx| {
@@ -567,6 +694,29 @@ 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,
@@ -655,6 +805,7 @@ fn fold_toggle(
pub enum MentionLink {
File(ProjectPath, Entry),
Symbol(ProjectPath, String),
Selection(ProjectPath, Range<usize>),
Fetch(String),
Thread(ThreadId),
Rules(UserPromptId),
@@ -663,6 +814,7 @@ 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";
@@ -672,8 +824,9 @@ impl MentionLink {
pub fn is_valid(url: &str) -> bool {
url.starts_with(Self::FILE)
|| url.starts_with(Self::SYMBOL)
|| url.starts_with(Self::THREAD)
|| url.starts_with(Self::FETCH)
|| url.starts_with(Self::SELECTION)
|| url.starts_with(Self::THREAD)
|| url.starts_with(Self::RULES)
}
@@ -691,6 +844,19 @@ 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)
}
@@ -739,6 +905,20 @@ 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))

View File

@@ -1,22 +1,23 @@
use std::cell::RefCell;
use std::ops::Range;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId};
use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
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, ToPoint};
use text::{Anchor, OffsetRangeExt, ToPoint};
use ui::prelude::*;
use workspace::Workspace;
@@ -32,8 +33,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::{
ContextPickerMode, MentionLink, RecentEntry, recent_context_picker_entries,
supported_context_picker_modes,
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
available_context_picker_entries, recent_context_picker_entries, selection_ranges,
};
pub(crate) enum Match {
@@ -42,19 +43,19 @@ pub(crate) enum Match {
Thread(ThreadMatch),
Fetch(SharedString),
Rules(RulesContextEntry),
Mode(ModeMatch),
Entry(EntryMatch),
}
pub struct ModeMatch {
pub struct EntryMatch {
mat: Option<StringMatch>,
mode: ContextPickerMode,
entry: ContextPickerEntry,
}
impl Match {
pub fn score(&self) -> f64 {
match self {
Match::File(file) => file.mat.score,
Match::Mode(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
Match::Thread(_) => 1.,
Match::Symbol(_) => 1.,
Match::Fetch(_) => 1.,
@@ -162,9 +163,14 @@ fn search(
.collect::<Vec<_>>();
matches.extend(
supported_context_picker_modes(&thread_store)
available_context_picker_entries(&thread_store, &workspace, cx)
.into_iter()
.map(|mode| Match::Mode(ModeMatch { mode, mat: None })),
.map(|mode| {
Match::Entry(EntryMatch {
entry: mode,
mat: None,
})
}),
);
Task::ready(matches)
@@ -174,11 +180,11 @@ fn search(
let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
let modes = supported_context_picker_modes(&thread_store);
let mode_candidates = modes
let entries = available_context_picker_entries(&thread_store, &workspace, cx);
let entry_candidates = entries
.iter()
.enumerate()
.map(|(ix, mode)| StringMatchCandidate::new(ix, mode.mention_prefix()))
.map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
.collect::<Vec<_>>();
cx.background_spawn(async move {
@@ -188,8 +194,8 @@ fn search(
.map(Match::File)
.collect::<Vec<_>>();
let mode_matches = fuzzy::match_strings(
&mode_candidates,
let entry_matches = fuzzy::match_strings(
&entry_candidates,
&query,
false,
100,
@@ -198,9 +204,9 @@ fn search(
)
.await;
matches.extend(mode_matches.into_iter().map(|mat| {
Match::Mode(ModeMatch {
mode: modes[mat.candidate_id],
matches.extend(entry_matches.into_iter().map(|mat| {
Match::Entry(EntryMatch {
entry: entries[mat.candidate_id],
mat: Some(mat),
})
}));
@@ -240,19 +246,137 @@ impl ContextPickerCompletionProvider {
}
}
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)),
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),
})
}
}
}
@@ -686,9 +810,15 @@ impl CompletionProvider for ContextPickerCompletionProvider {
context_store.clone(),
http_client.clone(),
)),
Match::Mode(ModeMatch { mode, .. }) => {
Some(Self::completion_for_mode(source_range.clone(), mode))
}
Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
entry,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
&workspace,
cx,
),
})
.collect()
})?))

View File

@@ -6,9 +6,10 @@ 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, SharedString, Task, WeakEntity};
use language::{Buffer, File};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use gpui::{App, AppContext as _, Context, Entity, Image, SharedString, Task, WeakEntity};
use language::Buffer;
use language_model::LanguageModelImage;
use project::{Project, ProjectEntryId, ProjectItem, ProjectPath, Worktree};
use prompt_store::UserPromptId;
use rope::{Point, Rope};
use text::{Anchor, BufferId, OffsetRangeExt};
@@ -17,7 +18,8 @@ use util::{ResultExt as _, maybe};
use crate::ThreadStore;
use crate::context::{
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
ExcerptContext, FetchedUrlContext, FileContext, RulesContext, SymbolContext, ThreadContext,
FetchedUrlContext, FileContext, ImageContext, RulesContext, SelectionContext, SymbolContext,
ThreadContext,
};
use crate::context_strip::SuggestedContext;
use crate::thread::{Thread, ThreadId};
@@ -112,13 +114,12 @@ impl ContextStore {
return anyhow::Ok(());
}
let (buffer_info, text_task) =
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
let text = text_task.await;
let context_buffer = this
.update(cx, |_, cx| load_context_buffer(buffer, cx))??
.await;
this.update(cx, |this, cx| {
this.insert_file(make_context_buffer(buffer_info, text), cx);
this.insert_file(context_buffer, cx);
})?;
anyhow::Ok(())
@@ -131,14 +132,11 @@ impl ContextStore {
cx: &mut Context<Self>,
) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
let (buffer_info, text_task) =
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
let context_buffer = this
.update(cx, |_, cx| load_context_buffer(buffer, cx))??
.await;
let text = text_task.await;
this.update(cx, |this, cx| {
this.insert_file(make_context_buffer(buffer_info, text), cx)
})?;
this.update(cx, |this, cx| this.insert_file(context_buffer, cx))?;
anyhow::Ok(())
})
@@ -162,6 +160,14 @@ impl ContextStore {
return Task::ready(Err(anyhow!("failed to read project")));
};
let Some(entry_id) = project
.read(cx)
.entry_for_path(&project_path, cx)
.map(|entry| entry.id)
else {
return Task::ready(Err(anyhow!("no entry found for directory context")));
};
let already_included = match self.includes_directory(&project_path) {
Some(FileInclusion::Direct(context_id)) => {
if remove_if_exists {
@@ -203,27 +209,15 @@ impl ContextStore {
let buffers = open_buffers_task.await;
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_buffer_tasks = this.update(cx, |_, cx| {
buffers
.into_iter()
.flatten()
.flat_map(move |buffer| load_context_buffer(buffer, cx).log_err())
.collect::<Vec<_>>()
})?;
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<_>>();
let context_buffers = future::join_all(context_buffer_tasks).await;
if context_buffers.is_empty() {
let full_path = cx.update(|cx| worktree.read(cx).full_path(&project_path.path))?;
@@ -231,7 +225,7 @@ impl ContextStore {
}
this.update(cx, |this, cx| {
this.insert_directory(worktree, project_path, context_buffers, cx);
this.insert_directory(worktree, entry_id, project_path, context_buffers, cx);
})?;
anyhow::Ok(())
@@ -241,19 +235,21 @@ impl ContextStore {
fn insert_directory(
&mut self,
worktree: Entity<Worktree>,
entry_id: ProjectEntryId,
project_path: ProjectPath,
context_buffers: Vec<ContextBuffer>,
cx: &mut Context<Self>,
) {
let id = self.next_context_id.post_inc();
let path = project_path.path.clone();
let last_path = project_path.path.clone();
self.directories.insert(project_path, id);
self.context
.push(AssistantContext::Directory(DirectoryContext {
id,
worktree,
path,
entry_id,
last_path,
context_buffers,
}));
cx.notify();
@@ -293,27 +289,23 @@ impl ContextStore {
}
}
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)),
};
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)),
};
cx.spawn(async move |this, cx| {
let content = collect_content_task.await;
let context_buffer = context_buffer_task.await;
this.update(cx, |this, cx| {
this.insert_symbol(
make_context_symbol(
buffer_info,
context_buffer,
project_path,
symbol_name,
symbol_range,
symbol_enclosing_range,
content,
),
cx,
)
@@ -458,33 +450,54 @@ impl ContextStore {
cx.notify();
}
pub fn add_excerpt(
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(
&mut self,
range: Range<Anchor>,
buffer: Entity<Buffer>,
range: Range<Anchor>,
cx: &mut Context<ContextStore>,
) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
let (line_range, buffer_info, text_task) = this.update(cx, |_, cx| {
collect_buffer_info_and_text_for_range(buffer, range.clone(), cx)
let (line_range, context_buffer_task) = this.update(cx, |_, cx| {
load_context_buffer_range(buffer, range.clone(), cx)
})??;
let text = text_task.await;
let context_buffer = context_buffer_task.await;
this.update(cx, |this, cx| {
this.insert_excerpt(
make_context_buffer(buffer_info, text),
range,
line_range,
cx,
)
this.insert_selection(context_buffer, range, line_range, cx)
})?;
anyhow::Ok(())
})
}
fn insert_excerpt(
fn insert_selection(
&mut self,
context_buffer: ContextBuffer,
range: Range<Anchor>,
@@ -492,12 +505,13 @@ impl ContextStore {
cx: &mut Context<Self>,
) {
let id = self.next_context_id.post_inc();
self.context.push(AssistantContext::Excerpt(ExcerptContext {
id,
range,
line_range,
context_buffer,
}));
self.context
.push(AssistantContext::Selection(SelectionContext {
id,
range,
line_range,
context_buffer,
}));
cx.notify();
}
@@ -550,7 +564,7 @@ impl ContextStore {
self.symbol_buffers.remove(&symbol.context_symbol.id);
self.symbols.retain(|_, context_id| *context_id != id);
}
AssistantContext::Excerpt(_) => {}
AssistantContext::Selection(_) => {}
AssistantContext::FetchedUrl(_) => {
self.fetched_urls.retain(|_, context_id| *context_id != id);
}
@@ -560,6 +574,7 @@ impl ContextStore {
AssistantContext::Rules(RulesContext { prompt_id, .. }) => {
self.user_rules.remove(&prompt_id);
}
AssistantContext::Image(_) => {}
}
cx.notify();
@@ -685,10 +700,11 @@ impl ContextStore {
}
AssistantContext::Directory(_)
| AssistantContext::Symbol(_)
| AssistantContext::Excerpt(_)
| AssistantContext::Selection(_)
| AssistantContext::FetchedUrl(_)
| AssistantContext::Thread(_)
| AssistantContext::Rules(_) => None,
| AssistantContext::Rules(_)
| AssistantContext::Image(_) => None,
})
.collect()
}
@@ -703,92 +719,78 @@ 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(
info: BufferInfo,
context_buffer: ContextBuffer,
path: ProjectPath,
name: SharedString,
range: Range<Anchor>,
enclosing_range: Range<Anchor>,
text: SharedString,
) -> ContextSymbol {
ContextSymbol {
id: ContextSymbolId { name, range, path },
buffer_version: info.version,
buffer_version: context_buffer.version,
enclosing_range,
buffer: info.buffer,
text,
buffer: context_buffer.buffer,
text: context_buffer.text,
}
}
fn collect_buffer_info_and_text_for_range(
fn load_context_buffer_range(
buffer: Entity<Buffer>,
range: Range<Anchor>,
cx: &App,
) -> Result<(Range<Point>, BufferInfo, Task<SharedString>)> {
let content = buffer
.read(cx)
.text_for_range(range.clone())
.collect::<Rope>();
let line_range = range.to_point(&buffer.read(cx).snapshot());
let buffer_info = collect_buffer_info(buffer, cx)?;
let full_path = buffer_info.file.full_path(cx);
let text_task = cx.background_spawn({
let line_range = line_range.clone();
async move { to_fenced_codeblock(&full_path, content, Some(line_range)) }
});
Ok((line_range, buffer_info, text_task))
}
fn collect_buffer_info_and_text(
buffer: Entity<Buffer>,
cx: &App,
) -> Result<(BufferInfo, Task<SharedString>)> {
let content = buffer.read(cx).as_rope().clone();
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> {
) -> Result<(Range<Point>, Task<ContextBuffer>)> {
let buffer_ref = buffer.read(cx);
let file = buffer_ref.file().context("file context must have a path")?;
let id = buffer_ref.remote_id();
let file = buffer_ref.file().context("context buffer missing path")?;
let full_path = file.full_path(cx);
// 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());
Ok(BufferInfo {
buffer,
id: buffer_ref.remote_id(),
file: file.clone(),
version,
})
// Build the text on a background thread.
let 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,
}
}
});
Ok((line_range, 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();
let file = buffer_ref.file().context("context buffer missing path")?;
let full_path = file.full_path(cx);
// 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,
}
}))
}
fn to_fenced_codeblock(
@@ -875,6 +877,7 @@ pub fn refresh_context_store_text(
let task = maybe!({
match context {
AssistantContext::File(file_context) => {
// TODO: Should refresh if the path has changed, as it's in the text.
if changed_buffers.is_empty()
|| changed_buffers.contains(&file_context.context_buffer.buffer)
{
@@ -883,8 +886,9 @@ pub fn refresh_context_store_text(
}
}
AssistantContext::Directory(directory_context) => {
let directory_path = directory_context.project_path(cx);
let should_refresh = changed_buffers.is_empty()
let directory_path = directory_context.project_path(cx)?;
let should_refresh = directory_path.path != directory_context.last_path
|| changed_buffers.is_empty()
|| changed_buffers.iter().any(|buffer| {
let Some(buffer_path) = buffer.read(cx).project_path(cx) else {
return false;
@@ -894,10 +898,16 @@ pub fn refresh_context_store_text(
if should_refresh {
let context_store = context_store.clone();
return refresh_directory_text(context_store, directory_context, cx);
return refresh_directory_text(
context_store,
directory_context,
directory_path,
cx,
);
}
}
AssistantContext::Symbol(symbol_context) => {
// TODO: Should refresh if the path has changed, as it's in the text.
if changed_buffers.is_empty()
|| changed_buffers.contains(&symbol_context.context_symbol.buffer)
{
@@ -905,12 +915,13 @@ pub fn refresh_context_store_text(
return refresh_symbol_text(context_store, symbol_context, cx);
}
}
AssistantContext::Excerpt(excerpt_context) => {
AssistantContext::Selection(selection_context) => {
// TODO: Should refresh if the path has changed, as it's in the text.
if changed_buffers.is_empty()
|| changed_buffers.contains(&excerpt_context.context_buffer.buffer)
|| changed_buffers.contains(&selection_context.context_buffer.buffer)
{
let context_store = context_store.clone();
return refresh_excerpt_text(context_store, excerpt_context, cx);
return refresh_selection_text(context_store, selection_context, cx);
}
}
AssistantContext::Thread(thread_context) => {
@@ -927,6 +938,7 @@ 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
@@ -965,6 +977,7 @@ fn refresh_file_text(
fn refresh_directory_text(
context_store: Entity<ContextStore>,
directory_context: &DirectoryContext,
directory_path: ProjectPath,
cx: &App,
) -> Option<Task<()>> {
let mut stale = false;
@@ -989,7 +1002,8 @@ fn refresh_directory_text(
let id = directory_context.id;
let worktree = directory_context.worktree.clone();
let path = directory_context.path.clone();
let entry_id = directory_context.entry_id;
let last_path = directory_path.path;
Some(cx.spawn(async move |cx| {
let context_buffers = context_buffers.await;
context_store
@@ -997,7 +1011,8 @@ fn refresh_directory_text(
let new_directory_context = DirectoryContext {
id,
worktree,
path,
entry_id,
last_path,
context_buffers,
};
context_store.replace_context(AssistantContext::Directory(new_directory_context));
@@ -1028,26 +1043,27 @@ fn refresh_symbol_text(
}
}
fn refresh_excerpt_text(
fn refresh_selection_text(
context_store: Entity<ContextStore>,
excerpt_context: &ExcerptContext,
selection_context: &SelectionContext,
cx: &App,
) -> Option<Task<()>> {
let id = excerpt_context.id;
let range = excerpt_context.range.clone();
let task = refresh_context_excerpt(&excerpt_context.context_buffer, range.clone(), cx);
let id = selection_context.id;
let range = selection_context.range.clone();
let task = refresh_context_excerpt(&selection_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_excerpt_context = ExcerptContext {
let new_selection_context = SelectionContext {
id,
range,
line_range,
context_buffer,
};
context_store.replace_context(AssistantContext::Excerpt(new_excerpt_context));
context_store
.replace_context(AssistantContext::Selection(new_selection_context));
})
.ok();
}))
@@ -1116,15 +1132,10 @@ fn refresh_user_rules(
})
}
fn refresh_context_buffer(
context_buffer: &ContextBuffer,
cx: &App,
) -> Option<impl Future<Output = ContextBuffer> + use<>> {
fn refresh_context_buffer(context_buffer: &ContextBuffer, cx: &App) -> Option<Task<ContextBuffer>> {
let buffer = context_buffer.buffer.read(cx);
if buffer.version.changed_since(&context_buffer.version) {
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)))
load_context_buffer(context_buffer.buffer.clone(), cx).log_err()
} else {
None
}
@@ -1137,10 +1148,9 @@ 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, 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))))
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)))
} else {
None
}
@@ -1153,7 +1163,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 (_, buffer_info, text_task) = collect_buffer_info_and_text_for_range(
let (_line_range, context_buffer_task) = load_context_buffer_range(
context_symbol.buffer.clone(),
context_symbol.enclosing_range.clone(),
cx,
@@ -1162,15 +1172,8 @@ 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(text_task.map(move |text| {
make_context_symbol(
buffer_info,
project_path,
name,
range,
enclosing_range,
text,
)
Some(context_buffer_task.map(move |context_buffer| {
make_context_symbol(context_buffer, project_path, name, range, enclosing_range)
}))
} else {
None

View File

@@ -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;
use editor::actions::{MoveUp, Paste};
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, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
Animation, AnimationExt, App, ClipboardEntry, 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,6 +271,7 @@ 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();
@@ -280,6 +281,7 @@ 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| {
@@ -293,7 +295,12 @@ impl MessageEditor {
let excerpt_ids = context_store
.context()
.iter()
.filter(|ctx| matches!(ctx, AssistantContext::Excerpt(_)))
.filter(|ctx| {
matches!(
ctx,
AssistantContext::Selection(_) | AssistantContext::Image(_)
)
})
.map(|ctx| ctx.id())
.collect::<Vec<_>>();
@@ -370,6 +377,34 @@ 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();
}
@@ -445,6 +480,7 @@ 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)

View File

@@ -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,
LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
LanguageModelImage, 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, USING_TOOL_MARKER};
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState};
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,
@@ -97,6 +97,7 @@ pub struct Message {
pub role: Role,
pub segments: Vec<MessageSegment>,
pub context: String,
pub images: Vec<LanguageModelImage>,
}
impl Message {
@@ -167,12 +168,9 @@ 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() || text.trim() == USING_TOOL_MARKER,
Self::Thinking { text, .. } => text.is_empty() || text.trim() == USING_TOOL_MARKER,
Self::Text(text) => text.is_empty(),
Self::Thinking { text, .. } => text.is_empty(),
Self::RedactedThinking(_) => false,
}
}
@@ -418,6 +416,7 @@ impl Thread {
})
.collect(),
context: message.context,
images: Vec::new(),
})
.collect(),
next_message_id,
@@ -750,6 +749,19 @@ 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 {
@@ -768,15 +780,16 @@ impl Thread {
cx,
);
}
AssistantContext::Excerpt(excerpt_context) => {
AssistantContext::Selection(selection_context) => {
log.buffer_added_as_context(
excerpt_context.context_buffer.buffer.clone(),
selection_context.context_buffer.buffer.clone(),
cx,
);
}
AssistantContext::FetchedUrl(_)
| AssistantContext::Thread(_)
| AssistantContext::Rules(_) => {}
| AssistantContext::Rules(_)
| AssistantContext::Image(_) => {}
}
}
});
@@ -817,6 +830,7 @@ impl Thread {
role,
segments,
context: String::new(),
images: Vec::new(),
});
self.touch_updated_at();
cx.emit(ThreadEvent::MessageAdded(id));
@@ -1040,6 +1054,21 @@ 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) => {
@@ -1234,6 +1263,7 @@ 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);
@@ -1783,7 +1813,7 @@ impl Thread {
thread_data,
final_project_snapshot
);
client.telemetry().flush_events();
client.telemetry().flush_events().await;
Ok(())
})
@@ -1828,7 +1858,7 @@ impl Thread {
thread_data,
final_project_snapshot
);
client.telemetry().flush_events();
client.telemetry().flush_events().await;
Ok(())
})
@@ -2084,7 +2114,7 @@ impl Thread {
github_login = github_login
);
client.telemetry().flush_events();
client.telemetry().flush_events().await;
}
}
})
@@ -2202,6 +2232,7 @@ pub enum ThreadEvent {
ShowError(ThreadError),
UsageUpdated(RequestUsage),
StreamedCompletion,
ReceivedTextChunk,
StreamedAssistantText(MessageId, String),
StreamedAssistantThinking(MessageId, String),
StreamedToolUse {

View File

@@ -27,8 +27,6 @@ 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>>,
@@ -452,28 +450,8 @@ impl ToolUseState {
request_message: &mut LanguageModelRequestMessage,
) {
if let Some(tool_uses) = self.tool_uses_by_assistant_message.get(&message_id) {
let mut found_tool_use = false;
for tool_use in tool_uses {
if self.tool_results.contains_key(&tool_use.id) {
if !found_tool_use {
// The API fails if a message contains a tool use without any (non-whitespace) text around it
match request_message.content.last_mut() {
Some(MessageContent::Text(txt)) => {
if txt.is_empty() {
txt.push_str(USING_TOOL_MARKER);
}
}
None | Some(_) => {
request_message
.content
.push(MessageContent::Text(USING_TOOL_MARKER.into()));
}
};
}
found_tool_use = true;
// Do not send tool uses until they are completed
request_message
.content

View File

@@ -12,6 +12,7 @@ pub struct AgentNotification {
title: SharedString,
caption: SharedString,
icon: IconName,
project_name: Option<SharedString>,
}
impl AgentNotification {
@@ -19,11 +20,13 @@ 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()),
}
}
@@ -130,11 +133,34 @@ impl Render for AgentNotification {
.child(gradient_overflow()),
)
.child(
div()
h_flex()
.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()),
),

View File

@@ -1,11 +1,14 @@
use std::sync::Arc;
use std::{rc::Rc, time::Duration};
use file_icons::FileIcons;
use gpui::ClickEvent;
use gpui::{Animation, AnimationExt as _, pulsating_between};
use ui::{IconButtonShape, Tooltip, prelude::*};
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 crate::context::{AssistantContext, ContextId, ContextKind};
use crate::context::{AssistantContext, ContextId, ContextKind, ImageContext};
#[derive(IntoElement)]
pub enum ContextPill {
@@ -120,74 +123,100 @@ impl RenderOnce for ContextPill {
on_remove,
focused,
on_click,
} => 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)
} => {
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(),
}),
)
})
.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 {
.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();
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()
}
}),
.cursor_pointer()
.on_click(move |event, window, cx| on_click(event, window, cx))
})
.into_any_element()
}
ContextPill::Suggested {
name,
icon_path: _,
@@ -198,15 +227,15 @@ impl RenderOnce for ContextPill {
.cursor_pointer()
.pr_1()
.border_dashed()
.border_color(if *focused {
color.border_focused
} else {
color.border
.map(|pill| {
if *focused {
pill.border_color(color.border_focused)
.bg(color.element_background.opacity(0.5))
} else {
pill.border_color(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())
@@ -227,6 +256,13 @@ 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,
@@ -234,14 +270,15 @@ pub struct AddedContext {
pub parent: Option<SharedString>,
pub tooltip: Option<SharedString>,
pub icon_path: Option<SharedString>,
pub summarizing: bool,
pub status: ContextStatus,
pub render_preview: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
}
impl AddedContext {
pub fn new(context: &AssistantContext, cx: &App) -> AddedContext {
match context {
AssistantContext::File(file_context) => {
let full_path = file_context.context_buffer.file.full_path(cx);
let full_path = file_context.context_buffer.full_path(cx);
let full_path_string: SharedString =
full_path.to_string_lossy().into_owned().into();
let name = full_path
@@ -259,15 +296,20 @@ impl AddedContext {
parent,
tooltip: Some(full_path_string),
icon_path: FileIcons::get_icon(&full_path, cx),
summarizing: false,
status: ContextStatus::Ready,
render_preview: None,
}
}
AssistantContext::Directory(directory_context) => {
let full_path = directory_context
.worktree
.read(cx)
.full_path(&directory_context.path);
let worktree = directory_context.worktree.read(cx);
// If the directory no longer exists, use its last known path.
let full_path = worktree
.entry_for_id(directory_context.entry_id)
.map_or_else(
|| directory_context.last_path.clone(),
|entry| worktree.full_path(&entry.path).into(),
);
let full_path_string: SharedString =
full_path.to_string_lossy().into_owned().into();
let name = full_path
@@ -285,7 +327,8 @@ impl AddedContext {
parent,
tooltip: Some(full_path_string),
icon_path: None,
summarizing: false,
status: ContextStatus::Ready,
render_preview: None,
}
}
@@ -296,11 +339,12 @@ impl AddedContext {
parent: None,
tooltip: None,
icon_path: None,
summarizing: false,
status: ContextStatus::Ready,
render_preview: None,
},
AssistantContext::Excerpt(excerpt_context) => {
let full_path = excerpt_context.context_buffer.file.full_path(cx);
AssistantContext::Selection(selection_context) => {
let full_path = selection_context.context_buffer.full_path(cx);
let mut full_path_string = full_path.to_string_lossy().into_owned();
let mut name = full_path
.file_name()
@@ -309,8 +353,8 @@ impl AddedContext {
let line_range_text = format!(
" ({}-{})",
excerpt_context.line_range.start.row + 1,
excerpt_context.line_range.end.row + 1
selection_context.line_range.start.row + 1,
selection_context.line_range.end.row + 1
);
full_path_string.push_str(&line_range_text);
@@ -322,13 +366,25 @@ impl AddedContext {
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
id: excerpt_context.id,
kind: ContextKind::File, // Use File icon for excerpts
id: selection_context.id,
kind: ContextKind::Selection,
name: name.into(),
parent,
tooltip: Some(full_path_string.into()),
tooltip: None,
icon_path: FileIcons::get_icon(&full_path, cx),
summarizing: false,
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()
}
})),
}
}
@@ -339,7 +395,8 @@ impl AddedContext {
parent: None,
tooltip: None,
icon_path: None,
summarizing: false,
status: ContextStatus::Ready,
render_preview: None,
},
AssistantContext::Thread(thread_context) => AddedContext {
@@ -349,10 +406,18 @@ impl AddedContext {
parent: None,
tooltip: None,
icon_path: None,
summarizing: thread_context
status: if thread_context
.thread
.read(cx)
.is_generating_detailed_summary(),
.is_generating_detailed_summary()
{
ContextStatus::Loading {
message: "Summarizing…".into(),
}
} else {
ContextStatus::Ready
},
render_preview: None,
},
AssistantContext::Rules(user_rules_context) => AddedContext {
@@ -362,8 +427,122 @@ impl AddedContext {
parent: None,
tooltip: None,
icon_path: None,
summarizing: false,
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()
}
})),
},
}
}
}
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(),
)
}
}

View File

@@ -2089,7 +2089,7 @@ impl ContextEditor {
continue;
};
let image_id = image.id();
let image_task = LanguageModelImage::from_image(image, cx).shared();
let image_task = LanguageModelImage::from_image(Arc::new(image), cx).shared();
for image_position in image_positions.iter() {
context.insert_content(

View File

@@ -22,6 +22,7 @@ mod schema;
mod symbol_info_tool;
mod terminal_tool;
mod thinking_tool;
mod ui;
mod web_search_tool;
use std::sync::Arc;

View File

@@ -0,0 +1,3 @@
mod tool_call_card_header;
pub use tool_call_card_header::*;

View File

@@ -0,0 +1,102 @@
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
}
},
),
)
}
}

View File

@@ -1,13 +1,11 @@
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::{FutureExt, TryFutureExt};
use gpui::{
Animation, AnimationExt, App, AppContext, Context, Entity, IntoElement, Task, Window,
pulsating_between,
};
use futures::{Future, FutureExt, TryFutureExt};
use gpui::{App, AppContext, Context, Entity, IntoElement, Task, Window};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -47,7 +45,7 @@ impl Tool for WebSearchTool {
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
"Web Search".to_string()
"Searching the Web".to_string()
}
fn run(
@@ -115,61 +113,30 @@ impl ToolCard for WebSearchToolCard {
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
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 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 content =
self.response.as_ref().and_then(|response| match response {
Ok(response) => {
Some(
v_flex()
.overflow_hidden()
.ml_1p5()
.pl_1p5()
.pl(px(5.))
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.gap_1()
@@ -209,7 +176,7 @@ impl ToolCard for WebSearchToolCard {
Err(_) => None,
});
v_flex().my_2().gap_1().child(header).children(content)
v_flex().mb_3().gap_1().child(header).children(content)
}
}

View File

@@ -4,7 +4,7 @@ use crate::TelemetrySettings;
use anyhow::Result;
use clock::SystemClock;
use futures::channel::mpsc;
use futures::{Future, StreamExt};
use futures::{Future, FutureExt, StreamExt};
use gpui::{App, AppContext as _, BackgroundExecutor, Task};
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
use parking_lot::Mutex;
@@ -290,6 +290,10 @@ 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>,
@@ -430,7 +434,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();
this.flush_events().detach();
}));
}
@@ -456,7 +460,7 @@ impl Telemetry {
if state.installation_id.is_some() && state.events_queue.len() >= state.max_queue_size {
drop(state);
self.flush_events();
self.flush_events().detach();
}
}
@@ -499,60 +503,59 @@ impl Telemetry {
.body(json_bytes.into())?)
}
pub fn flush_events(self: &Arc<Self>) {
pub fn flush_events(self: &Arc<Self>) -> Task<()> {
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;
return Task::ready(());
}
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();
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,
}
};
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(())
}
.log_err(),
)
.detach();
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(),
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());
}
anyhow::Ok(())
}
.log_err()
.map(|_| ()),
)
}
}

View File

@@ -128,7 +128,6 @@ serde_json.workspace = true
session = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
sqlx = { version = "0.8", features = ["sqlite"] }
task.workspace = true
theme.workspace = true
unindent.workspace = true
util.workspace = true

View File

@@ -516,6 +516,7 @@ 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,
@@ -530,9 +531,12 @@ pub async fn post_events(
.build()
.unwrap(),
);
has_records = true;
}
}
request.send().await.log_err();
if has_records {
request.send().await.log_err();
}
}
};
@@ -555,7 +559,7 @@ fn for_snowflake(
country_code: Option<String>,
checksum_matched: bool,
) -> impl Iterator<Item = SnowflakeRow> {
body.events.into_iter().flat_map(move |event| {
body.events.into_iter().filter_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
@@ -744,9 +748,11 @@ 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 = Some(serde_json::json!({
"is_staff": body.is_staff,
}));
let user_properties = body.is_staff.map(|is_staff| {
serde_json::json!({
"is_staff": is_staff,
})
});
Some(SnowflakeRow {
time: timestamp,

View File

@@ -1,7 +1,7 @@
use call::ActiveCall;
use dap::DebugRequestType;
use dap::requests::{Initialize, Launch, StackTrace};
use dap::{SourceBreakpoint, requests::SetBreakpoints};
use dap::DebugRequestType;
use dap::{requests::SetBreakpoints, SourceBreakpoint};
use debugger_ui::debugger_panel::DebugPanel;
use debugger_ui::session::DebugSession;
use editor::Editor;
@@ -13,7 +13,7 @@ use std::{
path::Path,
sync::atomic::{AtomicBool, Ordering},
};
use workspace::{Workspace, dock::Panel};
use workspace::{dock::Panel, Workspace};
use super::{TestClient, TestServer};

View File

@@ -2,13 +2,11 @@ use crate::tests::TestServer;
use call::ActiveCall;
use collections::{HashMap, HashSet};
use debugger_ui::debugger_panel::DebugPanel;
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::StreamExt as _;
use gpui::{
AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
VisualContext,
};
use http_client::BlockedHttpClient;
use language::{
@@ -26,7 +24,6 @@ use project::{
};
use remote::SshRemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
use rpc::proto;
use serde_json::json;
use settings::SettingsStore;
use std::{path::Path, sync::Arc};
@@ -579,108 +576,3 @@ async fn test_ssh_collaboration_formatting_with_prettier(
"Prettier formatting was not applied to client buffer after host's request"
);
}
#[gpui::test]
async fn test_remote_server_debugger(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
cx_a.update(|cx| {
release_channel::init(SemanticVersion::default(), cx);
command_palette_hooks::init(cx);
if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok();
}
});
server_cx.update(|cx| {
release_channel::init(SemanticVersion::default(), cx);
});
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
path!("/code"),
json!({
"lib.rs": "fn one() -> usize { 1 }"
}),
)
.await;
// User A connects to the remote project via SSH.
server_cx.update(HeadlessProject::init);
let remote_http_client = Arc::new(BlockedHttpClient);
let node = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
let _headless_project = server_cx.new(|cx| {
client::init_settings(cx);
HeadlessProject::new(
HeadlessAppState {
session: server_ssh,
fs: remote_fs.clone(),
http_client: remote_http_client,
node_runtime: node,
languages,
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let mut server = TestServer::start(server_cx.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
cx_a.update(|cx| {
debugger_ui::init(cx);
command_palette_hooks::init(cx);
});
let (project_a, _) = client_a
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
.await;
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
let debugger_panel = workspace
.update_in(cx_a, |_workspace, window, cx| {
cx.spawn_in(window, DebugPanel::load)
})
.await
.unwrap();
workspace.update_in(cx_a, |workspace, window, cx| {
workspace.add_panel(debugger_panel, window, cx);
});
cx_a.run_until_parked();
let debug_panel = workspace
.update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
.unwrap();
let workspace_window = cx_a
.window_handle()
.downcast::<workspace::Workspace>()
.unwrap();
let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
cx_a.run_until_parked();
debug_panel.update(cx_a, |debug_panel, cx| {
assert_eq!(
debug_panel.active_session().unwrap().read(cx).session(cx),
session
)
});
session.update(cx_a, |session, _| {
assert_eq!(session.binary().command, "ssh");
});
let shutdown_session = workspace.update(cx_a, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
})
});
client_ssh.update(cx_a, |a, _| {
a.shutdown_processes(Some(proto::ShutdownRemoteServer {}))
});
shutdown_session.await.unwrap();
}

View File

@@ -22,7 +22,7 @@ use std::{
time::Duration,
};
use task::TcpArgumentsTemplate;
use util::{ResultExt as _, TryFutureExt};
use util::ResultExt as _;
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
@@ -126,7 +126,6 @@ pub(crate) struct TransportDelegate {
pending_requests: Requests,
transport: Transport,
server_tx: Arc<Mutex<Option<Sender<Message>>>>,
_tasks: Vec<gpui::Task<Option<()>>>,
}
impl TransportDelegate {
@@ -141,7 +140,6 @@ impl TransportDelegate {
log_handlers: Default::default(),
current_requests: Default::default(),
pending_requests: Default::default(),
_tasks: Default::default(),
};
let messages = this.start_handlers(transport_pipes, cx).await?;
Ok((messages, this))
@@ -168,43 +166,35 @@ impl TransportDelegate {
cx.update(|cx| {
if let Some(stdout) = params.stdout.take() {
self._tasks.push(
cx.background_executor()
.spawn(Self::handle_adapter_log(stdout, log_handler.clone()).log_err()),
);
cx.background_executor()
.spawn(Self::handle_adapter_log(stdout, log_handler.clone()))
.detach_and_log_err(cx);
}
self._tasks.push(
cx.background_executor().spawn(
Self::handle_output(
params.output,
client_tx,
self.pending_requests.clone(),
log_handler.clone(),
)
.log_err(),
),
);
cx.background_executor()
.spawn(Self::handle_output(
params.output,
client_tx,
self.pending_requests.clone(),
log_handler.clone(),
))
.detach_and_log_err(cx);
if let Some(stderr) = params.stderr.take() {
self._tasks.push(
cx.background_executor()
.spawn(Self::handle_error(stderr, self.log_handlers.clone()).log_err()),
);
cx.background_executor()
.spawn(Self::handle_error(stderr, self.log_handlers.clone()))
.detach_and_log_err(cx);
}
self._tasks.push(
cx.background_executor().spawn(
Self::handle_input(
params.input,
client_rx,
self.current_requests.clone(),
self.pending_requests.clone(),
log_handler.clone(),
)
.log_err(),
),
);
cx.background_executor()
.spawn(Self::handle_input(
params.input,
client_rx,
self.current_requests.clone(),
self.pending_requests.clone(),
log_handler.clone(),
))
.detach_and_log_err(cx);
})?;
{
@@ -377,7 +367,6 @@ impl TransportDelegate {
where
Stderr: AsyncRead + Unpin + Send + 'static,
{
log::debug!("Handle error started");
let mut buffer = String::new();
let mut reader = BufReader::new(stderr);

View File

@@ -12,9 +12,6 @@ workspace = true
path = "src/debugger_tools.rs"
doctest = false
[features]
test-support = []
[dependencies]
anyhow.workspace = true
dap.workspace = true

View File

@@ -41,7 +41,7 @@ struct DapLogView {
_subscriptions: Vec<Subscription>,
}
pub struct LogStore {
struct LogStore {
projects: HashMap<WeakEntity<Project>, ProjectState>,
debug_clients: HashMap<SessionId, DebugAdapterState>,
rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,
@@ -101,7 +101,7 @@ impl DebugAdapterState {
}
impl LogStore {
pub fn new(cx: &Context<Self>) -> Self {
fn new(cx: &Context<Self>) -> Self {
let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>();
cx.spawn(async move |this, cx| {
while let Some((client_id, io_kind, message)) = rpc_rx.next().await {
@@ -845,29 +845,3 @@ impl EventEmitter<Event> for LogStore {}
impl EventEmitter<Event> for DapLogView {}
impl EventEmitter<EditorEvent> for DapLogView {}
impl EventEmitter<SearchEvent> for DapLogView {}
#[cfg(any(test, feature = "test-support"))]
impl LogStore {
pub fn contained_session_ids(&self) -> Vec<SessionId> {
self.debug_clients.keys().cloned().collect()
}
pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
self.debug_clients
.get(&session_id)
.expect("This session should exist if a test is calling")
.rpc_messages
.messages
.clone()
.into()
}
pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
self.debug_clients
.get(&session_id)
.expect("This session should exist if a test is calling")
.log_messages
.clone()
.into()
}
}

View File

@@ -20,9 +20,6 @@ test-support = [
"project/test-support",
"util/test-support",
"workspace/test-support",
"env_logger",
"unindent",
"debugger_tools"
]
[dependencies]
@@ -40,7 +37,6 @@ gpui.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
parking_lot.workspace = true
picker.workspace = true
pretty_assertions.workspace = true
project.workspace = true
@@ -57,13 +53,9 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
env_logger = { workspace = true, optional = true }
debugger_tools = { workspace = true, optional = true }
unindent = { workspace = true, optional = true }
[dev-dependencies]
dap = { workspace = true, features = ["test-support"] }
debugger_tools = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -1,7 +1,7 @@
use dap::DebugRequest;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::Subscription;
use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
use gpui::{Subscription, WeakEntity};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
@@ -9,9 +9,7 @@ use sysinfo::System;
use ui::{Context, Tooltip, prelude::*};
use ui::{ListItem, ListItemSpacing};
use util::debug_panic;
use workspace::{ModalView, Workspace};
use crate::debugger_panel::DebugPanel;
use workspace::ModalView;
#[derive(Debug, Clone)]
pub(super) struct Candidate {
@@ -24,19 +22,19 @@ pub(crate) struct AttachModalDelegate {
selected_index: usize,
matches: Vec<StringMatch>,
placeholder_text: Arc<str>,
workspace: WeakEntity<Workspace>,
project: Entity<project::Project>,
pub(crate) debug_config: task::DebugTaskDefinition,
candidates: Arc<[Candidate]>,
}
impl AttachModalDelegate {
fn new(
workspace: Entity<Workspace>,
project: Entity<project::Project>,
debug_config: task::DebugTaskDefinition,
candidates: Arc<[Candidate]>,
) -> Self {
Self {
workspace: workspace.downgrade(),
project,
debug_config,
candidates,
selected_index: 0,
@@ -53,7 +51,7 @@ pub struct AttachModal {
impl AttachModal {
pub fn new(
workspace: Entity<Workspace>,
project: Entity<project::Project>,
debug_config: task::DebugTaskDefinition,
modal: bool,
window: &mut Window,
@@ -77,11 +75,11 @@ impl AttachModal {
.collect();
processes.sort_by_key(|k| k.name.clone());
let processes = processes.into_iter().collect();
Self::with_processes(workspace, debug_config, processes, modal, window, cx)
Self::with_processes(project, debug_config, processes, modal, window, cx)
}
pub(super) fn with_processes(
workspace: Entity<Workspace>,
project: Entity<project::Project>,
debug_config: task::DebugTaskDefinition,
processes: Arc<[Candidate]>,
modal: bool,
@@ -90,7 +88,7 @@ impl AttachModal {
) -> Self {
let picker = cx.new(|cx| {
Picker::uniform_list(
AttachModalDelegate::new(workspace, debug_config, processes),
AttachModalDelegate::new(project, debug_config, processes),
window,
cx,
)
@@ -204,7 +202,7 @@ impl PickerDelegate for AttachModalDelegate {
})
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let candidate = self
.matches
.get(self.selected_index())
@@ -227,17 +225,14 @@ impl PickerDelegate for AttachModalDelegate {
}
}
let definition = self.debug_config.clone();
let panel = self
.workspace
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
.ok()
.flatten();
if let Some(panel) = panel {
panel.update(cx, |panel, cx| {
panel.start_session(definition, window, cx);
});
}
let config = self.debug_config.clone();
self.project
.update(cx, |project, cx| {
let ret = project.start_debug_session(config, cx);
ret
})
.detach_and_log_err(cx);
cx.emit(DismissEvent);
}

View File

@@ -6,7 +6,6 @@ use crate::{new_session_modal::NewSessionModal, session::DebugSession};
use anyhow::{Result, anyhow};
use collections::HashMap;
use command_palette_hooks::CommandPaletteFilter;
use dap::StartDebuggingRequestArguments;
use dap::{
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
client::SessionId, debugger_settings::DebuggerSettings,
@@ -18,7 +17,6 @@ use gpui::{
actions, anchored, deferred,
};
use project::debugger::session::{Session, SessionStateEvent};
use project::{
Project,
debugger::{
@@ -32,9 +30,10 @@ use settings::Settings;
use std::any::TypeId;
use std::path::Path;
use std::sync::Arc;
use task::{DebugTaskDefinition, DebugTaskTemplate};
use task::DebugTaskDefinition;
use terminal_view::terminal_panel::TerminalPanel;
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
use util::debug_panic;
use workspace::{
Workspace,
dock::{DockPosition, Panel, PanelEvent},
@@ -64,7 +63,7 @@ pub struct DebugPanel {
active_session: Option<Entity<DebugSession>>,
/// This represents the last debug definition that was created in the new session modal
pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
project: Entity<Project>,
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
@@ -98,10 +97,10 @@ impl DebugPanel {
window,
|panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
panel.workspace.update(cx, |workspace, cx| {
let workspace_handle = cx.entity().clone();
let project = workspace.project().clone();
workspace.toggle_modal(window, cx, |window, cx| {
crate::attach_modal::AttachModal::new(
workspace_handle,
project,
event.debug_config.clone(),
true,
window,
@@ -128,7 +127,7 @@ impl DebugPanel {
_subscriptions,
past_debug_definition: None,
focus_handle: cx.focus_handle(),
project,
project: project.downgrade(),
workspace: workspace.weak_handle(),
context_menu: None,
};
@@ -220,7 +219,7 @@ impl DebugPanel {
pub fn load(
workspace: WeakEntity<Workspace>,
cx: &mut AsyncWindowContext,
cx: AsyncWindowContext,
) -> Task<Result<Entity<Self>>> {
cx.spawn(async move |cx| {
workspace.update_in(cx, |workspace, window, cx| {
@@ -246,226 +245,114 @@ impl DebugPanel {
});
})
.detach();
workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
debug_panel
})
})
}
pub fn start_session(
&mut self,
definition: DebugTaskDefinition,
window: &mut Window,
cx: &mut Context<Self>,
) {
let task_contexts = self
.workspace
.update(cx, |workspace, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})
.ok();
let dap_store = self.project.read(cx).dap_store().clone();
cx.spawn_in(window, async move |this, cx| {
let task_context = if let Some(task) = task_contexts {
task.await
.active_worktree_context
.map_or(task::TaskContext::default(), |context| context.1)
} else {
task::TaskContext::default()
};
let (session, task) = dap_store.update(cx, |dap_store, cx| {
let template = DebugTaskTemplate {
locator: None,
definition: definition.clone(),
};
let session = if let Some(debug_config) = template
.to_zed_format()
.resolve_task("debug_task", &task_context)
.and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
{
dap_store.new_session(debug_config.definition, None, cx)
} else {
dap_store.new_session(definition.clone(), None, cx)
};
(session.clone(), dap_store.boot_session(session, cx))
})?;
match task.await {
Err(e) => {
this.update(cx, |this, cx| {
this.workspace
.update(cx, |workspace, cx| {
workspace.show_error(&e, cx);
})
.ok();
})
.ok();
session
.update(cx, |session, cx| session.shutdown(cx))?
.await;
}
Ok(_) => Self::register_session(this, session, cx).await?,
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
async fn register_session(
this: WeakEntity<Self>,
session: Entity<Session>,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let adapter_name = session.update(cx, |session, _| session.adapter_name())?;
this.update_in(cx, |_, window, cx| {
cx.subscribe_in(
&session,
window,
move |_, session, event: &SessionStateEvent, window, cx| match event {
SessionStateEvent::Restart => {
let mut curr_session = session.clone();
while let Some(parent_session) = curr_session
.read_with(cx, |session, _| session.parent_session().cloned())
{
curr_session = parent_session;
}
let definition = curr_session.update(cx, |session, _| session.definition());
let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
let definition = definition.clone();
cx.spawn_in(window, async move |this, cx| {
task.await;
this.update_in(cx, |this, window, cx| {
this.start_session(definition, window, cx)
})
})
.detach_and_log_err(cx);
}
_ => {}
},
)
.detach();
})
.ok();
let serialized_layout = persistence::get_serialized_pane_layout(adapter_name).await;
let workspace = this.update_in(cx, |this, window, cx| {
this.sessions.retain(|session| {
session
.read(cx)
.mode()
.as_running()
.map_or(false, |running_state| {
!running_state.read(cx).session().read(cx).is_terminated()
})
});
let session_item = DebugSession::running(
this.project.clone(),
this.workspace.clone(),
session,
cx.weak_entity(),
serialized_layout,
window,
cx,
);
if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
// We might want to make this an event subscription and only notify when a new thread is selected
// This is used to filter the command menu correctly
cx.observe(&running, |_, _, cx| cx.notify()).detach();
}
this.sessions.push(session_item.clone());
this.activate_session(session_item, window, cx);
this.workspace.clone()
})?;
workspace.update_in(cx, |workspace, window, cx| {
workspace.focus_panel::<Self>(window, cx);
})?;
Ok(())
}
pub fn start_child_session(
&mut self,
request: &StartDebuggingRequestArguments,
parent_session: Entity<Session>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(worktree) = parent_session.read(cx).worktree() else {
log::error!("Attempted to start a child session from non local debug session");
return;
};
let dap_store_handle = self.project.read(cx).dap_store().clone();
let breakpoint_store = self.project.read(cx).breakpoint_store();
let definition = parent_session.read(cx).definition().clone();
let mut binary = parent_session.read(cx).binary().clone();
binary.request_args = request.clone();
cx.spawn_in(window, async move |this, cx| {
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
let session =
dap_store.new_session(definition.clone(), Some(parent_session.clone()), cx);
let task = session.update(cx, |session, cx| {
session.boot(
binary,
worktree,
breakpoint_store,
dap_store_handle.downgrade(),
cx,
)
});
(session, task)
})?;
match task.await {
Err(e) => {
this.update(cx, |this, cx| {
this.workspace
.update(cx, |workspace, cx| {
workspace.show_error(&e, cx);
})
.ok();
})
.ok();
session
.update(cx, |session, cx| session.shutdown(cx))?
.await;
}
Ok(_) => Self::register_session(this, session, cx).await?,
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
self.active_session.clone()
}
pub fn debug_panel_items_by_client(
&self,
client_id: &SessionId,
cx: &Context<Self>,
) -> Vec<Entity<DebugSession>> {
self.sessions
.iter()
.filter(|item| item.read(cx).session_id(cx) == *client_id)
.map(|item| item.clone())
.collect()
}
pub fn debug_panel_item_by_client(
&self,
client_id: SessionId,
cx: &mut Context<Self>,
) -> Option<Entity<DebugSession>> {
self.sessions
.iter()
.find(|item| {
let item = item.read(cx);
item.session_id(cx) == client_id
})
.cloned()
}
fn handle_dap_store_event(
&mut self,
_dap_store: &Entity<DapStore>,
dap_store: &Entity<DapStore>,
event: &dap_store::DapStoreEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => {
let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
return log::error!(
"Couldn't get session with id: {session_id:?} from DebugClientStarted event"
);
};
let adapter_name = session.read(cx).adapter_name();
let session_id = *session_id;
cx.spawn_in(window, async move |this, cx| {
let serialized_layout =
persistence::get_serialized_pane_layout(adapter_name).await;
this.update_in(cx, |this, window, cx| {
let Some(project) = this.project.upgrade() else {
return log::error!(
"Debug Panel out lived it's weak reference to Project"
);
};
if this
.sessions
.iter()
.any(|item| item.read(cx).session_id(cx) == session_id)
{
// We already have an item for this session.
debug_panic!("We should never reuse session ids");
return;
}
this.sessions.retain(|session| {
session
.read(cx)
.mode()
.as_running()
.map_or(false, |running_state| {
!running_state.read(cx).session().read(cx).is_terminated()
})
});
let session_item = DebugSession::running(
project,
this.workspace.clone(),
session,
cx.weak_entity(),
serialized_layout,
window,
cx,
);
if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
// We might want to make this an event subscription and only notify when a new thread is selected
// This is used to filter the command menu correctly
cx.observe(&running, |_, _, cx| cx.notify()).detach();
}
this.sessions.push(session_item.clone());
this.activate_session(session_item, window, cx);
})
})
.detach();
}
dap_store::DapStoreEvent::RunInTerminal {
title,
cwd,
@@ -487,12 +374,6 @@ impl DebugPanel {
)
.detach_and_log_err(cx);
}
dap_store::DapStoreEvent::SpawnChildSession {
request,
parent_session,
} => {
self.start_child_session(request, parent_session.clone(), window, cx);
}
_ => {}
}
}
@@ -527,7 +408,7 @@ impl DebugPanel {
cwd,
title,
},
task::RevealStrategy::Never,
task::RevealStrategy::Always,
window,
cx,
);
@@ -587,6 +468,8 @@ impl DebugPanel {
let session = this.dap_store().read(cx).session_by_id(session_id);
session.map(|session| !session.read(cx).is_terminated())
})
.ok()
.flatten()
.unwrap_or_default();
cx.spawn_in(window, async move |this, cx| {
@@ -1010,6 +893,7 @@ impl DebugPanel {
impl EventEmitter<PanelEvent> for DebugPanel {}
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
impl EventEmitter<project::Event> for DebugPanel {}
impl Focusable for DebugPanel {
fn focus_handle(&self, _: &App) -> FocusHandle {
@@ -1155,15 +1039,3 @@ impl Render for DebugPanel {
.into_any()
}
}
struct DebuggerProvider(Entity<DebugPanel>);
impl workspace::DebuggerProvider for DebuggerProvider {
fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App) {
self.0.update(cx, |_, cx| {
cx.defer_in(window, |this, window, cx| {
this.start_session(definition, window, cx);
})
})
}
}

View File

@@ -16,7 +16,7 @@ mod new_session_modal;
mod persistence;
pub(crate) mod session;
#[cfg(any(test, feature = "test-support"))]
#[cfg(test)]
pub mod tests;
actions!(

View File

@@ -4,12 +4,14 @@ use std::{
path::{Path, PathBuf},
};
use anyhow::{Result, anyhow};
use dap::{DapRegistry, DebugRequest};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
WeakEntity,
};
use project::Project;
use settings::Settings;
use task::{DebugTaskDefinition, DebugTaskTemplate, LaunchRequest};
use theme::ThemeSettings;
@@ -19,6 +21,7 @@ use ui::{
LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton,
ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
};
use util::ResultExt;
use workspace::{ModalView, Workspace};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
@@ -85,11 +88,11 @@ impl NewSessionModal {
}
}
fn debug_config(&self, cx: &App, debugger: &str) -> DebugTaskDefinition {
fn debug_config(&self, cx: &App) -> Option<DebugTaskDefinition> {
let request = self.mode.debug_task(cx);
DebugTaskDefinition {
adapter: debugger.to_owned(),
label: suggested_label(&request, debugger),
Some(DebugTaskDefinition {
adapter: self.debugger.clone()?.to_string(),
label: suggested_label(&request, self.debugger.as_deref()?),
request,
initialize_args: self.initialize_args.clone(),
tcp_connection: None,
@@ -97,26 +100,26 @@ impl NewSessionModal {
ToggleState::Selected => Some(true),
_ => None,
},
}
})
}
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
let Some(debugger) = self.debugger.as_ref() else {
// todo: show in UI.
log::error!("No debugger selected");
return;
};
let config = self.debug_config(cx, debugger);
let debug_panel = self.debug_panel.clone();
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) -> Result<()> {
let workspace = self.workspace.clone();
let config = self
.debug_config(cx)
.ok_or_else(|| anyhow!("Failed to create a debug config"))?;
let task_contexts = self
.workspace
let _ = self.debug_panel.update(cx, |panel, _| {
panel.past_debug_definition = Some(config.clone());
});
let task_contexts = workspace
.update(cx, |workspace, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})
.ok();
cx.spawn_in(window, async move |this, cx| {
cx.spawn(async move |this, cx| {
let task_context = if let Some(task) = task_contexts {
task.await
.active_worktree_context
@@ -124,8 +127,9 @@ impl NewSessionModal {
} else {
task::TaskContext::default()
};
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
debug_panel.update_in(cx, |debug_panel, window, cx| {
let task = project.update(cx, |this, cx| {
let template = DebugTaskTemplate {
locator: None,
definition: config.clone(),
@@ -135,18 +139,23 @@ impl NewSessionModal {
.resolve_task("debug_task", &task_context)
.and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
{
debug_panel.start_session(debug_config.definition, window, cx)
this.start_debug_session(debug_config.definition, cx)
} else {
debug_panel.start_session(config, window, cx)
this.start_debug_session(config, cx)
}
})?;
this.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
let spawn_result = task.await;
if spawn_result.is_ok() {
this.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
spawn_result?;
anyhow::Result::<_, anyhow::Error>::Ok(())
})
.detach_and_log_err(cx);
Ok(())
}
fn update_attach_picker(
@@ -240,12 +249,15 @@ impl NewSessionModal {
);
}
DebugRequest::Attach(_) => {
let Some(workspace) = this.workspace.upgrade() else {
let Ok(project) = this
.workspace
.read_with(cx, |this, _| this.project().clone())
else {
return;
};
this.mode = NewSessionMode::attach(
this.debugger.clone(),
workspace,
project,
window,
cx,
);
@@ -345,7 +357,7 @@ struct AttachMode {
impl AttachMode {
fn new(
debugger: Option<SharedString>,
workspace: Entity<Workspace>,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Entity<Self> {
@@ -358,7 +370,7 @@ impl AttachMode {
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
let modal = AttachModal::new(workspace, debug_definition.clone(), false, window, cx);
let modal = AttachModal::new(project, debug_definition.clone(), false, window, cx);
window.focus(&modal.focus_handle(cx));
modal
@@ -458,11 +470,11 @@ impl RenderOnce for NewSessionMode {
impl NewSessionMode {
fn attach(
debugger: Option<SharedString>,
workspace: Entity<Workspace>,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Self {
Self::Attach(AttachMode::new(debugger, workspace, window, cx))
Self::Attach(AttachMode::new(debugger, project, window, cx))
}
fn launch(
past_launch_config: Option<LaunchRequest>,
@@ -557,12 +569,15 @@ impl Render for NewSessionModal {
.toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
.style(ui::ButtonStyle::Subtle)
.on_click(cx.listener(|this, _, window, cx| {
let Some(workspace) = this.workspace.upgrade() else {
let Ok(project) = this
.workspace
.read_with(cx, |this, _| this.project().clone())
else {
return;
};
this.mode = NewSessionMode::attach(
this.debugger.clone(),
workspace,
project,
window,
cx,
);
@@ -616,7 +631,7 @@ impl Render for NewSessionModal {
.child(
Button::new("debugger-spawn", "Start")
.on_click(cx.listener(|this, _, window, cx| {
this.start_new_session(window, cx);
this.start_new_session(window, cx).log_err();
}))
.disabled(self.debugger.is_none()),
),

View File

@@ -88,12 +88,6 @@ impl DebugSession {
}
}
pub fn session(&self, cx: &App) -> Entity<Session> {
match &self.mode {
DebugSessionState::Running(entity) => entity.read(cx).session().clone(),
}
}
pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
match &self.mode {
DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)),
@@ -121,7 +115,13 @@ impl DebugSession {
};
self.label
.get_or_init(|| session.read(cx).label())
.get_or_init(|| {
session
.read(cx)
.as_local()
.expect("Remote Debug Sessions are not implemented yet")
.label()
})
.to_owned()
}

View File

@@ -418,19 +418,6 @@ impl RunningState {
let threads = this.session.update(cx, |this, cx| this.threads(cx));
this.select_current_thread(&threads, cx);
}
SessionEvent::CapabilitiesLoaded => {
let capabilities = this.capabilities(cx);
if !capabilities.supports_modules_request.unwrap_or(false) {
this.remove_pane_item(DebuggerPaneItem::Modules, window, cx);
}
if !capabilities
.supports_loaded_sources_request
.unwrap_or(false)
{
this.remove_pane_item(DebuggerPaneItem::LoadedSources, window, cx);
}
}
_ => {}
}
cx.notify()
@@ -460,14 +447,35 @@ impl RunningState {
workspace::PaneGroup::with_root(root)
} else {
pane_close_subscriptions.clear();
let module_list = if session
.read(cx)
.capabilities()
.supports_modules_request
.unwrap_or(false)
{
Some(&module_list)
} else {
None
};
let loaded_source_list = if session
.read(cx)
.capabilities()
.supports_loaded_sources_request
.unwrap_or(false)
{
Some(&loaded_source_list)
} else {
None
};
let root = Self::default_pane_layout(
project,
&workspace,
&stack_frame_list,
&variable_list,
&module_list,
&loaded_source_list,
module_list,
loaded_source_list,
&console,
&breakpoint_list,
&mut pane_close_subscriptions,
@@ -504,6 +512,11 @@ impl RunningState {
window: &mut Window,
cx: &mut Context<Self>,
) {
debug_assert!(
item_kind.is_supported(self.session.read(cx).capabilities()),
"We should only allow removing supported item kinds"
);
if let Some((pane, item_id)) = self.panes.panes().iter().find_map(|pane| {
Some(pane).zip(
pane.read(cx)
@@ -933,8 +946,8 @@ impl RunningState {
workspace: &WeakEntity<Workspace>,
stack_frame_list: &Entity<StackFrameList>,
variable_list: &Entity<VariableList>,
module_list: &Entity<ModuleList>,
loaded_source_list: &Entity<LoadedSourceList>,
module_list: Option<&Entity<ModuleList>>,
loaded_source_list: Option<&Entity<LoadedSourceList>>,
console: &Entity<Console>,
breakpoints: &Entity<BreakpointList>,
subscriptions: &mut HashMap<EntityId, Subscription>,
@@ -990,36 +1003,41 @@ impl RunningState {
window,
cx,
);
this.add_item(
Box::new(SubView::new(
module_list.focus_handle(cx),
module_list.clone().into(),
DebuggerPaneItem::Modules,
if let Some(module_list) = module_list {
this.add_item(
Box::new(SubView::new(
module_list.focus_handle(cx),
module_list.clone().into(),
DebuggerPaneItem::Modules,
None,
cx,
)),
false,
false,
None,
window,
cx,
)),
false,
false,
None,
window,
cx,
);
);
this.activate_item(0, false, false, window, cx);
}
this.add_item(
Box::new(SubView::new(
loaded_source_list.focus_handle(cx),
loaded_source_list.clone().into(),
DebuggerPaneItem::LoadedSources,
if let Some(loaded_source_list) = loaded_source_list {
this.add_item(
Box::new(SubView::new(
loaded_source_list.focus_handle(cx),
loaded_source_list.clone().into(),
DebuggerPaneItem::LoadedSources,
None,
cx,
)),
false,
false,
None,
window,
cx,
)),
false,
false,
None,
window,
cx,
);
this.activate_item(0, false, false, window, cx);
);
this.activate_item(1, false, false, window, cx);
}
});
let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);

View File

@@ -1,29 +1,16 @@
use std::sync::Arc;
use anyhow::{Result, anyhow};
use dap::{DebugRequest, client::DebugAdapterClient};
use gpui::{Entity, TestAppContext, WindowHandle};
use project::{Project, debugger::session::Session};
use project::Project;
use settings::SettingsStore;
use task::DebugTaskDefinition;
use terminal_view::terminal_panel::TerminalPanel;
use workspace::Workspace;
use crate::{debugger_panel::DebugPanel, session::DebugSession};
#[cfg(test)]
mod attach_modal;
#[cfg(test)]
mod console;
#[cfg(test)]
mod dap_logger;
#[cfg(test)]
mod debugger_panel;
#[cfg(test)]
mod module_list;
#[cfg(test)]
mod stack_frame_list;
#[cfg(test)]
mod variable_list;
pub fn init_test(cx: &mut gpui::TestAppContext) {
@@ -55,7 +42,7 @@ pub async fn init_test_workspace(
let debugger_panel = workspace_handle
.update(cx, |_, window, cx| {
cx.spawn_in(window, async move |this, cx| {
DebugPanel::load(this, cx).await
DebugPanel::load(this, cx.clone()).await
})
})
.unwrap()
@@ -95,46 +82,3 @@ pub fn active_debug_session_panel(
})
.unwrap()
}
pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
workspace: &WindowHandle<Workspace>,
cx: &mut gpui::TestAppContext,
config: DebugTaskDefinition,
configure: T,
) -> Result<Entity<Session>> {
let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure);
workspace.update(cx, |workspace, window, cx| {
workspace.start_debug_session(config, window, cx)
})?;
cx.run_until_parked();
let session = workspace.read_with(cx, |workspace, cx| {
workspace
.panel::<DebugPanel>(cx)
.and_then(|panel| panel.read(cx).active_session())
.and_then(|session| session.read(cx).mode().as_running().cloned())
.map(|running| running.read(cx).session().clone())
.ok_or_else(|| anyhow!("Failed to get active session"))
})??;
Ok(session)
}
pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
workspace: &WindowHandle<Workspace>,
cx: &mut gpui::TestAppContext,
configure: T,
) -> Result<Entity<Session>> {
start_debug_session_with(
workspace,
cx,
DebugTaskDefinition {
adapter: "fake-adapter".to_string(),
request: DebugRequest::Launch(Default::default()),
label: "test".to_string(),
initialize_args: None,
tcp_connection: None,
stop_on_entry: None,
},
configure,
)
}

View File

@@ -1,4 +1,4 @@
use crate::{attach_modal::Candidate, tests::start_debug_session_with, *};
use crate::{attach_modal::Candidate, *};
use attach_modal::AttachModal;
use dap::{FakeAdapter, client::SessionId};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
@@ -26,8 +26,8 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session_with(
&workspace,
let session = debugger::test::start_debug_session_with(
&project,
cx,
DebugTaskDefinition {
adapter: "fake-adapter".to_string(),
@@ -47,6 +47,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
});
},
)
.await
.unwrap();
cx.run_until_parked();
@@ -98,10 +99,9 @@ async fn test_show_attach_modal_and_select_process(
});
let attach_modal = workspace
.update(cx, |workspace, window, cx| {
let workspace_handle = cx.entity();
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::with_processes(
workspace_handle,
project.clone(),
DebugTaskDefinition {
adapter: FakeAdapter::ADAPTER_NAME.into(),
request: dap::DebugRequest::Attach(AttachRequest::default()),

View File

@@ -1,7 +1,4 @@
use crate::{
tests::{active_debug_session_panel, start_debug_session},
*,
};
use crate::{tests::active_debug_session_panel, *};
use dap::requests::StackTrace;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
@@ -31,7 +28,9 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
})
.unwrap();
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<StackTrace, _>(move |_, _| {

View File

@@ -1,118 +0,0 @@
use crate::tests::{init_test, init_test_workspace, start_debug_session};
use dap::requests::{StackTrace, Threads};
use debugger_tools::LogStore;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::Project;
use serde_json::json;
use std::cell::OnceCell;
#[gpui::test]
async fn test_dap_logger_captures_all_session_rpc_messages(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
let log_store_cell = std::rc::Rc::new(OnceCell::new());
cx.update(|cx| {
let log_store_cell = log_store_cell.clone();
cx.observe_new::<LogStore>(move |_, _, cx| {
log_store_cell.set(cx.entity()).unwrap();
})
.detach();
debugger_tools::init(cx);
});
init_test(cx);
let log_store = log_store_cell.get().unwrap().clone();
// Create a filesystem with a simple project
let fs = project::FakeFs::new(executor.clone());
fs.insert_tree(
"/project",
json!({
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}"
}),
)
.await;
assert!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids()
.is_empty()),
"log_store shouldn't contain any session IDs before any sessions were created"
);
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Start a debug session
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session_id = session.read_with(cx, |session, _| session.session_id());
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
assert_eq!(
log_store.read_with(cx, |log_store, _| log_store.contained_session_ids().len()),
1,
);
assert!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids()
.contains(&session_id)),
"log_store should contain the session IDs of the started session"
);
assert!(
!log_store.read_with(cx, |log_store, _| log_store
.rpc_messages_for_session_id(session_id)
.is_empty()),
"We should have the initialization sequence in the log store"
);
// Set up basic responses for common requests
client.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
});
client.on_request::<StackTrace, _>(move |_, _| {
Ok(dap::StackTraceResponse {
stack_frames: Vec::default(),
total_frames: None,
})
});
// Run until all pending tasks are executed
cx.run_until_parked();
// Simulate a stopped event to generate more DAP messages
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
// Shutdown the debug session
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
cx.run_until_parked();
}

View File

@@ -1,4 +1,4 @@
use crate::{tests::start_debug_session, *};
use crate::*;
use dap::{
ErrorResponse, Message, RunInTerminalRequestArguments, SourceBreakpoint,
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
@@ -48,7 +48,9 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Threads, _>(move |_, _| {
@@ -185,7 +187,9 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Threads, _>(move |_, _| {
@@ -350,7 +354,9 @@ async fn test_handle_successful_run_in_terminal_reverse_request(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
@@ -413,86 +419,6 @@ async fn test_handle_successful_run_in_terminal_reverse_request(
shutdown_session.await.unwrap();
}
#[gpui::test]
async fn test_handle_start_debugging_request(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
"/project",
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
let fake_config = json!({"one": "two"});
let launched_with = Arc::new(parking_lot::Mutex::new(None));
let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
let launched_with = launched_with.clone();
move |client| {
let launched_with = launched_with.clone();
client.on_request::<dap::requests::Launch, _>(move |_, args| {
launched_with.lock().replace(args.raw);
Ok(())
});
client.on_request::<dap::requests::Attach, _>(move |_, _| {
assert!(false, "should not get attach request");
Ok(())
});
}
});
client
.fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
request: StartDebuggingRequestArgumentsRequest::Launch,
configuration: fake_config.clone(),
})
.await;
cx.run_until_parked();
workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
let active_session = debug_panel
.read(cx)
.active_session()
.unwrap()
.read(cx)
.session(cx);
let parent_session = active_session.read(cx).parent_session().unwrap();
assert_eq!(
active_session.read(cx).definition(),
parent_session.read(cx).definition()
);
})
.unwrap();
assert_eq!(&fake_config, launched_with.lock().as_ref().unwrap());
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
// // covers that we always send a response back, if something when wrong,
// // while spawning the terminal
#[gpui::test]
@@ -518,7 +444,9 @@ async fn test_handle_error_run_in_terminal_reverse_request(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
@@ -594,7 +522,9 @@ async fn test_handle_start_debugging_reverse_request(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -699,7 +629,9 @@ async fn test_shutdown_children_when_parent_session_shutdown(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let parent_session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -805,7 +737,9 @@ async fn test_shutdown_parent_session_if_all_children_are_shutdown(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let parent_session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_response::<StartDebugging, _>(move |_| {}).await;
@@ -924,7 +858,7 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |client| {
let session = debugger::test::start_debug_session(&project, cx, |client| {
client.on_request::<dap::requests::Initialize, _>(move |_, _| {
Ok(dap::Capabilities {
supports_step_back: Some(true),
@@ -932,6 +866,7 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
})
});
})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
@@ -1138,7 +1073,9 @@ async fn test_send_breakpoints_when_editor_has_been_saved(
.update(cx, |_, _, cx| worktree.read(cx).id())
.unwrap();
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
let buffer = project
@@ -1353,7 +1290,9 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
});
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
let called_set_breakpoints = Arc::new(AtomicBool::new(false));
@@ -1419,7 +1358,7 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
start_debug_session(&workspace, cx, |client| {
let task = project::debugger::test::start_debug_session(&project, cx, |client| {
client.on_request::<dap::requests::Initialize, _>(|_, _| {
Err(ErrorResponse {
error: Some(Message {
@@ -1433,8 +1372,12 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
}),
})
});
})
.ok();
});
assert!(
task.await.is_err(),
"Session should failed to start if launch request fails"
);
cx.run_until_parked();

View File

@@ -1,13 +1,16 @@
use crate::{
debugger_panel::DebugPanel,
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
tests::{active_debug_session_panel, init_test, init_test_workspace},
};
use dap::{
StoppedEvent,
requests::{Initialize, Modules},
};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use project::{
FakeFs, Project,
debugger::{self},
};
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicI32, Ordering},
@@ -28,7 +31,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |client| {
let session = debugger::test::start_debug_session(&project, cx, |client| {
client.on_request::<Initialize, _>(move |_, _| {
Ok(dap::Capabilities {
supports_modules_request: Some(true),
@@ -36,6 +39,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
})
});
})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());

View File

@@ -1,7 +1,7 @@
use crate::{
debugger_panel::DebugPanel,
session::running::stack_frame_list::StackFrameEntry,
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
tests::{active_debug_session_panel, init_test, init_test_workspace},
};
use dap::{
StackFrame,
@@ -9,7 +9,7 @@ use dap::{
};
use editor::{Editor, ToPoint as _};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use project::{FakeFs, Project, debugger};
use serde_json::json;
use std::sync::Arc;
use unindent::Unindent as _;
@@ -50,7 +50,9 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
@@ -227,7 +229,9 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
});
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Threads, _>(move |_, _| {
@@ -491,7 +495,9 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<Threads, _>(move |_, _| {

View File

@@ -6,7 +6,7 @@ use std::sync::{
use crate::{
DebugPanel,
session::running::variable_list::{CollapseSelectedEntry, ExpandSelectedEntry},
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
tests::{active_debug_session_panel, init_test, init_test_workspace},
};
use collections::HashMap;
use dap::{
@@ -15,7 +15,7 @@ use dap::{
};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::{SelectFirst, SelectNext, SelectPrevious};
use project::{FakeFs, Project};
use project::{FakeFs, Project, debugger};
use serde_json::json;
use unindent::Unindent as _;
use util::path;
@@ -54,7 +54,9 @@ async fn test_basic_fetch_initial_scope_and_variables(
})
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -264,7 +266,9 @@ async fn test_fetch_variables_for_multiple_scopes(
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -524,7 +528,9 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
})
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -1307,7 +1313,9 @@ async fn test_variable_list_only_sends_requests_when_rendering(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {
@@ -1552,7 +1560,9 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let session = debugger::test::start_debug_session(&project, cx, |_| {})
.await
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client.on_request::<dap::requests::Threads, _>(move |_, _| {

View File

@@ -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();
let mut ref_iter = reference_excerpts.lines().filter(|line| *line != "§ -----");
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('§') {
} else if mut_line.contains('§') && mut_line != "§ -----" {
skipped_block = true;
}
}

View File

@@ -3108,6 +3108,13 @@ 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,

View File

@@ -199,6 +199,9 @@ 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());
}

View File

@@ -29,6 +29,7 @@ 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

View File

@@ -1,3 +0,0 @@
url = "https://github.com/GyulyVGC/sniffnet.git"
revision = "cfb5b6519bd7838f279e5be9d360445aaffaa647"
language_extension = "rs"

View File

@@ -1,16 +0,0 @@
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.

View File

@@ -1 +0,0 @@
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 dont 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.

View File

@@ -1,3 +0,0 @@
url = "https://github.com/swc-project/swc.git"
revision = "787d5fabf410fafe6595ec00c197181b27578cb1"
language_extension = "rs"

View File

@@ -1,6 +0,0 @@
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.

View File

@@ -1 +0,0 @@
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.

View File

@@ -0,0 +1,3 @@
url = "https://github.com/tmickleydoyle/cli-todo-app.git"
revision = "0e9cb6528a28a34ca491d184263993775dbde9c4"
language_extension = "rs"

View File

@@ -0,0 +1,5 @@
- A complete command-line todo application was implemented in Rust featuring core functionality for adding tasks, listing tasks with filtering options, and marking tasks as complete, with persistent local storage using JSON.
- Essential dependencies were added to the project including clap for CLI argument parsing, serde/serde_json for data serialization, chrono for timestamp handling, and uuid for generating unique task identifiers.
- A task management system was established through a Task struct containing ID, description, creation timestamp, and status fields, coupled with file-based storage logic that handles atomic read/write operations to maintain data consistency.
- Comprehensive user documentation was provided in the README file, detailing installation instructions, command usage examples, project structure explanations, and licensing information.
- Basic error handling was implemented for file I/O operations and data parsing failures, along with empty state detection to improve user experience when no tasks exist.

View File

@@ -0,0 +1 @@
Create a new Rust project for a command-line todo list application. The app should support adding new tasks, listing all tasks, and marking tasks as complete. Tasks should be stored in a local file using JSON or TOML for persistence. Each task should have a unique ID, a description, a creation timestamp, and a status indicating whether it's pending or completed. Use common Rust libraries for CLI argument parsing and data serialization. Organize the code in a modular way with separate files or modules for task definitions and storage logic. Include a README file in the project to explain how to use the todo commands.

View File

@@ -0,0 +1,7 @@
- The first tool call should be to path search for "Cargo.toml" to identify the Rust projects root directory and validate its structure, ensuring no assumptions are made about file locations.
- After confirming the project root, search for and verify paths to critical implementation files: src/main.rs, src/cli.rs, src/storage.rs, and src/task.rs before reading or modifying their contents.
- When adding dependencies, first read the existing Cargo.toml contents to avoid version conflicts, then incrementally insert clap, serde, chrono, and uuid with explicit version/feature specs.
- For the task storage system, locate or create tasks.json only after confirming the projects working directory via path checks, then implement read/write operations with atomic file handling.
- When designing the CLI, derive command structures in cli.rs (or similar file) after validating the files existence and checking for preexisting argument-parsing logic to avoid duplication.
- Integrate components in main.rs only after confirming module paths (mod cli; mod storage; mod task;) and ensuring no breaking changes to existing entry-point logic.
- Validate documentation updates by first locating README.md in the project root, then inserting usage examples and installation steps while preserving any prior content.

View File

@@ -1,4 +0,0 @@
url = "https://github.com/dani-garcia/vaultwarden.git"
revision = "3a1f1bae002bebf26ce3a38b879c1ba26529af1e"
language_extension = "rs"
allow_preexisting_diagnostics = true

View File

@@ -1,6 +0,0 @@
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.

View File

@@ -1 +0,0 @@
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. Id 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.

View File

@@ -1,3 +0,0 @@
url = "https://github.com/qarmin/czkawka.git"
revision = "db164d3698198dd46653b1c3bb0384f8a9e38fab"
language_extension = "rs"

View File

@@ -1,7 +0,0 @@
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.

View File

@@ -1 +0,0 @@
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 18 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.

View File

@@ -1,2 +1,2 @@
1. The changes must replace the previous output returned by `FindReplaceFileTool` with the new `ToolResult` struct. The struct should contain an `output` field that is the same as the string we were returning before, and a new `card` field that contains a view for the card
2. The card should be a view that displays a diff. Each line in the diff should be colored according to whether it was added, removed or unchanged.
- 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.

View File

@@ -1,3 +1,3 @@
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.
- 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.

View File

@@ -1,3 +0,0 @@
url = "https://github.com/clockworklabs/SpacetimeDB.git"
revision = "68d23d4c25548fd74f1bde28a57d8858022b9671"
language_extension = "rs"

View File

@@ -1,6 +0,0 @@
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.

View File

@@ -1,12 +0,0 @@
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.

View File

@@ -1,3 +0,0 @@
url = "https://github.com/Rust-GPU/Rust-CUDA.git"
revision = "728013419b6c4c80e099a42413574c36a9aff9c7"
language_extension = "rs"

View File

@@ -1,4 +0,0 @@
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.

View File

@@ -1 +0,0 @@
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.

View File

@@ -1,3 +0,0 @@
url = "https://github.com/SAP-samples/abap-cheat-sheets.git"
revision = "262c0472eeb03e05ff8235767356a328d97850e6"
language_extension = "rs"

View File

@@ -1,3 +0,0 @@
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.

View File

@@ -1,17 +0,0 @@
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?

View File

@@ -1,4 +0,0 @@
url = "https://github.com/huggingface/candle.git"
revision = "3164a19a5dc18f5e0f7a063ae85a0cfd289e98f1"
language_extension = "rs"
allow_preexisting_diagnostics = true

View File

@@ -1,4 +0,0 @@
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.

View File

@@ -1 +0,0 @@
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.

View File

@@ -1,3 +0,0 @@
url = "https://github.com/clockworklabs/SpacetimeDB.git"
revision = "13dfb031351c3adf308c74b2a085ca15aa797db1"
language_extension = "rs"

View File

@@ -1,6 +0,0 @@
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.

View File

@@ -1 +0,0 @@
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.

View File

@@ -1,3 +0,0 @@
url = "https://github.com/bevyengine/bevy.git"
revision = "ac52cca033b351cc966cd3d40eb99ffbefbdb104"
language_extension = "rs"

View File

@@ -1,5 +0,0 @@
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.

View File

@@ -1 +0,0 @@
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.

View File

@@ -1,3 +0,0 @@
url = "https://github.com/alacritty/alacritty.git"
revision = "c9c41e637ac49f3cd67cf0362c596ae9d947f896"
language_extension = "rs"

View File

@@ -1,6 +0,0 @@
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 processs 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).

View File

@@ -1 +0,0 @@
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.

View File

@@ -1,3 +0,0 @@
url = "https://github.com/lancedb/lancedb.git"
revision = "698f329598bcfa8a5bf0feedfdd4344a4cdc7e4d"
language_extension = "rs"

View File

@@ -1,6 +0,0 @@
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.

View File

@@ -1 +0,0 @@
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.

View File

@@ -1,3 +0,0 @@
url = "https://github.com/tikv/tikv.git"
revision = "be74cadcdd6608e5788d0c2a6784c456b4ce84e6"
language_extension = "rs"

View File

@@ -1,5 +0,0 @@
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.

View File

@@ -1 +0,0 @@
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.

View File

@@ -1,3 +0,0 @@
url = "https://github.com/block/goose.git"
revision = "d7308457fe3f1b9c7253de45b2f81ddc4f005fe5"
language_extension = "rs"

View File

@@ -1,3 +0,0 @@
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.

View File

@@ -1 +0,0 @@
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.

View File

@@ -1,4 +0,0 @@
url = "https://github.com/firecracker-microvm/firecracker.git"
revision = "5eaa6e08e350cd38c8102848913a096312e59097"
language_extension = "rs"
allow_preexisting_diagnostics = true

View File

@@ -1,5 +0,0 @@
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 devices 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.

View File

@@ -1 +0,0 @@
Refactor the virtio block devices 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.

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