Compare commits
63 Commits
debugger-e
...
agent-docs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e809aa190 | ||
|
|
6866528943 | ||
|
|
d156fd4c09 | ||
|
|
f6774ae60d | ||
|
|
92e810bfec | ||
|
|
724c935196 | ||
|
|
ef54b58346 | ||
|
|
01bdd170ec | ||
|
|
4b9f4feff1 | ||
|
|
19fb1e1b0d | ||
|
|
f2cb6d69d5 | ||
|
|
822b6f837d | ||
|
|
09db31288a | ||
|
|
a320d324f1 | ||
|
|
266c41ed9a | ||
|
|
4f4bbf264f | ||
|
|
990ca48744 | ||
|
|
f69aeb6311 | ||
|
|
d5f3fbdc88 | ||
|
|
76a78b550b | ||
|
|
e515b2c714 | ||
|
|
55ea481707 | ||
|
|
5e31d86f1f | ||
|
|
4a8f114528 | ||
|
|
ce1a674eba | ||
|
|
0d3fe474db | ||
|
|
6a009b447a | ||
|
|
75ab8ff9a1 | ||
|
|
3705986fac | ||
|
|
aefb3aa2fa | ||
|
|
8e7c145f20 | ||
|
|
a2a502f026 | ||
|
|
c231c95521 | ||
|
|
fcc6a86c90 | ||
|
|
338a6a3b7e | ||
|
|
a0eaede13d | ||
|
|
abf2b9d7d3 | ||
|
|
a50fbc9b5c | ||
|
|
9bbc2e0fb2 | ||
|
|
6caf34ab7e | ||
|
|
8607c7d3ee | ||
|
|
e26bb05567 | ||
|
|
b3b89c8443 | ||
|
|
962b024248 | ||
|
|
833653a3ea | ||
|
|
886f0b7214 | ||
|
|
207fb04969 | ||
|
|
36d02de784 | ||
|
|
36da97935a | ||
|
|
19b547565d | ||
|
|
109f1d43fc | ||
|
|
a5852d4537 | ||
|
|
10ded0ab75 | ||
|
|
b0b620af56 | ||
|
|
eca6d5a04e | ||
|
|
3357736aea | ||
|
|
458ffaa134 | ||
|
|
b14356d1d3 | ||
|
|
19ef56ba7c | ||
|
|
dfbd132d9f | ||
|
|
2e8ee9b64f | ||
|
|
c15382c4d8 | ||
|
|
70c51b513b |
71
.github/workflows/eval.yml
vendored
Normal file
71
.github/workflows/eval.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Run Agent Eval
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_EVAL_TELEMETRY: 1
|
||||
|
||||
jobs:
|
||||
run_eval:
|
||||
timeout-minutes: 60
|
||||
name: Run Agent Eval
|
||||
if: >
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-provider: "buildjet"
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: ./script/linux
|
||||
|
||||
- name: Configure CI
|
||||
run: |
|
||||
mkdir -p ./../.cargo
|
||||
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
||||
|
||||
- name: Compile eval
|
||||
run: cargo build --package=eval
|
||||
|
||||
- name: Run eval
|
||||
run: cargo run --package=eval -- --repetitions=3 --concurrency=1
|
||||
|
||||
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
|
||||
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
|
||||
# to clean up the config file, I’ve included the cleanup code here as a precaution.
|
||||
# While it’s not strictly necessary at this moment, I believe it’s better to err on the side of caution.
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
28
.github/workflows/run_agent_eval_daily.yml
vendored
28
.github/workflows/run_agent_eval_daily.yml
vendored
@@ -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
|
||||
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -703,9 +703,10 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"component",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"html_to_markdown",
|
||||
@@ -3042,6 +3043,7 @@ dependencies = [
|
||||
"strum 0.27.1",
|
||||
"subtle",
|
||||
"supermaven_api",
|
||||
"task",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"theme",
|
||||
@@ -4189,6 +4191,7 @@ dependencies = [
|
||||
"command_palette_hooks",
|
||||
"dap",
|
||||
"db",
|
||||
"debugger_tools",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
@@ -4198,6 +4201,7 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"parking_lot",
|
||||
"picker",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
@@ -4892,6 +4896,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
"assistant_tools",
|
||||
"async-trait",
|
||||
"async-watch",
|
||||
"chrono",
|
||||
"clap",
|
||||
@@ -4915,9 +4920,11 @@ dependencies = [
|
||||
"paths",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"shellexpand 2.1.2",
|
||||
"smol",
|
||||
@@ -14227,6 +14234,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"hex",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"proto",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -16624,7 +16632,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
@@ -18202,7 +18209,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.184.0"
|
||||
version = "0.185.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -18393,9 +18400,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad17428120f5ca776dc5195e2411a282f5150a26d5536671f8943c622c31274f"
|
||||
checksum = "3c1666cd923c5eb4635f3743e69c6920d0ed71f29b26920616a5d220607df7c4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
@@ -605,7 +605,7 @@ wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.6.1"
|
||||
zed_llm_client = "0.7.0"
|
||||
zstd = "0.11"
|
||||
metal = "0.29"
|
||||
|
||||
|
||||
1
assets/icons/image.svg
Normal file
1
assets/icons/image.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-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 |
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -6,7 +6,9 @@ use crate::thread::{
|
||||
};
|
||||
use crate::thread_store::{RulesLoadingError, ThreadStore};
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse};
|
||||
use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
|
||||
use crate::ui::{
|
||||
AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
|
||||
};
|
||||
use crate::{AssistantPanel, OpenActiveThreadAsMarkdown};
|
||||
use anyhow::Context as _;
|
||||
use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
|
||||
@@ -670,6 +672,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 +1032,7 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
ThreadEvent::CheckpointChanged => cx.notify(),
|
||||
ThreadEvent::ReceivedTextChunk => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1072,9 +1095,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()
|
||||
{
|
||||
@@ -1471,45 +1506,8 @@ impl ActiveThread {
|
||||
|
||||
let needs_confirmation = tool_uses.iter().any(|tool_use| tool_use.needs_confirmation);
|
||||
|
||||
let generating_label = (is_generating && is_last_message).then(|| {
|
||||
Label::new("Generating")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.with_animations(
|
||||
"generating-label",
|
||||
vec![
|
||||
Animation::new(Duration::from_secs(1)),
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
],
|
||||
|mut label, animation_ix, delta| {
|
||||
match animation_ix {
|
||||
0 => {
|
||||
let chars_to_show = (delta * 10.).ceil() as usize;
|
||||
let text = &"Generating"[0..chars_to_show];
|
||||
label.set_text(text);
|
||||
}
|
||||
1 => {
|
||||
let text = match delta {
|
||||
d if d < 0.25 => "Generating",
|
||||
d if d < 0.5 => "Generating.",
|
||||
d if d < 0.75 => "Generating..",
|
||||
_ => "Generating...",
|
||||
};
|
||||
label.set_text(text);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
label
|
||||
},
|
||||
)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.map_element(|label| label.alpha(delta)),
|
||||
)
|
||||
});
|
||||
let generating_label = (is_generating && is_last_message)
|
||||
.then(|| AnimatedLabel::new("Generating").size(LabelSize::Small));
|
||||
|
||||
// Don't render user messages that are just there for returning tool results.
|
||||
if message.role == Role::User && thread.message_has_tool_results(message_id) {
|
||||
@@ -1536,9 +1534,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)
|
||||
@@ -1665,7 +1661,7 @@ impl ActiveThread {
|
||||
let message_content =
|
||||
has_content.then(|| {
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.gap_1()
|
||||
.when(!message_is_empty, |parent| {
|
||||
parent.child(
|
||||
if let Some(edit_message_editor) = edit_message_editor.clone() {
|
||||
@@ -1688,7 +1684,6 @@ impl ActiveThread {
|
||||
.on_action(cx.listener(Self::cancel_editing_message))
|
||||
.on_action(cx.listener(Self::confirm_editing_message))
|
||||
.min_h_6()
|
||||
.pt_1()
|
||||
.child(EditorElement::new(
|
||||
&edit_message_editor,
|
||||
EditorStyle {
|
||||
@@ -1703,7 +1698,6 @@ impl ActiveThread {
|
||||
} else {
|
||||
div()
|
||||
.min_h_6()
|
||||
.text_ui(cx)
|
||||
.child(self.render_message_content(
|
||||
message_id,
|
||||
rendered_message,
|
||||
@@ -1762,35 +1756,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().p_2().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 +1820,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 +1842,7 @@ impl ActiveThread {
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(div().p_2().children(message_content)),
|
||||
),
|
||||
),
|
||||
Role::Assistant => v_flex()
|
||||
.id(("message-container", ix))
|
||||
@@ -2101,11 +2081,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 +2108,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({
|
||||
@@ -2268,34 +2268,7 @@ impl ActiveThread {
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child({
|
||||
Label::new("Thinking")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"generating-label",
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
|mut label, delta| {
|
||||
let text = match delta {
|
||||
d if d < 0.25 => "Thinking",
|
||||
d if d < 0.5 => "Thinking.",
|
||||
d if d < 0.75 => "Thinking..",
|
||||
_ => "Thinking...",
|
||||
};
|
||||
label.set_text(text);
|
||||
label
|
||||
},
|
||||
)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| {
|
||||
label.map_element(|label| label.alpha(delta))
|
||||
},
|
||||
)
|
||||
}),
|
||||
.child(AnimatedLabel::new("Thinking").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -2493,7 +2466,7 @@ impl ActiveThread {
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).app_state().fs.clone());
|
||||
let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
|
||||
let edit_tools = tool_use.needs_confirmation;
|
||||
let needs_confirmation_tools = tool_use.needs_confirmation;
|
||||
|
||||
let status_icons = div().child(match &tool_use.status {
|
||||
ToolUseStatus::NeedsConfirmation => {
|
||||
@@ -2591,33 +2564,33 @@ impl ActiveThread {
|
||||
)),
|
||||
),
|
||||
ToolUseStatus::InputStillStreaming | ToolUseStatus::Running => container.child(
|
||||
results_content_container().child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.pb_1()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(
|
||||
delta,
|
||||
)))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Running…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(
|
||||
delta,
|
||||
)))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Running…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Error(_) => container.child(
|
||||
results_content_container()
|
||||
@@ -2681,8 +2654,8 @@ impl ActiveThread {
|
||||
))
|
||||
};
|
||||
|
||||
v_flex().gap_1().mb_3().map(|element| {
|
||||
if !edit_tools {
|
||||
v_flex().gap_1().mb_2().map(|element| {
|
||||
if !needs_confirmation_tools {
|
||||
element.child(
|
||||
v_flex()
|
||||
.child(
|
||||
@@ -2860,30 +2833,7 @@ impl ActiveThread {
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.rounded_b_lg()
|
||||
.child(
|
||||
Label::new("Waiting for Confirmation…")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"generating-label",
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
|mut label, delta| {
|
||||
let text = match delta {
|
||||
d if d < 0.25 => "Waiting for Confirmation",
|
||||
d if d < 0.5 => "Waiting for Confirmation.",
|
||||
d if d < 0.75 => "Waiting for Confirmation..",
|
||||
_ => "Waiting for Confirmation...",
|
||||
};
|
||||
label.set_text(text);
|
||||
label
|
||||
},
|
||||
)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.map_element(|label| label.alpha(delta)),
|
||||
),
|
||||
AnimatedLabel::new("Waiting for Confirmation").size(LabelSize::Small)
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -3282,12 +3232,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 +3258,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 +3293,7 @@ pub(crate) fn open_context(
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
AssistantContext::Image(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Keep, KeepAll, Reject, RejectAll, Thread, ThreadEvent};
|
||||
use crate::{Keep, KeepAll, Reject, RejectAll, Thread, ThreadEvent, ui::AnimatedLabel};
|
||||
use anyhow::Result;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -8,8 +8,8 @@ use editor::{
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
|
||||
Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
Action, AnyElement, AnyView, App, Empty, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
};
|
||||
use language::{Capability, DiskState, OffsetRangeExt, Point};
|
||||
use multi_buffer::PathKey;
|
||||
@@ -307,6 +307,10 @@ impl AgentDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.thread.read(cx).is_generating() {
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||
let diff_hunks_in_ranges = self
|
||||
.editor
|
||||
@@ -339,6 +343,10 @@ impl AgentDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.thread.read(cx).is_generating() {
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||
let diff_hunks_in_ranges = self
|
||||
.editor
|
||||
@@ -650,6 +658,11 @@ fn render_diff_hunk_controls(
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let editor = editor.clone();
|
||||
|
||||
if agent_diff.read(cx).thread.read(cx).is_generating() {
|
||||
return Empty.into_any();
|
||||
}
|
||||
|
||||
h_flex()
|
||||
.h(line_height)
|
||||
.mr_0p5()
|
||||
@@ -857,8 +870,14 @@ impl Render for AgentDiffToolbar {
|
||||
None => return div(),
|
||||
};
|
||||
|
||||
let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();
|
||||
let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
|
||||
if is_generating {
|
||||
return div()
|
||||
.w(rems(6.5625)) // Arbitrary 105px size—so the label doesn't dance around
|
||||
.child(AnimatedLabel::new("Generating"));
|
||||
}
|
||||
|
||||
let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();
|
||||
if is_empty {
|
||||
return div();
|
||||
}
|
||||
@@ -969,7 +988,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|_, cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit(
|
||||
|
||||
@@ -1550,7 +1550,7 @@ impl AssistantPanel {
|
||||
fn render_usage_banner(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let usage = self.thread.read(cx).last_usage()?;
|
||||
|
||||
Some(UsageBanner::new(zed_llm_client::Plan::ZedProTrial, usage.amount).into_any_element())
|
||||
Some(UsageBanner::new(zed_llm_client::Plan::ZedProTrial, usage).into_any_element())
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
@@ -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);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
})?))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1328,7 +1328,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<InlineAssist>(
|
||||
row_range,
|
||||
cx.theme().status().info_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1393,7 +1393,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
cx.theme().status().deleted_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::context::{AssistantContext, format_context_as_string};
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashSet;
|
||||
use editor::actions::MoveUp;
|
||||
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,8 +377,38 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let images = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| {
|
||||
item.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::Image(image) = entry {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if images.is_empty() {
|
||||
return;
|
||||
}
|
||||
cx.stop_propagation();
|
||||
|
||||
self.context_store.update(cx, |store, cx| {
|
||||
for image in images {
|
||||
store.add_image(Arc::new(image), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.edits_expanded = true;
|
||||
AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_file_click(
|
||||
@@ -445,6 +482,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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -317,6 +315,7 @@ pub struct Thread {
|
||||
request_callback: Option<
|
||||
Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>,
|
||||
>,
|
||||
remaining_turns: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -370,6 +369,7 @@ impl Thread {
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,6 +418,7 @@ impl Thread {
|
||||
})
|
||||
.collect(),
|
||||
context: message.context,
|
||||
images: Vec::new(),
|
||||
})
|
||||
.collect(),
|
||||
next_message_id,
|
||||
@@ -443,6 +444,7 @@ impl Thread {
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,7 +525,7 @@ impl Thread {
|
||||
self.messages.iter().find(|message| message.id == id)
|
||||
}
|
||||
|
||||
pub fn messages(&self) -> impl Iterator<Item = &Message> {
|
||||
pub fn messages(&self) -> impl ExactSizeIterator<Item = &Message> {
|
||||
self.messages.iter()
|
||||
}
|
||||
|
||||
@@ -617,24 +619,12 @@ impl Thread {
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
if equal {
|
||||
git_store
|
||||
.update(cx, |store, cx| {
|
||||
store.delete_checkpoint(pending_checkpoint.git_checkpoint, cx)
|
||||
})?
|
||||
.detach();
|
||||
} else {
|
||||
if !equal {
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_checkpoint(pending_checkpoint, cx)
|
||||
})?;
|
||||
}
|
||||
|
||||
git_store
|
||||
.update(cx, |store, cx| {
|
||||
store.delete_checkpoint(final_checkpoint, cx)
|
||||
})?
|
||||
.detach();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => this.update(cx, |this, cx| {
|
||||
@@ -750,33 +740,41 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(message) = self.messages.iter_mut().find(|m| m.id == message_id) {
|
||||
message.images = new_context
|
||||
.iter()
|
||||
.filter_map(|context| {
|
||||
if let AssistantContext::Image(image_context) = context {
|
||||
image_context.image_task.clone().now_or_never().flatten()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
self.action_log.update(cx, |log, cx| {
|
||||
// Track all buffers added as context
|
||||
for ctx in &new_context {
|
||||
match ctx {
|
||||
AssistantContext::File(file_ctx) => {
|
||||
log.buffer_added_as_context(file_ctx.context_buffer.buffer.clone(), cx);
|
||||
log.track_buffer(file_ctx.context_buffer.buffer.clone(), cx);
|
||||
}
|
||||
AssistantContext::Directory(dir_ctx) => {
|
||||
for context_buffer in &dir_ctx.context_buffers {
|
||||
log.buffer_added_as_context(context_buffer.buffer.clone(), cx);
|
||||
log.track_buffer(context_buffer.buffer.clone(), cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Symbol(symbol_ctx) => {
|
||||
log.buffer_added_as_context(
|
||||
symbol_ctx.context_symbol.buffer.clone(),
|
||||
cx,
|
||||
);
|
||||
log.track_buffer(symbol_ctx.context_symbol.buffer.clone(), cx);
|
||||
}
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
log.buffer_added_as_context(
|
||||
excerpt_context.context_buffer.buffer.clone(),
|
||||
cx,
|
||||
);
|
||||
AssistantContext::Selection(selection_context) => {
|
||||
log.track_buffer(selection_context.context_buffer.buffer.clone(), cx);
|
||||
}
|
||||
AssistantContext::FetchedUrl(_)
|
||||
| AssistantContext::Thread(_)
|
||||
| AssistantContext::Rules(_) => {}
|
||||
| AssistantContext::Rules(_)
|
||||
| AssistantContext::Image(_) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -817,6 +815,7 @@ impl Thread {
|
||||
role,
|
||||
segments,
|
||||
context: String::new(),
|
||||
images: Vec::new(),
|
||||
});
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageAdded(id));
|
||||
@@ -944,7 +943,21 @@ impl Thread {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remaining_turns(&self) -> u32 {
|
||||
self.remaining_turns
|
||||
}
|
||||
|
||||
pub fn set_remaining_turns(&mut self, remaining_turns: u32) {
|
||||
self.remaining_turns = remaining_turns;
|
||||
}
|
||||
|
||||
pub fn send_to_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
||||
if self.remaining_turns == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.remaining_turns -= 1;
|
||||
|
||||
let mut request = self.to_completion_request(cx);
|
||||
if model.supports_tools() {
|
||||
request.tools = {
|
||||
@@ -1040,6 +1053,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 +1262,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 +1812,7 @@ impl Thread {
|
||||
thread_data,
|
||||
final_project_snapshot
|
||||
);
|
||||
client.telemetry().flush_events();
|
||||
client.telemetry().flush_events().await;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -1828,7 +1857,7 @@ impl Thread {
|
||||
thread_data,
|
||||
final_project_snapshot
|
||||
);
|
||||
client.telemetry().flush_events();
|
||||
client.telemetry().flush_events().await;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -2084,7 +2113,7 @@ impl Thread {
|
||||
github_login = github_login
|
||||
);
|
||||
|
||||
client.telemetry().flush_events();
|
||||
client.telemetry().flush_events().await;
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2202,6 +2231,7 @@ pub enum ThreadEvent {
|
||||
ShowError(ThreadError),
|
||||
UsageUpdated(RequestUsage),
|
||||
StreamedCompletion,
|
||||
ReceivedTextChunk,
|
||||
StreamedAssistantText(MessageId, String),
|
||||
StreamedAssistantThinking(MessageId, String),
|
||||
StreamedToolUse {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
mod agent_notification;
|
||||
mod animated_label;
|
||||
mod context_pill;
|
||||
mod usage_banner;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use animated_label::*;
|
||||
pub use context_pill::*;
|
||||
pub use usage_banner::*;
|
||||
|
||||
@@ -12,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()),
|
||||
),
|
||||
|
||||
116
crates/agent/src/ui/animated_label.rs
Normal file
116
crates/agent/src/ui/animated_label.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use gpui::{Animation, AnimationExt, FontWeight, pulsating_between};
|
||||
use std::time::Duration;
|
||||
use ui::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct AnimatedLabel {
|
||||
base: Label,
|
||||
text: SharedString,
|
||||
}
|
||||
|
||||
impl AnimatedLabel {
|
||||
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||
let text = text.into();
|
||||
AnimatedLabel {
|
||||
base: Label::new(text.clone()),
|
||||
text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for AnimatedLabel {
|
||||
fn size(mut self, size: LabelSize) -> Self {
|
||||
self.base = self.base.size(size);
|
||||
self
|
||||
}
|
||||
|
||||
fn weight(mut self, weight: FontWeight) -> Self {
|
||||
self.base = self.base.weight(weight);
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
|
||||
self.base = self.base.line_height_style(line_height_style);
|
||||
self
|
||||
}
|
||||
|
||||
fn color(mut self, color: Color) -> Self {
|
||||
self.base = self.base.color(color);
|
||||
self
|
||||
}
|
||||
|
||||
fn strikethrough(mut self) -> Self {
|
||||
self.base = self.base.strikethrough();
|
||||
self
|
||||
}
|
||||
|
||||
fn italic(mut self) -> Self {
|
||||
self.base = self.base.italic();
|
||||
self
|
||||
}
|
||||
|
||||
fn alpha(mut self, alpha: f32) -> Self {
|
||||
self.base = self.base.alpha(alpha);
|
||||
self
|
||||
}
|
||||
|
||||
fn underline(mut self) -> Self {
|
||||
self.base = self.base.underline();
|
||||
self
|
||||
}
|
||||
|
||||
fn truncate(mut self) -> Self {
|
||||
self.base = self.base.truncate();
|
||||
self
|
||||
}
|
||||
|
||||
fn single_line(mut self) -> Self {
|
||||
self.base = self.base.single_line();
|
||||
self
|
||||
}
|
||||
|
||||
fn buffer_font(mut self, cx: &App) -> Self {
|
||||
self.base = self.base.buffer_font(cx);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AnimatedLabel {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let text = self.text.clone();
|
||||
|
||||
self.base
|
||||
.color(Color::Muted)
|
||||
.with_animations(
|
||||
"animated-label",
|
||||
vec![
|
||||
Animation::new(Duration::from_secs(1)),
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
],
|
||||
move |mut label, animation_ix, delta| {
|
||||
match animation_ix {
|
||||
0 => {
|
||||
let chars_to_show = (delta * text.len() as f32).ceil() as usize;
|
||||
let text = SharedString::from(text[0..chars_to_show].to_string());
|
||||
label.set_text(text);
|
||||
}
|
||||
1 => match delta {
|
||||
d if d < 0.25 => label.set_text(text.clone()),
|
||||
d if d < 0.5 => label.set_text(format!("{}.", text)),
|
||||
d if d < 0.75 => label.set_text(format!("{}..", text)),
|
||||
_ => label.set_text(format!("{}...", text)),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
label
|
||||
},
|
||||
)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.map_element(|label| label.alpha(delta)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
use client::zed_urls;
|
||||
use language_model::RequestUsage;
|
||||
use ui::{Banner, ProgressBar, Severity, prelude::*};
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct UsageBanner {
|
||||
plan: Plan,
|
||||
requests: i32,
|
||||
usage: RequestUsage,
|
||||
}
|
||||
|
||||
impl UsageBanner {
|
||||
pub fn new(plan: Plan, requests: i32) -> Self {
|
||||
Self { plan, requests }
|
||||
pub fn new(plan: Plan, usage: RequestUsage) -> Self {
|
||||
Self { plan, usage }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for UsageBanner {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let request_limit = self.plan.model_requests_limit();
|
||||
|
||||
let used_percentage = match request_limit {
|
||||
UsageLimit::Limited(limit) => Some((self.requests as f32 / limit as f32) * 100.),
|
||||
let used_percentage = match self.usage.limit {
|
||||
UsageLimit::Limited(limit) => Some((self.usage.amount as f32 / limit as f32) * 100.),
|
||||
UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
let (severity, message) = match request_limit {
|
||||
let (severity, message) = match self.usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
if self.requests >= limit {
|
||||
if self.usage.amount >= limit {
|
||||
let message = match self.plan {
|
||||
Plan::ZedPro => "Monthly request limit reached",
|
||||
Plan::ZedProTrial => "Trial request limit reached",
|
||||
@@ -33,7 +32,7 @@ impl RenderOnce for UsageBanner {
|
||||
};
|
||||
|
||||
(Severity::Error, message)
|
||||
} else if (self.requests as f32 / limit as f32) >= 0.9 {
|
||||
} else if (self.usage.amount as f32 / limit as f32) >= 0.9 {
|
||||
(Severity::Warning, "Approaching request limit")
|
||||
} else {
|
||||
let message = match self.plan {
|
||||
@@ -81,11 +80,11 @@ impl RenderOnce for UsageBanner {
|
||||
.child(ProgressBar::new("usage", percent, 100., cx))
|
||||
}))
|
||||
.child(
|
||||
Label::new(match request_limit {
|
||||
Label::new(match self.usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
format!("{} / {limit}", self.requests)
|
||||
format!("{} / {limit}", self.usage.amount)
|
||||
}
|
||||
UsageLimit::Unlimited => format!("{} / ∞", self.requests),
|
||||
UsageLimit::Unlimited => format!("{} / ∞", self.usage.amount),
|
||||
})
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
@@ -104,74 +103,131 @@ impl Component for UsageBanner {
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
let trial_limit = Plan::ZedProTrial.model_requests_limit();
|
||||
let trial_examples = vec![
|
||||
single_example(
|
||||
"Zed Pro Trial - New User",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 10))
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedProTrial,
|
||||
RequestUsage {
|
||||
limit: trial_limit,
|
||||
amount: 10,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 135))
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedProTrial,
|
||||
RequestUsage {
|
||||
limit: trial_limit,
|
||||
amount: 135,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 150))
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedProTrial,
|
||||
RequestUsage {
|
||||
limit: trial_limit,
|
||||
amount: 150,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let free_limit = Plan::Free.model_requests_limit();
|
||||
let free_examples = vec![
|
||||
single_example(
|
||||
"Free - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::Free, 25))
|
||||
.child(UsageBanner::new(
|
||||
Plan::Free,
|
||||
RequestUsage {
|
||||
limit: free_limit,
|
||||
amount: 25,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::Free, 45))
|
||||
.child(UsageBanner::new(
|
||||
Plan::Free,
|
||||
RequestUsage {
|
||||
limit: free_limit,
|
||||
amount: 45,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::Free, 50))
|
||||
.child(UsageBanner::new(
|
||||
Plan::Free,
|
||||
RequestUsage {
|
||||
limit: free_limit,
|
||||
amount: 50,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let zed_pro_limit = Plan::ZedPro.model_requests_limit();
|
||||
let zed_pro_examples = vec![
|
||||
single_example(
|
||||
"Zed Pro - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedPro, 250))
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedPro,
|
||||
RequestUsage {
|
||||
limit: zed_pro_limit,
|
||||
amount: 250,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedPro, 450))
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedPro,
|
||||
RequestUsage {
|
||||
limit: zed_pro_limit,
|
||||
amount: 450,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedPro, 500))
|
||||
.child(UsageBanner::new(
|
||||
Plan::ZedPro,
|
||||
RequestUsage {
|
||||
limit: zed_pro_limit,
|
||||
amount: 500,
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -23,7 +23,6 @@ use gpui::{
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
@@ -489,8 +488,8 @@ impl AssistantPanel {
|
||||
|
||||
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
|
||||
// the provider, we want to show a nudge to sign in.
|
||||
let show_zed_ai_notice = client_status.is_signed_out()
|
||||
&& model.map_or(true, |model| model.provider.id().0 == ZED_CLOUD_PROVIDER_ID);
|
||||
let show_zed_ai_notice =
|
||||
client_status.is_signed_out() && model.map_or(true, |model| model.is_provided_by_zed());
|
||||
|
||||
self.show_zed_ai_notice = show_zed_ai_notice;
|
||||
cx.notify();
|
||||
|
||||
@@ -1226,7 +1226,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<InlineAssist>(
|
||||
row_range,
|
||||
cx.theme().status().info_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1291,7 +1291,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
cx.theme().status().deleted_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -39,10 +39,9 @@ impl ActionLog {
|
||||
self.edited_since_project_diagnostics_check
|
||||
}
|
||||
|
||||
fn track_buffer(
|
||||
fn track_buffer_internal(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
created: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> &mut TrackedBuffer {
|
||||
let tracked_buffer = self
|
||||
@@ -59,7 +58,11 @@ impl ActionLog {
|
||||
let base_text;
|
||||
let status;
|
||||
let unreviewed_changes;
|
||||
if created {
|
||||
if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map_or(true, |file| !file.disk_state().exists())
|
||||
{
|
||||
base_text = Rope::default();
|
||||
status = TrackedBufferStatus::Created;
|
||||
unreviewed_changes = Patch::new(vec![Edit {
|
||||
@@ -146,7 +149,7 @@ impl ActionLog {
|
||||
// resurrected externally, we want to clear the changes we
|
||||
// were tracking and reset the buffer's state.
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.track_buffer(buffer, false, cx);
|
||||
self.track_buffer_internal(buffer, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -260,26 +263,15 @@ impl ActionLog {
|
||||
}
|
||||
|
||||
/// Track a buffer as read, so we can notify the model about user edits.
|
||||
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.track_buffer(buffer, false, cx);
|
||||
}
|
||||
|
||||
/// Track a buffer that was added as context, so we can notify the model about user edits.
|
||||
pub fn buffer_added_as_context(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.track_buffer(buffer, false, cx);
|
||||
}
|
||||
|
||||
/// Track a buffer as read, so we can notify the model about user edits.
|
||||
pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.track_buffer(buffer.clone(), true, cx);
|
||||
self.buffer_edited(buffer, cx)
|
||||
pub fn track_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.track_buffer_internal(buffer, cx);
|
||||
}
|
||||
|
||||
/// Mark a buffer as edited, so we can refresh it in the context
|
||||
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.edited_since_project_diagnostics_check = true;
|
||||
|
||||
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
|
||||
let tracked_buffer = self.track_buffer_internal(buffer.clone(), cx);
|
||||
if let TrackedBufferStatus::Deleted = tracked_buffer.status {
|
||||
tracked_buffer.status = TrackedBufferStatus::Modified;
|
||||
}
|
||||
@@ -287,7 +279,7 @@ impl ActionLog {
|
||||
}
|
||||
|
||||
pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
|
||||
let tracked_buffer = self.track_buffer_internal(buffer.clone(), cx);
|
||||
match tracked_buffer.status {
|
||||
TrackedBufferStatus::Created => {
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
@@ -397,7 +389,7 @@ impl ActionLog {
|
||||
|
||||
// Clear all tracked changes for this buffer and start over as if we just read it.
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.track_buffer(buffer.clone(), false, cx);
|
||||
self.track_buffer_internal(buffer.clone(), cx);
|
||||
cx.notify();
|
||||
save
|
||||
}
|
||||
@@ -695,12 +687,20 @@ mod tests {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
|
||||
@@ -765,12 +765,23 @@ mod tests {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
fs.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({"file": "abc\ndef\nghi\njkl\nmno\npqr"}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
|
||||
@@ -839,12 +850,20 @@ mod tests {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
|
||||
@@ -928,25 +947,21 @@ mod tests {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/dir"), json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/dir"), json!({})).await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
|
||||
.unwrap();
|
||||
|
||||
// Simulate file2 being recreated by a tool.
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
|
||||
action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
@@ -1067,8 +1082,9 @@ mod tests {
|
||||
.update(cx, |project, cx| project.open_buffer(file2_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer2.clone(), cx));
|
||||
buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
|
||||
action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx));
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
|
||||
.await
|
||||
@@ -1113,7 +1129,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
|
||||
@@ -1248,7 +1264,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer
|
||||
.edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
|
||||
@@ -1381,8 +1397,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
|
||||
action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
@@ -1438,7 +1455,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
|
||||
for _ in 0..operations {
|
||||
match rng.gen_range(0..100) {
|
||||
@@ -1490,7 +1507,7 @@ mod tests {
|
||||
log::info!("quiescing...");
|
||||
cx.run_until_parked();
|
||||
action_log.update(cx, |log, cx| {
|
||||
let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
|
||||
let tracked_buffer = log.track_buffer_internal(buffer.clone(), cx);
|
||||
let mut old_text = tracked_buffer.base_text.clone();
|
||||
let new_text = buffer.read(cx).as_rope();
|
||||
for edit in tracked_buffer.unreviewed_changes.edits() {
|
||||
|
||||
@@ -17,7 +17,6 @@ assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
@@ -41,6 +40,8 @@ worktree.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -22,15 +22,16 @@ mod schema;
|
||||
mod symbol_info_tool;
|
||||
mod terminal_tool;
|
||||
mod thinking_tool;
|
||||
mod ui;
|
||||
mod web_search_tool;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use copy_path_tool::CopyPathTool;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::App;
|
||||
use http_client::HttpClientWithUrl;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use move_path_tool::MovePathTool;
|
||||
use web_search_tool::WebSearchTool;
|
||||
|
||||
@@ -55,6 +56,8 @@ use crate::symbol_info_tool::SymbolInfoTool;
|
||||
use crate::terminal_tool::TerminalTool;
|
||||
use crate::thinking_tool::ThinkingTool;
|
||||
|
||||
pub use path_search_tool::PathSearchToolInput;
|
||||
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
|
||||
@@ -82,34 +85,45 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(ThinkingTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
|
||||
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
|
||||
move |is_enabled, cx| {
|
||||
if is_enabled {
|
||||
ToolRegistry::global(cx).register_tool(WebSearchTool);
|
||||
} else {
|
||||
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
|
||||
cx.subscribe(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
move |registry, event, cx| match event {
|
||||
language_model::Event::DefaultModelChanged => {
|
||||
let using_zed_provider = registry
|
||||
.read(cx)
|
||||
.default_model()
|
||||
.map_or(false, |default| default.is_provided_by_zed());
|
||||
if using_zed_provider {
|
||||
ToolRegistry::global(cx).register_tool(WebSearchTool);
|
||||
} else {
|
||||
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use client::Client;
|
||||
use clock::FakeSystemClock;
|
||||
use http_client::FakeHttpClient;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_builtin_tool_schema_compatibility(cx: &mut App) {
|
||||
crate::init(
|
||||
Arc::new(http_client::HttpClientWithUrl::new(
|
||||
FakeHttpClient::with_200_response(),
|
||||
"https://zed.dev",
|
||||
None,
|
||||
)),
|
||||
settings::init(cx);
|
||||
|
||||
let client = Client::new(
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_200_response(),
|
||||
cx,
|
||||
);
|
||||
language_model::init(client.clone(), cx);
|
||||
crate::init(client.http_client(), cx);
|
||||
|
||||
for tool in ToolRegistry::global(cx).tools() {
|
||||
let actual_schema = tool
|
||||
|
||||
@@ -159,7 +159,7 @@ impl Tool for CodeActionTool {
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
action_log.track_buffer(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
let range = {
|
||||
|
||||
@@ -174,7 +174,7 @@ pub async fn file_outline(
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
action_log.track_buffer(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
// Wait until the buffer has been fully parsed, so that we can read its outline.
|
||||
|
||||
@@ -209,7 +209,7 @@ impl Tool for ContentsTool {
|
||||
})?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer, cx);
|
||||
log.track_buffer(buffer, cx);
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
@@ -221,7 +221,7 @@ impl Tool for ContentsTool {
|
||||
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer, cx);
|
||||
log.track_buffer(buffer, cx);
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
|
||||
@@ -112,9 +112,12 @@ impl Tool for CreateFileTool {
|
||||
.await
|
||||
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.track_buffer(buffer.clone(), cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx));
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.will_create_buffer(buffer.clone(), cx)
|
||||
action_log.buffer_edited(buffer.clone(), cx)
|
||||
});
|
||||
})?;
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ struct PartialInput {
|
||||
|
||||
pub struct EditFileTool;
|
||||
|
||||
const DEFAULT_UI_TEXT: &str = "Edit file";
|
||||
const DEFAULT_UI_TEXT: &str = "Editing file";
|
||||
|
||||
impl Tool for EditFileTool {
|
||||
fn name(&self) -> String {
|
||||
@@ -87,7 +87,7 @@ impl Tool for EditFileTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<EditFileToolInput>(input.clone()) {
|
||||
Ok(input) => input.display_description,
|
||||
Err(_) => "Edit file".to_string(),
|
||||
Err(_) => "Editing file".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ impl Tool for EditFileTool {
|
||||
|
||||
let snapshot = cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx)
|
||||
log.track_buffer(buffer.clone(), cx)
|
||||
});
|
||||
let snapshot = buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
|
||||
@@ -134,7 +134,7 @@ impl Tool for ReadFileTool {
|
||||
})?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer, cx);
|
||||
log.track_buffer(buffer, cx);
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
@@ -147,7 +147,7 @@ impl Tool for ReadFileTool {
|
||||
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer, cx);
|
||||
log.track_buffer(buffer, cx);
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
|
||||
@@ -106,7 +106,7 @@ impl Tool for RenameTool {
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
action_log.track_buffer(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
let position = {
|
||||
|
||||
@@ -140,7 +140,7 @@ impl Tool for SymbolInfoTool {
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
action_log.track_buffer(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
let position = {
|
||||
|
||||
3
crates/assistant_tools/src/ui.rs
Normal file
3
crates/assistant_tools/src/ui.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod tool_call_card_header;
|
||||
|
||||
pub use tool_call_card_header::*;
|
||||
102
crates/assistant_tools/src/ui/tool_call_card_header.rs
Normal file
102
crates/assistant_tools/src/ui/tool_call_card_header.rs
Normal 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
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(|_| ()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ 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
|
||||
|
||||
@@ -492,7 +492,8 @@ CREATE TABLE IF NOT EXISTS billing_customers (
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER NOT NULL REFERENCES users (id),
|
||||
has_overdue_invoices BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
stripe_customer_id TEXT NOT NULL
|
||||
stripe_customer_id TEXT NOT NULL,
|
||||
trial_started_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "uix_billing_customers_on_user_id" ON billing_customers (user_id);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table billing_customers
|
||||
add column trial_started_at timestamp without time zone;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table project_repositories
|
||||
add column head_commit_details varchar;
|
||||
@@ -287,7 +287,7 @@ async fn create_billing_subscription(
|
||||
}
|
||||
}
|
||||
|
||||
let customer_id = if let Some(existing_customer) = existing_billing_customer {
|
||||
let customer_id = if let Some(existing_customer) = &existing_billing_customer {
|
||||
CustomerId::from_str(&existing_customer.stripe_customer_id)
|
||||
.context("failed to parse customer ID")?
|
||||
} else {
|
||||
@@ -320,6 +320,15 @@ async fn create_billing_subscription(
|
||||
.await?
|
||||
}
|
||||
Some(ProductCode::ZedProTrial) => {
|
||||
if let Some(existing_billing_customer) = &existing_billing_customer {
|
||||
if existing_billing_customer.trial_started_at.is_some() {
|
||||
return Err(Error::http(
|
||||
StatusCode::FORBIDDEN,
|
||||
"user already used free trial".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
stripe_billing
|
||||
.checkout_with_zed_pro_trial(
|
||||
app.config.zed_pro_price_id()?,
|
||||
@@ -817,6 +826,24 @@ async fn handle_customer_subscription_event(
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("billing customer not found"))?;
|
||||
|
||||
if let Some(SubscriptionKind::ZedProTrial) = subscription_kind {
|
||||
if subscription.status == SubscriptionStatus::Trialing {
|
||||
let current_period_start =
|
||||
DateTime::from_timestamp(subscription.current_period_start, 0)
|
||||
.ok_or_else(|| anyhow!("No trial subscription period start"))?;
|
||||
|
||||
app.db
|
||||
.update_billing_customer(
|
||||
billing_customer.id,
|
||||
&UpdateBillingCustomerParams {
|
||||
trial_started_at: ActiveValue::set(Some(current_period_start.naive_utc())),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let was_canceled_due_to_payment_failure = subscription.status == SubscriptionStatus::Canceled
|
||||
&& subscription
|
||||
.cancellation_details
|
||||
@@ -843,6 +870,28 @@ async fn handle_customer_subscription_event(
|
||||
.get_billing_subscription_by_stripe_subscription_id(&subscription.id)
|
||||
.await?
|
||||
{
|
||||
let llm_db = app
|
||||
.llm_db
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("LLM DB not initialized"))?;
|
||||
|
||||
let new_period_start_at =
|
||||
chrono::DateTime::from_timestamp(subscription.current_period_start, 0)
|
||||
.ok_or_else(|| anyhow!("No subscription period start"))?;
|
||||
let new_period_end_at =
|
||||
chrono::DateTime::from_timestamp(subscription.current_period_end, 0)
|
||||
.ok_or_else(|| anyhow!("No subscription period end"))?;
|
||||
|
||||
llm_db
|
||||
.transfer_existing_subscription_usage(
|
||||
billing_customer.user_id,
|
||||
&existing_subscription,
|
||||
subscription_kind,
|
||||
new_period_start_at,
|
||||
new_period_end_at,
|
||||
)
|
||||
.await?;
|
||||
|
||||
app.db
|
||||
.update_billing_subscription(
|
||||
existing_subscription.id,
|
||||
|
||||
@@ -516,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,
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct UpdateBillingCustomerParams {
|
||||
pub user_id: ActiveValue<UserId>,
|
||||
pub stripe_customer_id: ActiveValue<String>,
|
||||
pub has_overdue_invoices: ActiveValue<bool>,
|
||||
pub trial_started_at: ActiveValue<Option<DateTime>>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
@@ -45,7 +46,8 @@ impl Database {
|
||||
user_id: params.user_id.clone(),
|
||||
stripe_customer_id: params.stripe_customer_id.clone(),
|
||||
has_overdue_invoices: params.has_overdue_invoices.clone(),
|
||||
..Default::default()
|
||||
trial_started_at: params.trial_started_at.clone(),
|
||||
created_at: ActiveValue::not_set(),
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct Model {
|
||||
pub user_id: UserId,
|
||||
pub stripe_customer_id: String,
|
||||
pub has_overdue_invoices: bool,
|
||||
pub trial_started_at: Option<DateTime>,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,87 @@
|
||||
use crate::db::UserId;
|
||||
use chrono::Timelike;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::{UserId, billing_subscription};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn convert_chrono_to_time(datetime: DateTimeUtc) -> anyhow::Result<PrimitiveDateTime> {
|
||||
use chrono::{Datelike as _, Timelike as _};
|
||||
|
||||
let date = time::Date::from_calendar_date(
|
||||
datetime.year(),
|
||||
time::Month::try_from(datetime.month() as u8).unwrap(),
|
||||
datetime.day() as u8,
|
||||
)?;
|
||||
|
||||
let time = time::Time::from_hms_nano(
|
||||
datetime.hour() as u8,
|
||||
datetime.minute() as u8,
|
||||
datetime.second() as u8,
|
||||
datetime.nanosecond(),
|
||||
)?;
|
||||
|
||||
Ok(PrimitiveDateTime::new(date, time))
|
||||
}
|
||||
|
||||
impl LlmDatabase {
|
||||
pub async fn create_subscription_usage(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
period_start_at: DateTimeUtc,
|
||||
period_end_at: DateTimeUtc,
|
||||
plan: SubscriptionKind,
|
||||
model_requests: i32,
|
||||
edit_predictions: i32,
|
||||
) -> Result<subscription_usage::Model> {
|
||||
self.transaction(|tx| async move {
|
||||
self.create_subscription_usage_in_tx(
|
||||
user_id,
|
||||
period_start_at,
|
||||
period_end_at,
|
||||
plan,
|
||||
model_requests,
|
||||
edit_predictions,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_subscription_usage_in_tx(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
period_start_at: DateTimeUtc,
|
||||
period_end_at: DateTimeUtc,
|
||||
plan: SubscriptionKind,
|
||||
model_requests: i32,
|
||||
edit_predictions: i32,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<subscription_usage::Model> {
|
||||
// Clear out the nanoseconds so that these timestamps are comparable with Unix timestamps.
|
||||
let period_start_at = period_start_at.with_nanosecond(0).unwrap();
|
||||
let period_end_at = period_end_at.with_nanosecond(0).unwrap();
|
||||
|
||||
let period_start_at = convert_chrono_to_time(period_start_at)?;
|
||||
let period_end_at = convert_chrono_to_time(period_end_at)?;
|
||||
|
||||
Ok(
|
||||
subscription_usage::Entity::insert(subscription_usage::ActiveModel {
|
||||
id: ActiveValue::not_set(),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
period_start_at: ActiveValue::set(period_start_at),
|
||||
period_end_at: ActiveValue::set(period_end_at),
|
||||
plan: ActiveValue::set(plan),
|
||||
model_requests: ActiveValue::set(model_requests),
|
||||
edit_predictions: ActiveValue::set(edit_predictions),
|
||||
})
|
||||
.exec_with_returning(tx)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_subscription_usage_for_period(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
@@ -10,12 +89,77 @@ impl LlmDatabase {
|
||||
period_end_at: DateTimeUtc,
|
||||
) -> Result<Option<subscription_usage::Model>> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(subscription_usage::Entity::find()
|
||||
.filter(subscription_usage::Column::UserId.eq(user_id))
|
||||
.filter(subscription_usage::Column::PeriodStartAt.eq(period_start_at))
|
||||
.filter(subscription_usage::Column::PeriodEndAt.eq(period_end_at))
|
||||
.one(&*tx)
|
||||
.await?)
|
||||
self.get_subscription_usage_for_period_in_tx(
|
||||
user_id,
|
||||
period_start_at,
|
||||
period_end_at,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_subscription_usage_for_period_in_tx(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
period_start_at: DateTimeUtc,
|
||||
period_end_at: DateTimeUtc,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<subscription_usage::Model>> {
|
||||
Ok(subscription_usage::Entity::find()
|
||||
.filter(subscription_usage::Column::UserId.eq(user_id))
|
||||
.filter(subscription_usage::Column::PeriodStartAt.eq(period_start_at))
|
||||
.filter(subscription_usage::Column::PeriodEndAt.eq(period_end_at))
|
||||
.one(tx)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn transfer_existing_subscription_usage(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
existing_subscription: &billing_subscription::Model,
|
||||
new_subscription_kind: Option<SubscriptionKind>,
|
||||
new_period_start_at: DateTimeUtc,
|
||||
new_period_end_at: DateTimeUtc,
|
||||
) -> Result<Option<subscription_usage::Model>> {
|
||||
self.transaction(|tx| async move {
|
||||
match existing_subscription.kind {
|
||||
Some(SubscriptionKind::ZedProTrial) => {
|
||||
let trial_period_start_at = existing_subscription
|
||||
.current_period_start_at()
|
||||
.ok_or_else(|| anyhow!("No trial subscription period start"))?;
|
||||
let trial_period_end_at = existing_subscription
|
||||
.current_period_end_at()
|
||||
.ok_or_else(|| anyhow!("No trial subscription period end"))?;
|
||||
|
||||
let existing_usage = self
|
||||
.get_subscription_usage_for_period_in_tx(
|
||||
user_id,
|
||||
trial_period_start_at,
|
||||
trial_period_end_at,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
if let Some(existing_usage) = existing_usage {
|
||||
return Ok(Some(
|
||||
self.create_subscription_usage_in_tx(
|
||||
user_id,
|
||||
new_period_start_at,
|
||||
new_period_end_at,
|
||||
new_subscription_kind.unwrap_or(existing_usage.plan),
|
||||
existing_usage.model_requests,
|
||||
existing_usage.edit_predictions,
|
||||
&tx,
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod provider_tests;
|
||||
mod subscription_usage_tests;
|
||||
|
||||
use gpui::BackgroundExecutor;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
69
crates/collab/src/llm/db/tests/subscription_usage_tests.rs
Normal file
69
crates/collab/src/llm/db/tests/subscription_usage_tests.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::{UserId, billing_subscription};
|
||||
use crate::llm::db::LlmDatabase;
|
||||
use crate::test_llm_db;
|
||||
|
||||
test_llm_db!(
|
||||
test_transfer_existing_subscription_usage,
|
||||
test_transfer_existing_subscription_usage_postgres
|
||||
);
|
||||
|
||||
async fn test_transfer_existing_subscription_usage(db: &mut LlmDatabase) {
|
||||
let user_id = UserId(1);
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
let trial_period_start_at = now - Duration::days(14);
|
||||
let trial_period_end_at = now;
|
||||
|
||||
let new_period_start_at = now;
|
||||
let new_period_end_at = now + Duration::days(30);
|
||||
|
||||
let existing_subscription = billing_subscription::Model {
|
||||
kind: Some(SubscriptionKind::ZedProTrial),
|
||||
stripe_current_period_start: Some(trial_period_start_at.timestamp()),
|
||||
stripe_current_period_end: Some(trial_period_end_at.timestamp()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let existing_usage = db
|
||||
.create_subscription_usage(
|
||||
user_id,
|
||||
trial_period_start_at,
|
||||
trial_period_end_at,
|
||||
SubscriptionKind::ZedProTrial,
|
||||
25,
|
||||
1_000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let transferred_usage = db
|
||||
.transfer_existing_subscription_usage(
|
||||
user_id,
|
||||
&existing_subscription,
|
||||
Some(SubscriptionKind::ZedPro),
|
||||
new_period_start_at,
|
||||
new_period_end_at,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
transferred_usage.is_some(),
|
||||
"subscription usage not transferred successfully"
|
||||
);
|
||||
let transferred_usage = transferred_usage.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
transferred_usage.model_requests,
|
||||
existing_usage.model_requests
|
||||
);
|
||||
assert_eq!(
|
||||
transferred_usage.edit_predictions,
|
||||
existing_usage.edit_predictions
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::Cents;
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::{billing_subscription, user};
|
||||
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use crate::{Config, db::billing_preference};
|
||||
@@ -32,6 +33,8 @@ pub struct LlmTokenClaims {
|
||||
pub plan: Plan,
|
||||
#[serde(default)]
|
||||
pub subscription_period: Option<(NaiveDateTime, NaiveDateTime)>,
|
||||
#[serde(default)]
|
||||
pub can_use_web_search_tool: bool,
|
||||
}
|
||||
|
||||
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
|
||||
@@ -43,7 +46,6 @@ impl LlmTokenClaims {
|
||||
billing_preferences: Option<billing_preference::Model>,
|
||||
feature_flags: &Vec<String>,
|
||||
has_legacy_llm_subscription: bool,
|
||||
plan: rpc::proto::Plan,
|
||||
subscription: Option<billing_subscription::Model>,
|
||||
system_id: Option<String>,
|
||||
config: &Config,
|
||||
@@ -70,6 +72,7 @@ impl LlmTokenClaims {
|
||||
bypass_account_age_check: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == "bypass-account-age-check"),
|
||||
can_use_web_search_tool: feature_flags.iter().any(|flag| flag == "assistant2"),
|
||||
has_llm_subscription: has_legacy_llm_subscription,
|
||||
max_monthly_spend_in_cents: billing_preferences
|
||||
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {
|
||||
@@ -78,11 +81,14 @@ impl LlmTokenClaims {
|
||||
custom_llm_monthly_allowance_in_cents: user
|
||||
.custom_llm_monthly_allowance_in_cents
|
||||
.map(|allowance| allowance as u32),
|
||||
plan: match plan {
|
||||
rpc::proto::Plan::Free => Plan::Free,
|
||||
rpc::proto::Plan::ZedPro => Plan::ZedPro,
|
||||
rpc::proto::Plan::ZedProTrial => Plan::ZedProTrial,
|
||||
},
|
||||
plan: subscription
|
||||
.as_ref()
|
||||
.and_then(|subscription| subscription.kind)
|
||||
.map_or(Plan::Free, |kind| match kind {
|
||||
SubscriptionKind::ZedFree => Plan::Free,
|
||||
SubscriptionKind::ZedPro => Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
|
||||
}),
|
||||
subscription_period: maybe!({
|
||||
let subscription = subscription?;
|
||||
let period_start_at = subscription.current_period_start_at()?;
|
||||
|
||||
@@ -4147,7 +4147,6 @@ async fn get_llm_api_token(
|
||||
billing_preferences,
|
||||
&flags,
|
||||
has_legacy_llm_subscription,
|
||||
session.current_plan(&db).await?,
|
||||
billing_subscription,
|
||||
session.system_id.clone(),
|
||||
&session.app_state.config,
|
||||
|
||||
@@ -416,9 +416,16 @@ impl StripeBilling {
|
||||
let mut params = stripe::CreateCheckoutSession::new();
|
||||
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
|
||||
trial_period_days: Some(14),
|
||||
trial_settings: Some(stripe::CreateCheckoutSessionSubscriptionDataTrialSettings {
|
||||
end_behavior: stripe::CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior {
|
||||
missing_payment_method: stripe::CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod::Pause,
|
||||
}
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
|
||||
params.payment_method_collection =
|
||||
Some(stripe::CheckoutSessionPaymentMethodCollection::IfRequired);
|
||||
params.customer = Some(customer_id);
|
||||
params.client_reference_id = Some(github_login);
|
||||
params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use call::ActiveCall;
|
||||
use dap::requests::{Initialize, Launch, StackTrace};
|
||||
use dap::DebugRequestType;
|
||||
use dap::{requests::SetBreakpoints, SourceBreakpoint};
|
||||
use dap::requests::{Initialize, Launch, StackTrace};
|
||||
use dap::{SourceBreakpoint, requests::SetBreakpoints};
|
||||
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::{dock::Panel, Workspace};
|
||||
use workspace::{Workspace, dock::Panel};
|
||||
|
||||
use super::{TestClient, TestServer};
|
||||
|
||||
|
||||
@@ -6,17 +6,18 @@ use collab_ui::{
|
||||
channel_view::ChannelView,
|
||||
notifications::project_shared_notification::ProjectSharedNotification,
|
||||
};
|
||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use editor::{Editor, MultiBuffer, PathKey};
|
||||
use gpui::{
|
||||
AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, TestAppContext,
|
||||
VisualTestContext, point,
|
||||
VisualContext, VisualTestContext, point,
|
||||
};
|
||||
use language::Capability;
|
||||
use project::WorktreeSettings;
|
||||
use rpc::proto::PeerId;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
use text::{Point, ToPoint};
|
||||
use util::{path, test::sample_text};
|
||||
use workspace::{SplitDirection, Workspace, item::ItemHandle as _};
|
||||
|
||||
use super::TestClient;
|
||||
@@ -295,8 +296,20 @@ async fn test_basic_following(
|
||||
.unwrap()
|
||||
});
|
||||
let mut result = MultiBuffer::new(Capability::ReadWrite);
|
||||
result.push_excerpts(buffer_a1, [ExcerptRange::new(0..3)], cx);
|
||||
result.push_excerpts(buffer_a2, [ExcerptRange::new(4..7)], cx);
|
||||
result.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer_a1, cx),
|
||||
buffer_a1,
|
||||
[Point::row_range(1..2)],
|
||||
1,
|
||||
cx,
|
||||
);
|
||||
result.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer_a2, cx),
|
||||
buffer_a2,
|
||||
[Point::row_range(5..6)],
|
||||
1,
|
||||
cx,
|
||||
);
|
||||
result
|
||||
});
|
||||
let multibuffer_editor_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
@@ -2070,6 +2083,83 @@ async fn share_workspace(
|
||||
.await
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_test_workspace(cx_a).await;
|
||||
join_channel(channel, &client_a, cx_a).await.unwrap();
|
||||
share_workspace(&workspace, cx_a).await.unwrap();
|
||||
let buffer = workspace.update(cx_a, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.create_local_buffer(&sample_text(26, 5, 'a'), None, cx)
|
||||
})
|
||||
});
|
||||
let multibuffer = cx_a.new(|cx| {
|
||||
let mut mb = MultiBuffer::new(Capability::ReadWrite);
|
||||
mb.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer.clone(),
|
||||
[Point::row_range(1..1), Point::row_range(5..5)],
|
||||
1,
|
||||
cx,
|
||||
);
|
||||
mb
|
||||
});
|
||||
let snapshot = buffer.update(cx_a, |buffer, _| buffer.snapshot());
|
||||
let editor: Entity<Editor> = cx_a.new_window_entity(|window, cx| {
|
||||
Editor::for_multibuffer(
|
||||
multibuffer.clone(),
|
||||
Some(workspace.read(cx).project().clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
workspace.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
|
||||
});
|
||||
editor.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_ranges([Point::row_range(4..4)]);
|
||||
})
|
||||
});
|
||||
let positions = editor.update(cx_a, |editor, _| {
|
||||
editor
|
||||
.selections
|
||||
.disjoint_anchor_ranges()
|
||||
.map(|range| range.start.text_anchor.to_point(&snapshot))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
multibuffer.update(cx_a, |multibuffer, cx| {
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer,
|
||||
[Point::row_range(1..5)],
|
||||
1,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let (workspace_b, cx_b) = client_b.join_workspace(channel, cx_b).await;
|
||||
cx_b.run_until_parked();
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let new_positions = editor_b.update(cx_b, |editor, _| {
|
||||
editor
|
||||
.selections
|
||||
.disjoint_anchor_ranges()
|
||||
.map(|range| range.start.text_anchor.to_point(&snapshot))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(positions, new_positions);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_following_to_channel_notes_other_workspace(
|
||||
cx_a: &mut TestAppContext,
|
||||
|
||||
@@ -2,11 +2,13 @@ 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::{
|
||||
@@ -24,6 +26,7 @@ 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};
|
||||
@@ -576,3 +579,108 @@ 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();
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::ResultExt as _;
|
||||
use util::{ResultExt as _, TryFutureExt};
|
||||
|
||||
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
|
||||
|
||||
@@ -126,6 +126,7 @@ pub(crate) struct TransportDelegate {
|
||||
pending_requests: Requests,
|
||||
transport: Transport,
|
||||
server_tx: Arc<Mutex<Option<Sender<Message>>>>,
|
||||
_tasks: Vec<gpui::Task<Option<()>>>,
|
||||
}
|
||||
|
||||
impl TransportDelegate {
|
||||
@@ -140,6 +141,7 @@ 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))
|
||||
@@ -166,35 +168,43 @@ impl TransportDelegate {
|
||||
|
||||
cx.update(|cx| {
|
||||
if let Some(stdout) = params.stdout.take() {
|
||||
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_adapter_log(stdout, 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);
|
||||
self._tasks.push(
|
||||
cx.background_executor().spawn(
|
||||
Self::handle_output(
|
||||
params.output,
|
||||
client_tx,
|
||||
self.pending_requests.clone(),
|
||||
log_handler.clone(),
|
||||
)
|
||||
.log_err(),
|
||||
),
|
||||
);
|
||||
|
||||
if let Some(stderr) = params.stderr.take() {
|
||||
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_error(stderr, self.log_handlers.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);
|
||||
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(),
|
||||
),
|
||||
);
|
||||
})?;
|
||||
|
||||
{
|
||||
@@ -367,6 +377,7 @@ impl TransportDelegate {
|
||||
where
|
||||
Stderr: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
log::debug!("Handle error started");
|
||||
let mut buffer = String::new();
|
||||
|
||||
let mut reader = BufReader::new(stderr);
|
||||
|
||||
@@ -12,6 +12,9 @@ workspace = true
|
||||
path = "src/debugger_tools.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
dap.workspace = true
|
||||
|
||||
@@ -41,7 +41,7 @@ struct DapLogView {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct LogStore {
|
||||
pub 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 {
|
||||
fn new(cx: &Context<Self>) -> Self {
|
||||
pub 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,3 +845,29 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ test-support = [
|
||||
"project/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
"env_logger",
|
||||
"unindent",
|
||||
"debugger_tools"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -37,6 +40,7 @@ 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
|
||||
@@ -53,9 +57,13 @@ 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"] }
|
||||
|
||||
@@ -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,7 +9,9 @@ use sysinfo::System;
|
||||
use ui::{Context, Tooltip, prelude::*};
|
||||
use ui::{ListItem, ListItemSpacing};
|
||||
use util::debug_panic;
|
||||
use workspace::ModalView;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::debugger_panel::DebugPanel;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct Candidate {
|
||||
@@ -22,19 +24,19 @@ pub(crate) struct AttachModalDelegate {
|
||||
selected_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
placeholder_text: Arc<str>,
|
||||
project: Entity<project::Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
pub(crate) debug_config: task::DebugTaskDefinition,
|
||||
candidates: Arc<[Candidate]>,
|
||||
}
|
||||
|
||||
impl AttachModalDelegate {
|
||||
fn new(
|
||||
project: Entity<project::Project>,
|
||||
workspace: Entity<Workspace>,
|
||||
debug_config: task::DebugTaskDefinition,
|
||||
candidates: Arc<[Candidate]>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project,
|
||||
workspace: workspace.downgrade(),
|
||||
debug_config,
|
||||
candidates,
|
||||
selected_index: 0,
|
||||
@@ -51,7 +53,7 @@ pub struct AttachModal {
|
||||
|
||||
impl AttachModal {
|
||||
pub fn new(
|
||||
project: Entity<project::Project>,
|
||||
workspace: Entity<Workspace>,
|
||||
debug_config: task::DebugTaskDefinition,
|
||||
modal: bool,
|
||||
window: &mut Window,
|
||||
@@ -75,11 +77,11 @@ impl AttachModal {
|
||||
.collect();
|
||||
processes.sort_by_key(|k| k.name.clone());
|
||||
let processes = processes.into_iter().collect();
|
||||
Self::with_processes(project, debug_config, processes, modal, window, cx)
|
||||
Self::with_processes(workspace, debug_config, processes, modal, window, cx)
|
||||
}
|
||||
|
||||
pub(super) fn with_processes(
|
||||
project: Entity<project::Project>,
|
||||
workspace: Entity<Workspace>,
|
||||
debug_config: task::DebugTaskDefinition,
|
||||
processes: Arc<[Candidate]>,
|
||||
modal: bool,
|
||||
@@ -88,7 +90,7 @@ impl AttachModal {
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| {
|
||||
Picker::uniform_list(
|
||||
AttachModalDelegate::new(project, debug_config, processes),
|
||||
AttachModalDelegate::new(workspace, debug_config, processes),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -202,7 +204,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())
|
||||
@@ -225,14 +227,17 @@ impl PickerDelegate for AttachModalDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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,
|
||||
@@ -17,6 +18,7 @@ use gpui::{
|
||||
actions, anchored, deferred,
|
||||
};
|
||||
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::{
|
||||
Project,
|
||||
debugger::{
|
||||
@@ -30,10 +32,9 @@ use settings::Settings;
|
||||
use std::any::TypeId;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use task::DebugTaskDefinition;
|
||||
use task::{DebugTaskDefinition, DebugTaskTemplate};
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
|
||||
use util::debug_panic;
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
@@ -63,7 +64,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: WeakEntity<Project>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
@@ -97,10 +98,10 @@ impl DebugPanel {
|
||||
window,
|
||||
|panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
|
||||
panel.workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let workspace_handle = cx.entity().clone();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
crate::attach_modal::AttachModal::new(
|
||||
project,
|
||||
workspace_handle,
|
||||
event.debug_config.clone(),
|
||||
true,
|
||||
window,
|
||||
@@ -127,7 +128,7 @@ impl DebugPanel {
|
||||
_subscriptions,
|
||||
past_debug_definition: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
project: project.downgrade(),
|
||||
project,
|
||||
workspace: workspace.weak_handle(),
|
||||
context_menu: None,
|
||||
};
|
||||
@@ -219,7 +220,7 @@ impl DebugPanel {
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
cx.spawn(async move |cx| {
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
@@ -245,114 +246,226 @@ 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,
|
||||
@@ -374,6 +487,12 @@ impl DebugPanel {
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
dap_store::DapStoreEvent::SpawnChildSession {
|
||||
request,
|
||||
parent_session,
|
||||
} => {
|
||||
self.start_child_session(request, parent_session.clone(), window, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -408,7 +527,7 @@ impl DebugPanel {
|
||||
cwd,
|
||||
title,
|
||||
},
|
||||
task::RevealStrategy::Always,
|
||||
task::RevealStrategy::Never,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -468,8 +587,6 @@ 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| {
|
||||
@@ -893,7 +1010,6 @@ 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 {
|
||||
@@ -1039,3 +1155,15 @@ 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);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ mod new_session_modal;
|
||||
mod persistence;
|
||||
pub(crate) mod session;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod tests;
|
||||
|
||||
actions!(
|
||||
|
||||
@@ -4,14 +4,12 @@ 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;
|
||||
@@ -21,7 +19,6 @@ 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};
|
||||
@@ -88,11 +85,11 @@ impl NewSessionModal {
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_config(&self, cx: &App) -> Option<DebugTaskDefinition> {
|
||||
fn debug_config(&self, cx: &App, debugger: &str) -> DebugTaskDefinition {
|
||||
let request = self.mode.debug_task(cx);
|
||||
Some(DebugTaskDefinition {
|
||||
adapter: self.debugger.clone()?.to_string(),
|
||||
label: suggested_label(&request, self.debugger.as_deref()?),
|
||||
DebugTaskDefinition {
|
||||
adapter: debugger.to_owned(),
|
||||
label: suggested_label(&request, debugger),
|
||||
request,
|
||||
initialize_args: self.initialize_args.clone(),
|
||||
tcp_connection: None,
|
||||
@@ -100,26 +97,26 @@ impl NewSessionModal {
|
||||
ToggleState::Selected => Some(true),
|
||||
_ => None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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"))?;
|
||||
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();
|
||||
|
||||
let _ = self.debug_panel.update(cx, |panel, _| {
|
||||
panel.past_debug_definition = Some(config.clone());
|
||||
});
|
||||
|
||||
let task_contexts = workspace
|
||||
let task_contexts = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task_context = if let Some(task) = task_contexts {
|
||||
task.await
|
||||
.active_worktree_context
|
||||
@@ -127,9 +124,8 @@ impl NewSessionModal {
|
||||
} else {
|
||||
task::TaskContext::default()
|
||||
};
|
||||
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
|
||||
|
||||
let task = project.update(cx, |this, cx| {
|
||||
debug_panel.update_in(cx, |debug_panel, window, cx| {
|
||||
let template = DebugTaskTemplate {
|
||||
locator: None,
|
||||
definition: config.clone(),
|
||||
@@ -139,23 +135,18 @@ impl NewSessionModal {
|
||||
.resolve_task("debug_task", &task_context)
|
||||
.and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
|
||||
{
|
||||
this.start_debug_session(debug_config.definition, cx)
|
||||
debug_panel.start_session(debug_config.definition, window, cx)
|
||||
} else {
|
||||
this.start_debug_session(config, cx)
|
||||
debug_panel.start_session(config, window, cx)
|
||||
}
|
||||
})?;
|
||||
let spawn_result = task.await;
|
||||
if spawn_result.is_ok() {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
spawn_result?;
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
anyhow::Result::<_, anyhow::Error>::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_attach_picker(
|
||||
@@ -249,15 +240,12 @@ impl NewSessionModal {
|
||||
);
|
||||
}
|
||||
DebugRequest::Attach(_) => {
|
||||
let Ok(project) = this
|
||||
.workspace
|
||||
.read_with(cx, |this, _| this.project().clone())
|
||||
else {
|
||||
let Some(workspace) = this.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
this.mode = NewSessionMode::attach(
|
||||
this.debugger.clone(),
|
||||
project,
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -357,7 +345,7 @@ struct AttachMode {
|
||||
impl AttachMode {
|
||||
fn new(
|
||||
debugger: Option<SharedString>,
|
||||
project: Entity<Project>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> Entity<Self> {
|
||||
@@ -370,7 +358,7 @@ impl AttachMode {
|
||||
stop_on_entry: Some(false),
|
||||
};
|
||||
let attach_picker = cx.new(|cx| {
|
||||
let modal = AttachModal::new(project, debug_definition.clone(), false, window, cx);
|
||||
let modal = AttachModal::new(workspace, debug_definition.clone(), false, window, cx);
|
||||
window.focus(&modal.focus_handle(cx));
|
||||
|
||||
modal
|
||||
@@ -470,11 +458,11 @@ impl RenderOnce for NewSessionMode {
|
||||
impl NewSessionMode {
|
||||
fn attach(
|
||||
debugger: Option<SharedString>,
|
||||
project: Entity<Project>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> Self {
|
||||
Self::Attach(AttachMode::new(debugger, project, window, cx))
|
||||
Self::Attach(AttachMode::new(debugger, workspace, window, cx))
|
||||
}
|
||||
fn launch(
|
||||
past_launch_config: Option<LaunchRequest>,
|
||||
@@ -569,15 +557,12 @@ impl Render for NewSessionModal {
|
||||
.toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
|
||||
.style(ui::ButtonStyle::Subtle)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let Ok(project) = this
|
||||
.workspace
|
||||
.read_with(cx, |this, _| this.project().clone())
|
||||
else {
|
||||
let Some(workspace) = this.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
this.mode = NewSessionMode::attach(
|
||||
this.debugger.clone(),
|
||||
project,
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -631,7 +616,7 @@ impl Render for NewSessionModal {
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.start_new_session(window, cx).log_err();
|
||||
this.start_new_session(window, cx);
|
||||
}))
|
||||
.disabled(self.debugger.is_none()),
|
||||
),
|
||||
|
||||
@@ -88,6 +88,12 @@ 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)),
|
||||
@@ -115,13 +121,7 @@ impl DebugSession {
|
||||
};
|
||||
|
||||
self.label
|
||||
.get_or_init(|| {
|
||||
session
|
||||
.read(cx)
|
||||
.as_local()
|
||||
.expect("Remote Debug Sessions are not implemented yet")
|
||||
.label()
|
||||
})
|
||||
.get_or_init(|| session.read(cx).label())
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
|
||||
@@ -411,13 +411,26 @@ impl RunningState {
|
||||
.log_err();
|
||||
|
||||
if let Some(thread_id) = thread_id {
|
||||
this.select_thread(*thread_id, cx);
|
||||
this.select_thread(*thread_id, window, cx);
|
||||
}
|
||||
}
|
||||
SessionEvent::Threads => {
|
||||
let threads = this.session.update(cx, |this, cx| this.threads(cx));
|
||||
this.select_current_thread(&threads, cx);
|
||||
this.select_current_thread(&threads, window, 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()
|
||||
@@ -447,35 +460,14 @@ 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,
|
||||
@@ -512,11 +504,6 @@ 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)
|
||||
@@ -744,6 +731,7 @@ impl RunningState {
|
||||
pub fn select_current_thread(
|
||||
&mut self,
|
||||
threads: &Vec<(Thread, ThreadStatus)>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let selected_thread = self
|
||||
@@ -756,7 +744,7 @@ impl RunningState {
|
||||
};
|
||||
|
||||
if Some(ThreadId(selected_thread.id)) != self.thread_id {
|
||||
self.select_thread(ThreadId(selected_thread.id), cx);
|
||||
self.select_thread(ThreadId(selected_thread.id), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,7 +757,7 @@ impl RunningState {
|
||||
.map(|id| self.session().read(cx).thread_status(id))
|
||||
}
|
||||
|
||||
fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
|
||||
fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.thread_id.is_some_and(|id| id == thread_id) {
|
||||
return;
|
||||
}
|
||||
@@ -777,8 +765,7 @@ impl RunningState {
|
||||
self.thread_id = Some(thread_id);
|
||||
|
||||
self.stack_frame_list
|
||||
.update(cx, |list, cx| list.refresh(cx));
|
||||
cx.notify();
|
||||
.update(cx, |list, cx| list.schedule_refresh(true, window, cx));
|
||||
}
|
||||
|
||||
pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -930,9 +917,9 @@ impl RunningState {
|
||||
for (thread, _) in threads {
|
||||
let state = state.clone();
|
||||
let thread_id = thread.id;
|
||||
this = this.entry(thread.name, None, move |_, cx| {
|
||||
this = this.entry(thread.name, None, move |window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.select_thread(ThreadId(thread_id), cx);
|
||||
state.select_thread(ThreadId(thread_id), window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -946,8 +933,8 @@ impl RunningState {
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
stack_frame_list: &Entity<StackFrameList>,
|
||||
variable_list: &Entity<VariableList>,
|
||||
module_list: Option<&Entity<ModuleList>>,
|
||||
loaded_source_list: Option<&Entity<LoadedSourceList>>,
|
||||
module_list: &Entity<ModuleList>,
|
||||
loaded_source_list: &Entity<LoadedSourceList>,
|
||||
console: &Entity<Console>,
|
||||
breakpoints: &Entity<BreakpointList>,
|
||||
subscriptions: &mut HashMap<EntityId, Subscription>,
|
||||
@@ -1003,41 +990,36 @@ impl RunningState {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
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,
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
module_list.focus_handle(cx),
|
||||
module_list.clone().into(),
|
||||
DebuggerPaneItem::Modules,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.activate_item(0, false, false, window, cx);
|
||||
}
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
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,
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
loaded_source_list.focus_handle(cx),
|
||||
loaded_source_list.clone().into(),
|
||||
DebuggerPaneItem::LoadedSources,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.activate_item(1, false, false, window, cx);
|
||||
}
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.activate_item(0, false, false, window, cx);
|
||||
});
|
||||
|
||||
let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use dap::StackFrameId;
|
||||
@@ -28,11 +29,11 @@ pub struct StackFrameList {
|
||||
_subscription: Subscription,
|
||||
session: Entity<Session>,
|
||||
state: WeakEntity<RunningState>,
|
||||
invalidate: bool,
|
||||
entries: Vec<StackFrameEntry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
selected_stack_frame_id: Option<StackFrameId>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -68,14 +69,17 @@ impl StackFrameList {
|
||||
);
|
||||
|
||||
let _subscription =
|
||||
cx.subscribe_in(&session, window, |this, _, event, _, cx| match event {
|
||||
SessionEvent::Stopped(_) | SessionEvent::StackTrace | SessionEvent::Threads => {
|
||||
this.refresh(cx);
|
||||
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
|
||||
SessionEvent::Threads => {
|
||||
this.schedule_refresh(false, window, cx);
|
||||
}
|
||||
SessionEvent::Stopped(..) | SessionEvent::StackTrace => {
|
||||
this.schedule_refresh(true, window, cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Self {
|
||||
let mut this = Self {
|
||||
scrollbar_state: ScrollbarState::new(list.clone()),
|
||||
list,
|
||||
session,
|
||||
@@ -83,10 +87,12 @@ impl StackFrameList {
|
||||
focus_handle,
|
||||
state,
|
||||
_subscription,
|
||||
invalidate: true,
|
||||
entries: Default::default(),
|
||||
selected_stack_frame_id: None,
|
||||
}
|
||||
_refresh_task: Task::ready(()),
|
||||
};
|
||||
this.schedule_refresh(true, window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -136,10 +142,32 @@ impl StackFrameList {
|
||||
self.selected_stack_frame_id
|
||||
}
|
||||
|
||||
pub(super) fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||
self.invalidate = true;
|
||||
self.entries.clear();
|
||||
cx.notify();
|
||||
pub(super) fn schedule_refresh(
|
||||
&mut self,
|
||||
select_first: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(20);
|
||||
|
||||
self._refresh_task = cx.spawn_in(window, async move |this, cx| {
|
||||
let debounce = this
|
||||
.update(cx, |this, cx| {
|
||||
let new_stack_frames = this.stack_frames(cx);
|
||||
new_stack_frames.is_empty() && !this.entries.is_empty()
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
if debounce {
|
||||
cx.background_executor().timer(REFRESH_DEBOUNCE).await;
|
||||
}
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.build_entries(select_first, window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_entries(
|
||||
@@ -515,13 +543,7 @@ impl StackFrameList {
|
||||
}
|
||||
|
||||
impl Render for StackFrameList {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.invalidate {
|
||||
self.build_entries(self.entries.is_empty(), window, cx);
|
||||
self.invalidate = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.p_1()
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use dap::{DebugRequest, client::DebugAdapterClient};
|
||||
use gpui::{Entity, TestAppContext, WindowHandle};
|
||||
use project::Project;
|
||||
use project::{Project, debugger::session::Session};
|
||||
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) {
|
||||
@@ -42,7 +55,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.clone()).await
|
||||
DebugPanel::load(this, cx).await
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
@@ -82,3 +95,46 @@ 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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{attach_modal::Candidate, *};
|
||||
use crate::{attach_modal::Candidate, tests::start_debug_session_with, *};
|
||||
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 = debugger::test::start_debug_session_with(
|
||||
&project,
|
||||
let session = start_debug_session_with(
|
||||
&workspace,
|
||||
cx,
|
||||
DebugTaskDefinition {
|
||||
adapter: "fake-adapter".to_string(),
|
||||
@@ -47,7 +47,6 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
|
||||
});
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
@@ -99,9 +98,10 @@ 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(
|
||||
project.clone(),
|
||||
workspace_handle,
|
||||
DebugTaskDefinition {
|
||||
adapter: FakeAdapter::ADAPTER_NAME.into(),
|
||||
request: dap::DebugRequest::Attach(AttachRequest::default()),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::{tests::active_debug_session_panel, *};
|
||||
use crate::{
|
||||
tests::{active_debug_session_panel, start_debug_session},
|
||||
*,
|
||||
};
|
||||
use dap::requests::StackTrace;
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
@@ -28,9 +31,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let session = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<StackTrace, _>(move |_, _| {
|
||||
|
||||
118
crates/debugger_ui/src/tests/dap_logger.rs
Normal file
118
crates/debugger_ui/src/tests/dap_logger.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::*;
|
||||
use crate::{tests::start_debug_session, *};
|
||||
use dap::{
|
||||
ErrorResponse, Message, RunInTerminalRequestArguments, SourceBreakpoint,
|
||||
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
|
||||
@@ -48,9 +48,7 @@ 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 = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<Threads, _>(move |_, _| {
|
||||
@@ -187,9 +185,7 @@ 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 = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<Threads, _>(move |_, _| {
|
||||
@@ -354,9 +350,7 @@ 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 = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client
|
||||
@@ -419,6 +413,86 @@ 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]
|
||||
@@ -444,9 +518,7 @@ 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 = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client
|
||||
@@ -522,9 +594,7 @@ 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 = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<dap::requests::Threads, _>(move |_, _| {
|
||||
@@ -629,9 +699,7 @@ 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 = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<dap::requests::Threads, _>(move |_, _| {
|
||||
@@ -737,9 +805,7 @@ 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 = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_response::<StartDebugging, _>(move |_| {}).await;
|
||||
@@ -858,7 +924,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 = debugger::test::start_debug_session(&project, cx, |client| {
|
||||
let session = start_debug_session(&workspace, cx, |client| {
|
||||
client.on_request::<dap::requests::Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_step_back: Some(true),
|
||||
@@ -866,7 +932,6 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
|
||||
})
|
||||
});
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
@@ -1073,9 +1138,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved(
|
||||
.update(cx, |_, _, cx| worktree.read(cx).id())
|
||||
.unwrap();
|
||||
|
||||
let session = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
let buffer = project
|
||||
@@ -1290,9 +1353,7 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
|
||||
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
|
||||
});
|
||||
|
||||
let session = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
let called_set_breakpoints = Arc::new(AtomicBool::new(false));
|
||||
@@ -1358,7 +1419,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);
|
||||
|
||||
let task = project::debugger::test::start_debug_session(&project, cx, |client| {
|
||||
start_debug_session(&workspace, cx, |client| {
|
||||
client.on_request::<dap::requests::Initialize, _>(|_, _| {
|
||||
Err(ErrorResponse {
|
||||
error: Some(Message {
|
||||
@@ -1372,12 +1433,8 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
|
||||
}),
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
assert!(
|
||||
task.await.is_err(),
|
||||
"Session should failed to start if launch request fails"
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
tests::{active_debug_session_panel, init_test, init_test_workspace},
|
||||
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
|
||||
};
|
||||
use dap::{
|
||||
StoppedEvent,
|
||||
requests::{Initialize, Modules},
|
||||
};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{
|
||||
FakeFs, Project,
|
||||
debugger::{self},
|
||||
};
|
||||
use project::{FakeFs, Project};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicI32, Ordering},
|
||||
@@ -31,7 +28,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
.unwrap();
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let session = debugger::test::start_debug_session(&project, cx, |client| {
|
||||
let session = start_debug_session(&workspace, cx, |client| {
|
||||
client.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_modules_request: Some(true),
|
||||
@@ -39,7 +36,6 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
})
|
||||
});
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
@@ -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},
|
||||
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
|
||||
};
|
||||
use dap::{
|
||||
StackFrame,
|
||||
@@ -9,7 +9,7 @@ use dap::{
|
||||
};
|
||||
use editor::{Editor, ToPoint as _};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Project, debugger};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use unindent::Unindent as _;
|
||||
@@ -50,9 +50,7 @@ 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 = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
|
||||
|
||||
@@ -154,7 +152,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
|
||||
cx.run_until_parked();
|
||||
|
||||
// select first thread
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
@@ -164,6 +162,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -229,9 +228,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
});
|
||||
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let session = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<Threads, _>(move |_, _| {
|
||||
@@ -334,7 +331,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
cx.run_until_parked();
|
||||
|
||||
// select first thread
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
@@ -344,6 +341,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -495,9 +493,7 @@ 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 = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<Threads, _>(move |_, _| {
|
||||
@@ -710,7 +706,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
cx.run_until_parked();
|
||||
|
||||
// select first thread
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
|
||||
session
|
||||
.mode()
|
||||
.as_running()
|
||||
@@ -720,6 +716,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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},
|
||||
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
|
||||
};
|
||||
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, debugger};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use unindent::Unindent as _;
|
||||
use util::path;
|
||||
@@ -54,9 +54,7 @@ async fn test_basic_fetch_initial_scope_and_variables(
|
||||
})
|
||||
.unwrap();
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let session = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<dap::requests::Threads, _>(move |_, _| {
|
||||
@@ -266,9 +264,7 @@ async fn test_fetch_variables_for_multiple_scopes(
|
||||
.unwrap();
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let session = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<dap::requests::Threads, _>(move |_, _| {
|
||||
@@ -528,9 +524,7 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
|
||||
})
|
||||
.unwrap();
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let session = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<dap::requests::Threads, _>(move |_, _| {
|
||||
@@ -1313,9 +1307,7 @@ 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 = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<dap::requests::Threads, _>(move |_, _| {
|
||||
@@ -1560,9 +1552,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
|
||||
.unwrap();
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let session = debugger::test::start_debug_session(&project, cx, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
client.on_request::<dap::requests::Threads, _>(move |_, _| {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
1005
crates/editor/src/code_completion_tests.rs
Normal file
1005
crates/editor/src/code_completion_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,8 +27,8 @@ use util::ResultExt;
|
||||
|
||||
use crate::hover_popover::{hover_markdown_style, open_markdown_url};
|
||||
use crate::{
|
||||
CodeActionProvider, CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle,
|
||||
ResolvedTasks,
|
||||
CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
|
||||
EditorStyle, ResolvedTasks,
|
||||
actions::{ConfirmCodeAction, ConfirmCompletion},
|
||||
split_words, styled_runs_for_code_label,
|
||||
};
|
||||
@@ -657,6 +657,63 @@ impl CompletionsMenu {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sort_matches(matches: &mut Vec<SortableMatch<'_>>, query: Option<&str>) {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum MatchTier<'a> {
|
||||
WordStartMatch {
|
||||
sort_score_int: Reverse<i32>,
|
||||
sort_snippet: Reverse<i32>,
|
||||
sort_text: Option<&'a str>,
|
||||
sort_key: (usize, &'a str),
|
||||
},
|
||||
OtherMatch {
|
||||
sort_score: Reverse<OrderedFloat<f64>>,
|
||||
},
|
||||
}
|
||||
|
||||
// Our goal here is to intelligently sort completion suggestions. We want to
|
||||
// balance the raw fuzzy match score with hints from the language server
|
||||
//
|
||||
// We first primary sort using fuzzy score by putting matches into two buckets
|
||||
// strong one and weak one. Among these buckets matches are then compared by
|
||||
// various criteria like snippet, LSP hints, kind, label text etc.
|
||||
//
|
||||
const FUZZY_THRESHOLD: f64 = 0.1317;
|
||||
|
||||
let query_start_lower = query
|
||||
.and_then(|q| q.chars().next())
|
||||
.and_then(|c| c.to_lowercase().next());
|
||||
|
||||
matches.sort_unstable_by_key(|mat| {
|
||||
let score = mat.string_match.score;
|
||||
|
||||
let is_other_match = query_start_lower
|
||||
.map(|query_char| {
|
||||
!split_words(&mat.string_match.string).any(|word| {
|
||||
word.chars()
|
||||
.next()
|
||||
.and_then(|c| c.to_lowercase().next())
|
||||
.map_or(false, |word_char| word_char == query_char)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_other_match {
|
||||
let sort_score = Reverse(OrderedFloat(score));
|
||||
MatchTier::OtherMatch { sort_score }
|
||||
} else {
|
||||
let sort_score_int = Reverse(if score >= FUZZY_THRESHOLD { 1 } else { 0 });
|
||||
let sort_snippet = Reverse(if mat.is_snippet { 1 } else { 0 });
|
||||
MatchTier::WordStartMatch {
|
||||
sort_score_int,
|
||||
sort_snippet,
|
||||
sort_text: mat.sort_text,
|
||||
sort_key: mat.sort_key,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
|
||||
let mut matches = if let Some(query) = query {
|
||||
fuzzy::match_strings(
|
||||
@@ -681,85 +738,45 @@ impl CompletionsMenu {
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut additional_matches = Vec::new();
|
||||
// Deprioritize all candidates where the query's start does not match the start of any word in the candidate
|
||||
if let Some(query) = query {
|
||||
if let Some(query_start) = query.chars().next() {
|
||||
let (primary, secondary) = matches.into_iter().partition(|string_match| {
|
||||
split_words(&string_match.string).any(|word| {
|
||||
// Check that the first codepoint of the word as lowercase matches the first
|
||||
// codepoint of the query as lowercase
|
||||
word.chars()
|
||||
.flat_map(|codepoint| codepoint.to_lowercase())
|
||||
.zip(query_start.to_lowercase())
|
||||
.all(|(word_cp, query_cp)| word_cp == query_cp)
|
||||
})
|
||||
});
|
||||
matches = primary;
|
||||
additional_matches = secondary;
|
||||
}
|
||||
}
|
||||
|
||||
let completions = self.completions.borrow_mut();
|
||||
if self.sort_completions {
|
||||
matches.sort_unstable_by_key(|mat| {
|
||||
// We do want to strike a balance here between what the language server tells us
|
||||
// to sort by (the sort_text) and what are "obvious" good matches (i.e. when you type
|
||||
// `Creat` and there is a local variable called `CreateComponent`).
|
||||
// So what we do is: we bucket all matches into two buckets
|
||||
// - Strong matches
|
||||
// - Weak matches
|
||||
// Strong matches are the ones with a high fuzzy-matcher score (the "obvious" matches)
|
||||
// and the Weak matches are the rest.
|
||||
//
|
||||
// For the strong matches, we sort by our fuzzy-finder score first and for the weak
|
||||
// matches, we prefer language-server sort_text first.
|
||||
//
|
||||
// The thinking behind that: we want to show strong matches first in order of relevance(fuzzy score).
|
||||
// Rest of the matches(weak) can be sorted as language-server expects.
|
||||
let completions = self.completions.borrow();
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum MatchScore<'a> {
|
||||
Strong {
|
||||
score: Reverse<OrderedFloat<f64>>,
|
||||
sort_text: Option<&'a str>,
|
||||
sort_key: (usize, &'a str),
|
||||
},
|
||||
Weak {
|
||||
sort_text: Option<&'a str>,
|
||||
score: Reverse<OrderedFloat<f64>>,
|
||||
sort_key: (usize, &'a str),
|
||||
},
|
||||
}
|
||||
let mut sortable_items: Vec<SortableMatch<'_>> = matches
|
||||
.into_iter()
|
||||
.map(|string_match| {
|
||||
let completion = &completions[string_match.candidate_id];
|
||||
|
||||
let completion = &completions[mat.candidate_id];
|
||||
let sort_key = completion.sort_key();
|
||||
let sort_text =
|
||||
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
|
||||
lsp_completion.sort_text.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let score = Reverse(OrderedFloat(mat.score));
|
||||
let is_snippet = matches!(
|
||||
&completion.source,
|
||||
CompletionSource::Lsp { lsp_completion, .. }
|
||||
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
|
||||
);
|
||||
|
||||
if mat.score >= 0.2 {
|
||||
MatchScore::Strong {
|
||||
score,
|
||||
let sort_text =
|
||||
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
|
||||
lsp_completion.sort_text.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let sort_key = completion.sort_key();
|
||||
|
||||
SortableMatch {
|
||||
string_match,
|
||||
is_snippet,
|
||||
sort_text,
|
||||
sort_key,
|
||||
}
|
||||
} else {
|
||||
MatchScore::Weak {
|
||||
sort_text,
|
||||
score,
|
||||
sort_key,
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self::sort_matches(&mut sortable_items, query);
|
||||
|
||||
matches = sortable_items
|
||||
.into_iter()
|
||||
.map(|sortable| sortable.string_match)
|
||||
.collect();
|
||||
}
|
||||
drop(completions);
|
||||
|
||||
matches.extend(additional_matches);
|
||||
|
||||
*self.entries.borrow_mut() = matches;
|
||||
self.selected_item = 0;
|
||||
@@ -768,6 +785,14 @@ impl CompletionsMenu {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SortableMatch<'a> {
|
||||
pub string_match: StringMatch,
|
||||
pub is_snippet: bool,
|
||||
pub sort_text: Option<&'a str>,
|
||||
pub sort_key: (usize, &'a str),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AvailableCodeAction {
|
||||
pub excerpt_id: ExcerptId,
|
||||
|
||||
@@ -808,10 +808,14 @@ impl DisplaySnapshot {
|
||||
// used by line_mode selections and tries to match vim behavior
|
||||
pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
|
||||
let new_start = MultiBufferPoint::new(range.start.row, 0);
|
||||
let new_end = MultiBufferPoint::new(
|
||||
range.end.row,
|
||||
self.buffer_snapshot.line_len(MultiBufferRow(range.end.row)),
|
||||
);
|
||||
let new_end = if range.end.column > 0 {
|
||||
MultiBufferPoint::new(
|
||||
range.end.row,
|
||||
self.buffer_snapshot.line_len(MultiBufferRow(range.end.row)),
|
||||
)
|
||||
} else {
|
||||
range.end
|
||||
};
|
||||
|
||||
new_start..new_end
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ pub mod scroll;
|
||||
mod selections_collection;
|
||||
pub mod tasks;
|
||||
|
||||
#[cfg(test)]
|
||||
mod code_completion_tests;
|
||||
#[cfg(test)]
|
||||
mod editor_tests;
|
||||
#[cfg(test)]
|
||||
@@ -267,6 +269,12 @@ enum DocumentHighlightWrite {}
|
||||
enum InputComposition {}
|
||||
enum SelectedTextHighlight {}
|
||||
|
||||
pub enum ConflictsOuter {}
|
||||
pub enum ConflictsOurs {}
|
||||
pub enum ConflictsTheirs {}
|
||||
pub enum ConflictsOursMarker {}
|
||||
pub enum ConflictsTheirsMarker {}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Navigated {
|
||||
Yes,
|
||||
@@ -692,6 +700,10 @@ pub trait Addon: 'static {
|
||||
}
|
||||
|
||||
fn to_any(&self) -> &dyn std::any::Any;
|
||||
|
||||
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of caret positions, registered when the editor was edited.
|
||||
@@ -1081,11 +1093,27 @@ impl SelectionHistory {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RowHighlightOptions {
|
||||
pub autoscroll: bool,
|
||||
pub include_gutter: bool,
|
||||
}
|
||||
|
||||
impl Default for RowHighlightOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
autoscroll: Default::default(),
|
||||
include_gutter: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RowHighlight {
|
||||
index: usize,
|
||||
range: Range<Anchor>,
|
||||
color: Hsla,
|
||||
should_autoscroll: bool,
|
||||
options: RowHighlightOptions,
|
||||
type_id: TypeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -3108,6 +3136,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,
|
||||
@@ -5933,7 +5968,10 @@ impl Editor {
|
||||
self.highlight_rows::<EditPredictionPreview>(
|
||||
target..target,
|
||||
cx.theme().colors().editor_highlighted_line_background,
|
||||
true,
|
||||
RowHighlightOptions {
|
||||
autoscroll: true,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
@@ -13440,7 +13478,7 @@ impl Editor {
|
||||
start..end,
|
||||
highlight_color
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
|
||||
@@ -16756,7 +16794,7 @@ impl Editor {
|
||||
&mut self,
|
||||
range: Range<Anchor>,
|
||||
color: Hsla,
|
||||
should_autoscroll: bool,
|
||||
options: RowHighlightOptions,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
@@ -16788,7 +16826,7 @@ impl Editor {
|
||||
merged = true;
|
||||
prev_highlight.index = index;
|
||||
prev_highlight.color = color;
|
||||
prev_highlight.should_autoscroll = should_autoscroll;
|
||||
prev_highlight.options = options;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16799,7 +16837,8 @@ impl Editor {
|
||||
range: range.clone(),
|
||||
index,
|
||||
color,
|
||||
should_autoscroll,
|
||||
options,
|
||||
type_id: TypeId::of::<T>(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -16905,7 +16944,15 @@ impl Editor {
|
||||
used_highlight_orders.entry(row).or_insert(highlight.index);
|
||||
if highlight.index >= *used_index {
|
||||
*used_index = highlight.index;
|
||||
unique_rows.insert(DisplayRow(row), highlight.color.into());
|
||||
unique_rows.insert(
|
||||
DisplayRow(row),
|
||||
LineHighlight {
|
||||
include_gutter: highlight.options.include_gutter,
|
||||
border: None,
|
||||
background: highlight.color.into(),
|
||||
type_id: Some(highlight.type_id),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
unique_rows
|
||||
@@ -16921,7 +16968,7 @@ impl Editor {
|
||||
.values()
|
||||
.flat_map(|highlighted_rows| highlighted_rows.iter())
|
||||
.filter_map(|highlight| {
|
||||
if highlight.should_autoscroll {
|
||||
if highlight.options.autoscroll {
|
||||
Some(highlight.range.start.to_display_point(snapshot).row())
|
||||
} else {
|
||||
None
|
||||
@@ -17396,13 +17443,19 @@ impl Editor {
|
||||
});
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
}
|
||||
multi_buffer::Event::ExcerptsRemoved { ids } => {
|
||||
multi_buffer::Event::ExcerptsRemoved {
|
||||
ids,
|
||||
removed_buffer_ids,
|
||||
} => {
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
|
||||
let buffer = self.buffer.read(cx);
|
||||
self.registered_buffers
|
||||
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
|
||||
cx.emit(EditorEvent::ExcerptsRemoved {
|
||||
ids: ids.clone(),
|
||||
removed_buffer_ids: removed_buffer_ids.clone(),
|
||||
})
|
||||
}
|
||||
multi_buffer::Event::ExcerptsEdited {
|
||||
excerpt_ids,
|
||||
@@ -18210,6 +18263,13 @@ impl Editor {
|
||||
.and_then(|item| item.to_any().downcast_ref::<T>())
|
||||
}
|
||||
|
||||
pub fn addon_mut<T: Addon>(&mut self) -> Option<&mut T> {
|
||||
let type_id = std::any::TypeId::of::<T>();
|
||||
self.addons
|
||||
.get_mut(&type_id)
|
||||
.and_then(|item| item.to_any_mut()?.downcast_mut::<T>())
|
||||
}
|
||||
|
||||
fn character_size(&self, window: &mut Window) -> gpui::Size<Pixels> {
|
||||
let text_layout_details = self.text_layout_details(window);
|
||||
let style = &text_layout_details.editor_style;
|
||||
@@ -19723,6 +19783,7 @@ pub enum EditorEvent {
|
||||
},
|
||||
ExcerptsRemoved {
|
||||
ids: Vec<ExcerptId>,
|
||||
removed_buffer_ids: Vec<BufferId>,
|
||||
},
|
||||
BufferFoldToggled {
|
||||
ids: Vec<ExcerptId>,
|
||||
@@ -20663,24 +20724,8 @@ impl Render for MissingEditPredictionKeybindingTooltip {
|
||||
pub struct LineHighlight {
|
||||
pub background: Background,
|
||||
pub border: Option<gpui::Hsla>,
|
||||
}
|
||||
|
||||
impl From<Hsla> for LineHighlight {
|
||||
fn from(hsla: Hsla) -> Self {
|
||||
Self {
|
||||
background: hsla.into(),
|
||||
border: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Background> for LineHighlight {
|
||||
fn from(background: Background) -> Self {
|
||||
Self {
|
||||
background,
|
||||
border: None,
|
||||
}
|
||||
}
|
||||
pub include_gutter: bool,
|
||||
pub type_id: Option<TypeId>,
|
||||
}
|
||||
|
||||
fn render_diff_hunk_controls(
|
||||
|
||||
@@ -10704,7 +10704,7 @@ async fn test_completion(cx: &mut TestAppContext) {
|
||||
.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state("editor.closeˇ");
|
||||
cx.assert_editor_state("editor.clobberˇ");
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
}
|
||||
@@ -11266,76 +11266,6 @@ async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion_sort(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
cx.lsp
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "Range".into(),
|
||||
sort_text: Some("a".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "r".into(),
|
||||
sort_text: Some("b".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "ret".into(),
|
||||
sort_text: Some("c".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "return".into(),
|
||||
sort_text: Some("d".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "slice".into(),
|
||||
sort_text: Some("d".into()),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.set_state("rˇ");
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(
|
||||
&ShowCompletions {
|
||||
trigger: Some("r".into()),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu),
|
||||
&["r", "ret", "Range", "return"]
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_as_is_completions(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -12699,19 +12629,22 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
|
||||
// Insert some excerpts.
|
||||
leader.update(cx, |leader, cx| {
|
||||
leader.buffer.update(cx, |multibuffer, cx| {
|
||||
let excerpt_ids = multibuffer.push_excerpts(
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
|
||||
buffer_1.clone(),
|
||||
[
|
||||
ExcerptRange::new(1..6),
|
||||
ExcerptRange::new(12..15),
|
||||
ExcerptRange::new(0..3),
|
||||
vec![
|
||||
Point::row_range(0..3),
|
||||
Point::row_range(1..6),
|
||||
Point::row_range(12..15),
|
||||
],
|
||||
0,
|
||||
cx,
|
||||
);
|
||||
multibuffer.insert_excerpts_after(
|
||||
excerpt_ids[0],
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
|
||||
vec![Point::row_range(0..6), Point::row_range(8..12)],
|
||||
0,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -14061,7 +13994,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu),
|
||||
&["bg-red", "bg-blue", "bg-yellow"]
|
||||
&["bg-blue", "bg-red", "bg-yellow"]
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
|
||||
ChunkRendererContext, ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId,
|
||||
ChunkRendererContext, ChunkReplacement, ConflictsOurs, ConflictsOursMarker, ConflictsOuter,
|
||||
ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId,
|
||||
DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
|
||||
EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
|
||||
FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
|
||||
@@ -4036,6 +4037,7 @@ impl EditorElement {
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||||
highlighted_rows: &BTreeMap<DisplayRow, LineHighlight>,
|
||||
editor: Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -4064,6 +4066,22 @@ impl EditorElement {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if highlighted_rows
|
||||
.get(&display_row_range.start)
|
||||
.and_then(|highlight| highlight.type_id)
|
||||
.is_some_and(|type_id| {
|
||||
[
|
||||
TypeId::of::<ConflictsOuter>(),
|
||||
TypeId::of::<ConflictsOursMarker>(),
|
||||
TypeId::of::<ConflictsOurs>(),
|
||||
TypeId::of::<ConflictsTheirs>(),
|
||||
TypeId::of::<ConflictsTheirsMarker>(),
|
||||
]
|
||||
.contains(&type_id)
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let row_ix = (display_row_range.start - row_range.start).0 as usize;
|
||||
if row_infos[row_ix].diff_status.is_none() {
|
||||
continue;
|
||||
@@ -4258,14 +4276,21 @@ impl EditorElement {
|
||||
highlight_row_end: DisplayRow,
|
||||
highlight: crate::LineHighlight,
|
||||
edges| {
|
||||
let mut origin_x = layout.hitbox.left();
|
||||
let mut width = layout.hitbox.size.width;
|
||||
if !highlight.include_gutter {
|
||||
origin_x += layout.gutter_hitbox.size.width;
|
||||
width -= layout.gutter_hitbox.size.width;
|
||||
}
|
||||
|
||||
let origin = point(
|
||||
layout.hitbox.origin.x,
|
||||
origin_x,
|
||||
layout.hitbox.origin.y
|
||||
+ (highlight_row_start.as_f32() - scroll_top)
|
||||
* layout.position_map.line_height,
|
||||
);
|
||||
let size = size(
|
||||
layout.hitbox.size.width,
|
||||
width,
|
||||
layout.position_map.line_height
|
||||
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
|
||||
);
|
||||
@@ -6789,10 +6814,16 @@ impl Element for EditorElement {
|
||||
} else {
|
||||
background_color.opacity(0.36)
|
||||
}),
|
||||
include_gutter: true,
|
||||
type_id: None,
|
||||
};
|
||||
|
||||
let filled_highlight =
|
||||
solid_background(background_color.opacity(hunk_opacity)).into();
|
||||
let filled_highlight = LineHighlight {
|
||||
background: solid_background(background_color.opacity(hunk_opacity)),
|
||||
border: None,
|
||||
include_gutter: true,
|
||||
type_id: None,
|
||||
};
|
||||
|
||||
let background = if Self::diff_hunk_hollow(diff_status, cx) {
|
||||
hollow_highlight
|
||||
@@ -7551,6 +7582,7 @@ impl Element for EditorElement {
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&display_hunks,
|
||||
&highlighted_rows,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -99,25 +99,40 @@ impl FollowableItem for Editor {
|
||||
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
|
||||
} else {
|
||||
multibuffer = MultiBuffer::new(project.read(cx).capability());
|
||||
let mut excerpts = state.excerpts.into_iter().peekable();
|
||||
while let Some(excerpt) = excerpts.peek() {
|
||||
let mut sorted_excerpts = state.excerpts.clone();
|
||||
sorted_excerpts.sort_by_key(|e| e.id);
|
||||
let mut sorted_excerpts = sorted_excerpts.into_iter().peekable();
|
||||
|
||||
while let Some(excerpt) = sorted_excerpts.next() {
|
||||
let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
let buffer_excerpts = iter::from_fn(|| {
|
||||
let excerpt = excerpts.peek()?;
|
||||
(excerpt.buffer_id == u64::from(buffer_id))
|
||||
.then(|| excerpts.next().unwrap())
|
||||
});
|
||||
|
||||
let mut insert_position = ExcerptId::min();
|
||||
for e in &state.excerpts {
|
||||
if e.id == excerpt.id {
|
||||
break;
|
||||
}
|
||||
if e.id < excerpt.id {
|
||||
insert_position = ExcerptId::from_proto(e.id);
|
||||
}
|
||||
}
|
||||
|
||||
let buffer =
|
||||
buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
multibuffer.push_excerpts(
|
||||
buffer.clone(),
|
||||
buffer_excerpts.filter_map(deserialize_excerpt_range),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let Some(excerpt) = deserialize_excerpt_range(excerpt) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(buffer) = buffer else { continue };
|
||||
|
||||
multibuffer.insert_excerpts_with_ids_after(
|
||||
insert_position,
|
||||
buffer.clone(),
|
||||
[excerpt],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -202,25 +217,26 @@ impl FollowableItem for Editor {
|
||||
primary_end: Some(serialize_text_anchor(&range.primary.end)),
|
||||
})
|
||||
.collect();
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
|
||||
Some(proto::view::Variant::Editor(proto::view::Editor {
|
||||
singleton: buffer.is_singleton(),
|
||||
title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
|
||||
excerpts,
|
||||
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)),
|
||||
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor, &snapshot)),
|
||||
scroll_x: scroll_anchor.offset.x,
|
||||
scroll_y: scroll_anchor.offset.y,
|
||||
selections: self
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.map(serialize_selection)
|
||||
.map(|s| serialize_selection(s, &snapshot))
|
||||
.collect(),
|
||||
pending_selection: self
|
||||
.selections
|
||||
.pending_anchor()
|
||||
.as_ref()
|
||||
.map(serialize_selection),
|
||||
.map(|s| serialize_selection(s, &snapshot)),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -272,31 +288,34 @@ impl FollowableItem for Editor {
|
||||
}
|
||||
true
|
||||
}
|
||||
EditorEvent::ExcerptsRemoved { ids } => {
|
||||
EditorEvent::ExcerptsRemoved { ids, .. } => {
|
||||
update
|
||||
.deleted_excerpts
|
||||
.extend(ids.iter().map(ExcerptId::to_proto));
|
||||
true
|
||||
}
|
||||
EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let scroll_anchor = self.scroll_manager.anchor();
|
||||
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
|
||||
update.scroll_top_anchor =
|
||||
Some(serialize_anchor(&scroll_anchor.anchor, &snapshot));
|
||||
update.scroll_x = scroll_anchor.offset.x;
|
||||
update.scroll_y = scroll_anchor.offset.y;
|
||||
true
|
||||
}
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
update.selections = self
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.map(serialize_selection)
|
||||
.map(|s| serialize_selection(s, &snapshot))
|
||||
.collect();
|
||||
update.pending_selection = self
|
||||
.selections
|
||||
.pending_anchor()
|
||||
.as_ref()
|
||||
.map(serialize_selection);
|
||||
.map(|s| serialize_selection(s, &snapshot));
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
@@ -396,12 +415,7 @@ async fn update_editor_from_message(
|
||||
[excerpt]
|
||||
.into_iter()
|
||||
.chain(adjacent_excerpts)
|
||||
.filter_map(|excerpt| {
|
||||
Some((
|
||||
ExcerptId::from_proto(excerpt.id),
|
||||
deserialize_excerpt_range(excerpt)?,
|
||||
))
|
||||
}),
|
||||
.filter_map(deserialize_excerpt_range),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -478,23 +492,28 @@ fn serialize_excerpt(
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
|
||||
fn serialize_selection(
|
||||
selection: &Selection<Anchor>,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
) -> proto::Selection {
|
||||
proto::Selection {
|
||||
id: selection.id as u64,
|
||||
start: Some(serialize_anchor(&selection.start)),
|
||||
end: Some(serialize_anchor(&selection.end)),
|
||||
start: Some(serialize_anchor(&selection.start, &buffer)),
|
||||
end: Some(serialize_anchor(&selection.end, &buffer)),
|
||||
reversed: selection.reversed,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor {
|
||||
fn serialize_anchor(anchor: &Anchor, buffer: &MultiBufferSnapshot) -> proto::EditorAnchor {
|
||||
proto::EditorAnchor {
|
||||
excerpt_id: anchor.excerpt_id.to_proto(),
|
||||
excerpt_id: buffer.latest_excerpt_id(anchor.excerpt_id).to_proto(),
|
||||
anchor: Some(serialize_text_anchor(&anchor.text_anchor)),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option<ExcerptRange<language::Anchor>> {
|
||||
fn deserialize_excerpt_range(
|
||||
excerpt: proto::Excerpt,
|
||||
) -> Option<(ExcerptId, ExcerptRange<language::Anchor>)> {
|
||||
let context = {
|
||||
let start = language::proto::deserialize_anchor(excerpt.context_start?)?;
|
||||
let end = language::proto::deserialize_anchor(excerpt.context_end?)?;
|
||||
@@ -509,7 +528,10 @@ fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option<ExcerptRange<lan
|
||||
Some(start..end)
|
||||
})
|
||||
.unwrap_or_else(|| context.clone());
|
||||
Some(ExcerptRange { context, primary })
|
||||
Some((
|
||||
ExcerptId::from_proto(excerpt.id),
|
||||
ExcerptRange { context, primary },
|
||||
))
|
||||
}
|
||||
|
||||
fn deserialize_selection(
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ agent.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
assistant_tools.workspace = true
|
||||
async-trait.workspace = true
|
||||
async-watch.workspace = true
|
||||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
@@ -32,9 +33,11 @@ node_runtime.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
regex.workspace = true
|
||||
release_channel.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
shellexpand.workspace = true
|
||||
smol.workspace = true
|
||||
@@ -44,7 +47,6 @@ unindent.workspace = true
|
||||
util.workspace = true
|
||||
uuid = { version = "1.6", features = ["v4"] }
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "eval"
|
||||
path = "src/eval.rs"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
url = "https://github.com/GyulyVGC/sniffnet.git"
|
||||
revision = "cfb5b6519bd7838f279e5be9d360445aaffaa647"
|
||||
language_extension = "rs"
|
||||
@@ -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.
|
||||
@@ -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 don’t use ports. Finally, ensure the `connection_details_page` displays the ARP operation types with counts, using a `pretty_print_types` method similar to ICMP types.
|
||||
@@ -1,3 +0,0 @@
|
||||
url = "https://github.com/swc-project/swc.git"
|
||||
revision = "787d5fabf410fafe6595ec00c197181b27578cb1"
|
||||
language_extension = "rs"
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,4 +0,0 @@
|
||||
url = "https://github.com/dani-garcia/vaultwarden.git"
|
||||
revision = "3a1f1bae002bebf26ce3a38b879c1ba26529af1e"
|
||||
language_extension = "rs"
|
||||
allow_preexisting_diagnostics = true
|
||||
@@ -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.
|
||||
@@ -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. I’d like to move the JWT token generation to the top of the function to avoid duplication. Then, if mail sending is enabled, the code should check for the user, but only send the verification email if the user exists and has a `private_key` (otherwise it should send the email). Keep the random sleep logic for timing mitigation in the branch where no email is sent. Remove the old duplicated token generation logic and any redundant conditionals, while ensuring the core behavior and response flow stays the same.
|
||||
@@ -1,3 +0,0 @@
|
||||
url = "https://github.com/qarmin/czkawka.git"
|
||||
revision = "db164d3698198dd46653b1c3bb0384f8a9e38fab"
|
||||
language_extension = "rs"
|
||||
@@ -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.
|
||||
@@ -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 1–8 to a custom `ExifOrientation` enum. Ensure that these transformations are applied directly to the `DynamicImage` output when applicable. Also, bump the `CACHE_IMAGE_VERSION` to invalidate any outdated cached formats and update the Rust version across the codebase to `1.80.0` to support the latest dependencies. Make any required changes to Cargo.toml and lockfiles, and default the GUI to open the Duplicate Files tab instead of Similar Images for consistency.
|
||||
@@ -1,3 +0,0 @@
|
||||
url = "https://github.com/zed-industries/zed.git"
|
||||
revision = "38fcadf9481d018543c65f36ac3bafeba190179b"
|
||||
language_extension = "rs"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user