Compare commits

..

14 Commits

Author SHA1 Message Date
Richard Feldman
ac63ea504a wip 2025-04-16 11:10:25 -04:00
Richard Feldman
11aaea964f Use ToolOutput in rendering 2025-04-09 11:05:37 -04:00
Richard Feldman
cd9a45a262 Merge remote-tracking branch 'origin/main' into tool-rendering 2025-04-09 10:23:27 -04:00
Richard Feldman
1965029528 Refactor tool rendering API 2025-04-09 10:14:27 -04:00
Richard Feldman
46c738d724 Delete unused impl 2025-04-08 16:49:52 -04:00
Richard Feldman
b18bd2d67b Revert "Use erased_serde for ToolOutput serialization"
This reverts commit cad3ee8def.
2025-04-08 16:33:55 -04:00
Richard Feldman
7e0a97fabe Revert "wip"
This reverts commit 8ba9429e60.
2025-04-08 16:33:50 -04:00
Richard Feldman
8ba9429e60 wip 2025-04-08 16:33:49 -04:00
Richard Feldman
cad3ee8def Use erased_serde for ToolOutput serialization 2025-04-08 14:19:54 -04:00
Richard Feldman
42cb40170e Implement missing traits 2025-04-08 14:07:50 -04:00
Richard Feldman
7dc3a39edf Break cyclic dep 2025-04-08 14:07:40 -04:00
Richard Feldman
fc1b3889c2 wip 2025-04-08 13:37:59 -04:00
Richard Feldman
34c6fee959 Add ToolOutput 2025-04-08 12:52:18 -04:00
Richard Feldman
b25ec65b48 Add custom render method to Tool trait 2025-04-08 11:26:08 -04:00
170 changed files with 3444 additions and 7613 deletions

72
Cargo.lock generated
View File

@@ -52,7 +52,6 @@ dependencies = [
name = "agent"
version = "0.1.0"
dependencies = [
"agent_rules",
"anyhow",
"assistant_context_editor",
"assistant_settings",
@@ -162,20 +161,6 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "agent_rules"
version = "0.1.0"
dependencies = [
"anyhow",
"fs",
"gpui",
"indoc",
"prompt_store",
"util",
"workspace-hack",
"worktree",
]
[[package]]
name = "ahash"
version = "0.7.8"
@@ -761,8 +746,6 @@ dependencies = [
"itertools 0.14.0",
"language",
"language_model",
"log",
"lsp-types",
"open",
"project",
"rand 0.8.5",
@@ -770,7 +753,6 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"ui",
"unindent",
"util",
@@ -3347,15 +3329,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convert_case"
version = "0.8.0"
@@ -4488,32 +4461,6 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "documented"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc6db32f0995bc4553d2de888999075acd0dbeef75ba923503f6a724263dc6f3"
dependencies = [
"documented-macros",
"phf",
"thiserror 1.0.69",
]
[[package]]
name = "documented-macros"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a394bb35929b58f9a5fd418f7c6b17a4b616efcc1e53e6995ca123948f87e5fa"
dependencies = [
"convert_case 0.6.0",
"itertools 0.13.0",
"optfield",
"proc-macro2",
"quote",
"strum",
"syn 2.0.100",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
@@ -7928,7 +7875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -9595,17 +9542,6 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "optfield"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa59f025cde9c698fcb4fcb3533db4621795374065bee908215263488f2d2a1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -14191,14 +14127,12 @@ name = "tasks_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"debugger_ui",
"editor",
"feature_flags",
"file_icons",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"menu",
"picker",
@@ -15413,7 +15347,6 @@ version = "0.1.0"
dependencies = [
"chrono",
"component",
"documented",
"gpui",
"icons",
"itertools 0.14.0",
@@ -17679,7 +17612,6 @@ dependencies = [
"indexmap",
"inout",
"itertools 0.12.1",
"itertools 0.13.0",
"lazy_static",
"libc",
"libsqlite3-sys",
@@ -18108,7 +18040,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.183.0"
version = "0.182.0"
dependencies = [
"activity_indicator",
"agent",

View File

@@ -3,7 +3,6 @@ resolver = "2"
members = [
"crates/activity_indicator",
"crates/agent",
"crates/agent_rules",
"crates/anthropic",
"crates/askpass",
"crates/assets",
@@ -210,7 +209,6 @@ edition = "2024"
activity_indicator = { path = "crates/activity_indicator" }
agent = { path = "crates/agent" }
agent_rules = { path = "crates/agent_rules" }
ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
@@ -296,7 +294,6 @@ livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" }
lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "1fff0dd12e2071c5667327394cfec163d2a466ab" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
media = { path = "crates/media" }

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1331 11.3776C10.2754 10.6665 10.1331 9.78593 11.1998 8.53327C11.82 7.80489 12.2664 6.96894 12.2664 6.04456C12.2664 4.91305 11.8169 3.82788 11.0168 3.02778C10.2167 2.22769 9.13152 1.7782 8.00001 1.7782C6.8685 1.7782 5.78334 2.22769 4.98324 3.02778C4.18314 3.82788 3.73364 4.91305 3.73364 6.04456C3.73364 6.75562 3.87586 7.6089 4.80024 8.53327C5.86683 9.80679 5.72462 10.6665 5.86683 11.3776M10.1331 11.3776V12.8821C10.1331 13.622 9.53341 14.2218 8.79353 14.2218H7.2065C6.46662 14.2218 5.86683 13.622 5.86683 12.8821V11.3776M10.1331 11.3776H5.86683" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 751 B

View File

@@ -150,7 +150,7 @@
"context": "AgentDiff",
"bindings": {
"ctrl-y": "agent::Keep",
"ctrl-n": "agent::Reject"
"ctrl-k ctrl-r": "agent::Reject"
}
},
{
@@ -532,7 +532,6 @@
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
"shift-enter": "editor::ConfirmCompletionReplace",
"tab": "editor::ComposeCompletion"
}
},

View File

@@ -242,7 +242,7 @@
"use_key_equivalents": true,
"bindings": {
"cmd-y": "agent::Keep",
"cmd-n": "agent::Reject"
"cmd-alt-z": "agent::Reject"
}
},
{
@@ -681,7 +681,6 @@
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"shift-enter": "editor::ConfirmCompletionReplace",
"tab": "editor::ComposeCompletion"
}
},

View File

@@ -58,8 +58,7 @@
"ctrl-shift-home": "editor::SelectToBeginning",
"ctrl-shift-end": "editor::SelectToEnd",
"ctrl-f8": "editor::ToggleBreakpoint",
"ctrl-shift-f8": "editor::EditLogBreakpoint",
"ctrl-shift-u": "editor::ToggleCase"
"ctrl-shift-f8": "editor::EditLogBreakpoint"
}
},
{

View File

@@ -55,8 +55,7 @@
"cmd-shift-home": "editor::SelectToBeginning",
"cmd-shift-end": "editor::SelectToEnd",
"ctrl-f8": "editor::ToggleBreakpoint",
"ctrl-shift-f8": "editor::EditLogBreakpoint",
"cmd-shift-u": "editor::ToggleCase"
"ctrl-shift-f8": "editor::EditLogBreakpoint"
}
},
{

View File

@@ -16,8 +16,6 @@ no need to create backup files (e.g. `.bak` files) because these files will just
When attempting to resolve issues around failing tests, never simply remove the failing tests. Unless the user explicitly asks you to remove tests, ALWAYS attempt to fix the code causing the tests to fail.
Ignore "TODO"-type comments unless they're relevant to the user's explicit request or the user specifically asks you to address them. It is, however, okay to include them in codebase summaries.
<style>
Editing code:
- Make sure to take previous edits into account.
@@ -155,7 +153,7 @@ There are rules that apply to these root directories:
{{#each worktrees}}
{{#if rules_file}}
`{{root_name}}/{{rules_file.path_in_worktree}}`:
`{{root_name}}/{{rules_file.rel_path}}`:
``````
{{{rules_file.text}}}

View File

@@ -1 +0,0 @@
In your response, and also when thinking, make sure to remember and follow my instructions about how to format code blocks (and don't ever mention that you are remembering it, just follow the instructions).

View File

@@ -658,7 +658,6 @@
"tools": {
"bash": true,
"batch_tool": true,
"code_actions": true,
"code_symbols": true,
"copy_path": false,
"create_file": true,
@@ -672,7 +671,6 @@
"path_search": true,
"read_file": true,
"regex_search": true,
"rename": true,
"symbol_info": true,
"thinking": true
}
@@ -1138,8 +1136,7 @@
"code_actions_on_format": {},
// Settings related to running tasks.
"tasks": {
"variables": {},
"enabled": true
"variables": {}
},
// An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should
@@ -1459,8 +1456,6 @@
"lsp": {
// Specify the LSP name as a key here.
// "rust-analyzer": {
// // A special flag for rust-analyzer integration, to use server-provided tasks
// enable_lsp_tasks": true,
// // These initialization options are merged into Zed's defaults
// "initialization_options": {
// "check": {

View File

@@ -19,7 +19,6 @@ test-support = [
]
[dependencies]
agent_rules.workspace = true
anyhow.workspace = true
assistant_context_editor.workspace = true
assistant_settings.workspace = true

View File

@@ -21,7 +21,10 @@ use gpui::{
linear_color_stop, linear_gradient, list, percentage, pulsating_between,
};
use language::{Buffer, LanguageRegistry};
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelToolUseId, Role};
use language_model::{
ConfiguredModel, LanguageModelRegistry, LanguageModelToolUseId, Role, StringToolOutput,
ToolOutput,
};
use markdown::parser::CodeBlockKind;
use markdown::{Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, without_fences};
use project::ProjectItem as _;
@@ -36,6 +39,7 @@ use text::ToPoint;
use theme::ThemeSettings;
use ui::{Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*};
use util::ResultExt as _;
use util::markdown::MarkdownString;
use workspace::{OpenOptions, Workspace};
use crate::context_store::ContextStore;
@@ -74,7 +78,7 @@ struct RenderedMessage {
struct RenderedToolUse {
label: Entity<Markdown>,
input: Entity<Markdown>,
output: Entity<Markdown>,
output: ToolOutput,
}
impl RenderedMessage {
@@ -376,7 +380,7 @@ fn render_markdown_code_block(
.cursor_pointer()
.rounded_sm()
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
.tooltip(Tooltip::text("Jump to File"))
.tooltip(Tooltip::text("Jump to file"))
.children(
file_icons::FileIcons::get_icon(&path_range.path, cx)
.map(Icon::from_path)
@@ -456,7 +460,6 @@ fn render_markdown_code_block(
.contains(&(message_id, ix));
let codeblock_header = h_flex()
.group("codeblock_header")
.p_1()
.gap_1()
.justify_between()
@@ -466,47 +469,45 @@ fn render_markdown_code_block(
.rounded_t_md()
.children(label)
.child(
div().visible_on_hover("codeblock_header").child(
IconButton::new(
("copy-markdown-code", ix),
if codeblock_was_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code"))
.on_click({
let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix));
IconButton::new(
("copy-markdown-code", ix),
if codeblock_was_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code"))
.on_click({
let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix));
let code =
without_fences(&parsed_markdown.source()[codeblock_range.clone()])
.to_string();
let code =
without_fences(&parsed_markdown.source()[codeblock_range.clone()])
.to_string();
cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.update(|cx| {
this.update(cx, |this, cx| {
this.copied_code_block_ids.remove(&(message_id, ix));
cx.notify();
})
cx.update(|cx| {
this.update(cx, |this, cx| {
this.copied_code_block_ids.remove(&(message_id, ix));
cx.notify();
})
.ok();
})
.detach();
});
}
}),
),
.ok();
})
.detach();
});
}
}),
);
v_flex()
@@ -732,21 +733,28 @@ impl ActiveThread {
tool_use_id: LanguageModelToolUseId,
tool_label: impl Into<SharedString>,
tool_input: &serde_json::Value,
tool_output: SharedString,
tool_output: ToolOutput,
cx: &mut Context<Self>,
) {
let rendered = RenderedToolUse {
label: render_tool_use_markdown(tool_label.into(), self.language_registry.clone(), cx),
input: render_tool_use_markdown(
format!(
"```json\n{}\n```",
serde_json::to_string_pretty(tool_input).unwrap_or_default()
MarkdownString::code_block(
"json",
&serde_json::to_string_pretty(tool_input).unwrap_or_default(),
)
.to_string()
.into(),
self.language_registry.clone(),
cx,
),
output: render_tool_use_markdown(tool_output, self.language_registry.clone(), cx),
output: render_tool_use_markdown(
tool_output.clone(),
self.language_registry.clone(),
cx,
),
output: StringToolOutput::new(tool_output, language_registry: Arc<LanguageRegistry>),
};
self.rendered_tool_uses
.insert(tool_use_id.clone(), rendered);
@@ -1222,30 +1230,17 @@ impl ActiveThread {
Label::new("Generating")
.color(Color::Muted)
.size(LabelSize::Small)
.with_animations(
.with_animation(
"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);
}
_ => {}
}
Animation::new(Duration::from_secs(1)).repeat(),
|mut label, delta| {
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
},
)
@@ -1769,7 +1764,7 @@ impl ActiveThread {
None
};
v_flex()
div()
.text_ui(cx)
.gap_2()
.children(
@@ -1854,225 +1849,177 @@ impl ActiveThread {
.copied()
.unwrap_or_default();
let editor_bg = cx.theme().colors().panel_background;
let editor_bg = cx.theme().colors().editor_background;
div().map(|this| {
if pending {
this.v_flex()
.mt_neg_2()
.mb_1p5()
.child(
h_flex()
.group("disclosure-header")
.justify_between()
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::LightBulb)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child({
Label::new("Thinking")
.color(Color::Muted)
div().pt_0p5().pb_2().child(
v_flex()
.rounded_lg()
.border_1()
.border_color(self.tool_card_border_color(cx))
.child(
h_flex()
.group("disclosure-header")
.justify_between()
.py_1()
.px_2()
.bg(self.tool_card_header_bg(cx))
.map(|this| {
if pending || is_open {
this.rounded_t_md()
.border_b_1()
.border_color(self.tool_card_border_color(cx))
} else {
this.rounded_md()
}
})
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::Brain)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child({
if pending {
Label::new("Thinking…")
.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
},
)
.buffer_font(cx)
.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))
},
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
)
}),
)
.child(
h_flex()
.gap_1()
.child(
div().visible_on_hover("disclosure-header").child(
Disclosure::new("thinking-disclosure", is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener({
move |this, _event, _window, _cx| {
let is_open = this
.expanded_thinking_segments
.entry((message_id, ix))
.or_insert(false);
*is_open = !*is_open;
}
})),
),
)
.child({
Icon::new(IconName::ArrowCircle)
.color(Color::Accent)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(
percentage(delta),
))
},
)
}),
),
)
.when(!is_open, |this| {
let gradient_overlay = div()
.rounded_b_lg()
.h_full()
.absolute()
.w_full()
.bottom_0()
.left_0()
.bg(linear_gradient(
180.,
linear_color_stop(editor_bg, 1.),
linear_color_stop(editor_bg.opacity(0.2), 0.),
));
this.child(
div()
.relative()
.bg(editor_bg)
.rounded_b_lg()
.mt_2()
.pl_4()
.into_any_element()
} else {
Label::new("Thought Process")
.size(LabelSize::Small)
.buffer_font(cx)
.into_any_element()
}
}),
)
.child(
h_flex()
.gap_1()
.child(
div()
.id(("thinking-content", ix))
.max_h_20()
.track_scroll(scroll_handle)
.text_ui_sm(cx)
.overflow_hidden()
.child(
MarkdownElement::new(
markdown.clone(),
default_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(
text,
workspace.clone(),
window,
cx,
);
div().visible_on_hover("disclosure-header").child(
Disclosure::new("thinking-disclosure", is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener({
move |this, _event, _window, _cx| {
let is_open = this
.expanded_thinking_segments
.entry((message_id, ix))
.or_insert(false);
*is_open = !*is_open;
}
}),
),
})),
),
)
.child(gradient_overlay),
)
})
.when(is_open, |this| {
this.child(
div()
.id(("thinking-content", ix))
.h_full()
.bg(editor_bg)
.text_ui_sm(cx)
.child(
MarkdownElement::new(
markdown.clone(),
default_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
}
}),
),
)
})
} else {
this.v_flex()
.mt_neg_2()
.child(
h_flex()
.group("disclosure-header")
.pr_1()
.justify_between()
.opacity(0.8)
.hover(|style| style.opacity(1.))
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::LightBulb)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new("Thought Process").size(LabelSize::Small)),
)
.child(
div().visible_on_hover("disclosure-header").child(
Disclosure::new("thinking-disclosure", is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener({
move |this, _event, _window, _cx| {
let is_open = this
.expanded_thinking_segments
.entry((message_id, ix))
.or_insert(false);
.child({
let (icon_name, color, animated) = if pending {
(IconName::ArrowCircle, Color::Accent, true)
} else {
(IconName::Check, Color::Success, false)
};
*is_open = !*is_open;
let icon =
Icon::new(icon_name).color(color).size(IconSize::Small);
if animated {
icon.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(
delta,
)))
},
)
.into_any_element()
} else {
icon.into_any_element()
}
}),
),
)
.when(pending && !is_open, |this| {
let gradient_overlay = div()
.rounded_b_lg()
.h_20()
.absolute()
.w_full()
.bottom_0()
.left_0()
.bg(linear_gradient(
180.,
linear_color_stop(editor_bg, 1.),
linear_color_stop(editor_bg.opacity(0.2), 0.),
));
this.child(
div()
.relative()
.bg(editor_bg)
.rounded_b_lg()
.child(
div()
.id(("thinking-content", ix))
.p_2()
.h_20()
.track_scroll(scroll_handle)
.text_ui_sm(cx)
.child(
MarkdownElement::new(
markdown.clone(),
default_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(
text,
workspace.clone(),
window,
cx,
);
}
})),
),
),
}),
)
.overflow_hidden(),
)
.child(gradient_overlay),
)
.child(
})
.when(is_open, |this| {
this.child(
div()
.id(("thinking-content", ix))
.relative()
.mt_1p5()
.ml_1p5()
.pl_2p5()
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.h_full()
.p_2()
.rounded_b_lg()
.bg(editor_bg)
.text_ui_sm(cx)
.when(is_open, |this| {
this.child(
MarkdownElement::new(
markdown.clone(),
default_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
}
}),
.child(
MarkdownElement::new(
markdown.clone(),
default_markdown_style(window, cx),
)
}),
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
}
}),
),
)
}
})
}),
)
}
fn render_tool_use(
@@ -2094,7 +2041,6 @@ 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 status_icons = div().child(match &tool_use.status {
ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
@@ -2159,24 +2105,50 @@ impl ActiveThread {
results_content_container()
.border_t_1()
.border_color(self.tool_card_border_color(cx))
.child(
Label::new("Result")
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx),
)
.child(div().w_full().text_ui_sm(cx).children(
rendered_tool_use.as_ref().map(|rendered| {
MarkdownElement::new(
rendered.output.clone(),
tool_use_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
let tool_name = tool_use.name.to_string();
let tool_registry = assistant_tool::ToolRegistry::global(cx);
if let Some(_tool) = tool_registry.tool(&tool_name) {
// Tool doesn't have a render method, but ToolOutput does
match rendered.output.render(window, cx) {
Some(rendered) => rendered,
None => {
// Default to rendering the output as markdown
div()
.child(
Label::new("Result")
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx),
)
.child(
div().w_full().text_ui_sm(cx).child(
MarkdownElement::new(
rendered.output.clone(),
tool_use_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(
text,
workspace.clone(),
window,
cx,
);
}
}),
),
)
.into_any_element()
}
}
})
} else {
log::error!("Tool not found: {tool_name}");
gpui::Empty.into_any_element()
}
}),
)),
),
@@ -2223,16 +2195,30 @@ impl ActiveThread {
div()
.text_ui_sm(cx)
.children(rendered_tool_use.as_ref().map(|rendered| {
MarkdownElement::new(
rendered.output.clone(),
tool_use_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
}
})
let tool_name = tool_use.name.to_string();
let tool_registry = assistant_tool::ToolRegistry::global(cx);
tool_registry
.tool(&tool_name)
.and_then(|_tool| None) // Tool doesn't have a render method, but ToolOutput does
.unwrap_or_else(|| {
MarkdownElement::new(
rendered.output.clone(),
tool_use_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(
text,
workspace.clone(),
window,
cx,
);
}
})
.into_any_element()
})
})),
),
),
@@ -2271,10 +2257,10 @@ impl ActiveThread {
};
div().map(|element| {
if !edit_tools {
if !tool_use.needs_confirmation {
element.child(
v_flex()
.my_2()
.my_1p5()
.child(
h_flex()
.group("disclosure-header")
@@ -2581,7 +2567,7 @@ impl ActiveThread {
let label_text = match rules_files.as_slice() {
&[] => return div().into_any(),
&[rules_file] => {
format!("Using {:?} file", rules_file.path_in_worktree)
format!("Using {:?} file", rules_file.rel_path)
}
rules_files => {
format!("Using {} rules files", rules_files.len())

View File

@@ -227,14 +227,14 @@ impl AssistantPanel {
) -> Self {
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
let fs = workspace.app_state().fs.clone();
let project = workspace.project();
let project = workspace.project().clone();
let language_registry = project.read(cx).languages().clone();
let workspace = workspace.weak_handle();
let weak_self = cx.entity().downgrade();
let message_editor_context_store = cx.new(|_cx| {
crate::context_store::ContextStore::new(
project.downgrade(),
workspace.clone(),
Some(thread_store.downgrade()),
)
});
@@ -344,7 +344,7 @@ impl AssistantPanel {
let message_editor_context_store = cx.new(|_cx| {
crate::context_store::ContextStore::new(
self.project.downgrade(),
self.workspace.clone(),
Some(self.thread_store.downgrade()),
)
});
@@ -521,7 +521,7 @@ impl AssistantPanel {
this.set_active_view(thread_view, window, cx);
let message_editor_context_store = cx.new(|_cx| {
crate::context_store::ContextStore::new(
this.project.downgrade(),
this.workspace.clone(),
Some(this.thread_store.downgrade()),
)
});
@@ -855,11 +855,9 @@ impl AssistantPanel {
if is_empty {
Label::new(Thread::DEFAULT_SUMMARY.clone())
.truncate()
.ml_2()
.into_any_element()
} else if summary.is_none() {
Label::new(LOADING_SUMMARY_PLACEHOLDER)
.ml_2()
.truncate()
.into_any_element()
} else {
@@ -875,7 +873,7 @@ impl AssistantPanel {
})
.unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
Label::new(title).ml_2().truncate().into_any_element()
Label::new(title).truncate().into_any_element()
}
ActiveView::History => Label::new("History").truncate().into_any_element(),
ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
@@ -912,25 +910,23 @@ impl AssistantPanel {
let go_back_button = match &self.active_view {
ActiveView::History | ActiveView::Configuration => Some(
div().pl_1().child(
IconButton::new("go-back", IconName::ArrowLeft)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, window, cx| {
this.go_back(&workspace::GoBack, window, cx);
}))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Go Back",
&workspace::GoBack,
&focus_handle,
window,
cx,
)
}
}),
),
IconButton::new("go-back", IconName::ArrowLeft)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, window, cx| {
this.go_back(&workspace::GoBack, window, cx);
}))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Go Back",
&workspace::GoBack,
&focus_handle,
window,
cx,
)
}
}),
),
_ => None,
};
@@ -948,7 +944,8 @@ impl AssistantPanel {
.child(
h_flex()
.w_full()
.gap_1()
.pl_2()
.gap_2()
.children(go_back_button)
.child(self.render_title_view(window, cx)),
)
@@ -1083,7 +1080,7 @@ impl AssistantPanel {
cx,
|menu, _window, _cx| {
menu.action(
"New Text Thread",
"New Prompt Editor",
NewPromptEditor.boxed_clone(),
)
.when(!is_empty, |menu| {

View File

@@ -106,7 +106,7 @@ impl ContextPickerCompletionProvider {
.iter()
.map(|mode| {
Completion {
replace_range: source_range.clone(),
old_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()),
@@ -160,7 +160,7 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_thread(&thread_entry);
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
old_range: source_range.clone(),
new_text,
label: CodeLabel::plain(thread_entry.summary.to_string(), None),
documentation: None,
@@ -205,7 +205,7 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_fetch(&url_to_fetch);
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
old_range: source_range.clone(),
new_text,
label: CodeLabel::plain(url_to_fetch.to_string(), None),
documentation: None,
@@ -287,7 +287,7 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_file(&file_name, &full_path);
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
old_range: source_range.clone(),
new_text,
label,
documentation: None,
@@ -350,7 +350,7 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_symbol(&symbol.name, &full_path);
let new_text_len = new_text.len();
Some(Completion {
replace_range: source_range.clone(),
old_range: source_range.clone(),
new_text,
label,
documentation: None,
@@ -867,7 +867,7 @@ mod tests {
.expect("Opened test file wasn't an editor")
});
let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
let context_store = cx.new(|_| ContextStore::new(workspace.downgrade(), None));
let editor_entity = editor.downgrade();
editor.update_in(&mut cx, |editor, window, cx| {

View File

@@ -8,10 +8,11 @@ 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 project::{ProjectItem, ProjectPath, Worktree};
use rope::Rope;
use text::{Anchor, BufferId, OffsetRangeExt};
use util::{ResultExt as _, maybe};
use workspace::Workspace;
use crate::ThreadStore;
use crate::context::{
@@ -22,7 +23,7 @@ use crate::context_strip::SuggestedContext;
use crate::thread::{Thread, ThreadId};
pub struct ContextStore {
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
context: Vec<AssistantContext>,
thread_store: Option<WeakEntity<ThreadStore>>,
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
@@ -39,11 +40,11 @@ pub struct ContextStore {
impl ContextStore {
pub fn new(
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
) -> Self {
Self {
project,
workspace,
thread_store,
context: Vec::new(),
next_context_id: ContextId(0),
@@ -80,7 +81,12 @@ impl ContextStore {
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(project) = self.project.upgrade() else {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};
@@ -155,7 +161,11 @@ impl ContextStore {
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(project) = self.project.upgrade() else {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};

View File

@@ -262,7 +262,13 @@ impl InlineAssistant {
}
InlineAssistTarget::Terminal(active_terminal) => {
TerminalInlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_terminal, cx.entity(), thread_store, window, cx)
assistant.assist(
&active_terminal,
cx.entity().downgrade(),
thread_store,
window,
cx,
)
})
}
};
@@ -316,13 +322,6 @@ impl InlineAssistant {
window: &mut Window,
cx: &mut App,
) {
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().downgrade())
else {
return;
};
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
(
editor.snapshot(window, cx),
@@ -426,7 +425,7 @@ impl InlineAssistant {
for range in codegen_ranges {
let assist_id = self.next_assist_id.post_inc();
let context_store =
cx.new(|_cx| ContextStore::new(project.clone(), thread_store.clone()));
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
let codegen = cx.new(|cx| {
BufferCodegen::new(
editor.read(cx).buffer().clone(),
@@ -520,7 +519,7 @@ impl InlineAssistant {
initial_prompt: String,
initial_transaction_id: Option<TransactionId>,
focus: bool,
workspace: Entity<Workspace>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut App,
@@ -538,8 +537,8 @@ impl InlineAssistant {
range.end = range.end.bias_right(&snapshot);
}
let project = workspace.read(cx).project().downgrade();
let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
let context_store =
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
let codegen = cx.new(|cx| {
BufferCodegen::new(
@@ -563,7 +562,7 @@ impl InlineAssistant {
codegen.clone(),
self.fs.clone(),
context_store,
workspace.downgrade(),
workspace.clone(),
thread_store,
window,
cx,
@@ -590,7 +589,7 @@ impl InlineAssistant {
end_block_id,
range,
codegen.clone(),
workspace.downgrade(),
workspace.clone(),
window,
cx,
),
@@ -1780,7 +1779,6 @@ impl CodeActionProvider for AssistantCodeActionProvider {
let workspace = self.workspace.clone();
let thread_store = self.thread_store.clone();
window.spawn(cx, async move |cx| {
let workspace = workspace.upgrade().context("workspace was released")?;
let editor = editor.upgrade().context("editor was released")?;
let range = editor
.update(cx, |editor, cx| {

View File

@@ -66,7 +66,7 @@ impl TerminalInlineAssistant {
pub fn assist(
&mut self,
terminal_view: &Entity<TerminalView>,
workspace: Entity<Workspace>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut App,
@@ -75,8 +75,8 @@ impl TerminalInlineAssistant {
let assist_id = self.next_assist_id.post_inc();
let prompt_buffer =
cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local(String::new(), cx)), cx));
let project = workspace.read(cx).project().downgrade();
let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
let context_store =
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new(|cx| {
@@ -87,7 +87,7 @@ impl TerminalInlineAssistant {
codegen,
self.fs.clone(),
context_store.clone(),
workspace.downgrade(),
workspace.clone(),
thread_store.clone(),
window,
cx,
@@ -106,7 +106,7 @@ impl TerminalInlineAssistant {
assist_id,
terminal_view,
prompt_editor,
workspace.downgrade(),
workspace.clone(),
context_store,
window,
cx,

View File

@@ -3,7 +3,6 @@ use std::io::Write;
use std::ops::Range;
use std::sync::Arc;
use agent_rules::load_worktree_rules_file;
use anyhow::{Context as _, Result, anyhow};
use assistant_settings::AssistantSettings;
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
@@ -22,11 +21,13 @@ use language_model::{
};
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
use project::{Project, Worktree};
use prompt_store::{AssistantSystemPromptContext, PromptBuilder, WorktreeInfoForSystemPrompt};
use prompt_store::{
AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use util::{ResultExt as _, TryFutureExt as _, post_inc};
use util::{ResultExt as _, TryFutureExt as _, maybe, post_inc};
use uuid::Uuid;
use crate::context::{AssistantContext, ContextId, format_context_as_string};
@@ -853,36 +854,67 @@ impl Thread {
let root_name = worktree.root_name().into();
let abs_path = worktree.abs_path();
let rules_task = load_worktree_rules_file(fs, worktree, cx);
let Some(rules_task) = rules_task else {
return Task::ready((
// Note that Cline supports `.clinerules` being a directory, but that is not currently
// supported. This doesn't seem to occur often in GitHub repositories.
const RULES_FILE_NAMES: [&'static str; 6] = [
".rules",
".cursorrules",
".windsurfrules",
".clinerules",
".github/copilot-instructions.md",
"CLAUDE.md",
];
let selected_rules_file = RULES_FILE_NAMES
.into_iter()
.filter_map(|name| {
worktree
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| (entry.path.clone(), worktree.absolutize(&entry.path)))
})
.next();
if let Some((rel_rules_path, abs_rules_path)) = selected_rules_file {
cx.spawn(async move |_| {
let rules_file_result = maybe!(async move {
let abs_rules_path = abs_rules_path?;
let text = fs.load(&abs_rules_path).await.with_context(|| {
format!("Failed to load assistant rules file {:?}", abs_rules_path)
})?;
anyhow::Ok(RulesFile {
rel_path: rel_rules_path,
abs_path: abs_rules_path.into(),
text: text.trim().to_string(),
})
})
.await;
let (rules_file, rules_file_error) = match rules_file_result {
Ok(rules_file) => (Some(rules_file), None),
Err(err) => (
None,
Some(ThreadError::Message {
header: "Error loading rules file".into(),
message: format!("{err}").into(),
}),
),
};
let worktree_info = WorktreeInfoForSystemPrompt {
root_name,
abs_path,
rules_file,
};
(worktree_info, rules_file_error)
})
} else {
Task::ready((
WorktreeInfoForSystemPrompt {
root_name,
abs_path,
rules_file: None,
},
None,
));
};
cx.spawn(async move |_| {
let (rules_file, rules_file_error) = match rules_task.await {
Ok(rules_file) => (Some(rules_file), None),
Err(err) => (
None,
Some(ThreadError::Message {
header: "Error loading rules file".into(),
message: format!("{err}").into(),
}),
),
};
let worktree_info = WorktreeInfoForSystemPrompt {
root_name,
abs_path,
rules_file,
};
(worktree_info, rules_file_error)
})
))
}
}
pub fn send_to_model(
@@ -997,21 +1029,6 @@ impl Thread {
self.attached_tracked_files_state(&mut request.messages, cx);
// Add reminder to the last user message about
// easily-forgotten aspects of the system prompt.
if let Some(last_user_message) = request
.messages
.iter_mut()
.rev()
.find(|msg| msg.role == Role::User)
{
last_user_message
.content
.push(MessageContent::Text(system_prompt_reminder(
&self.prompt_builder,
)));
}
request
}
@@ -1397,7 +1414,7 @@ impl Thread {
for tool_use in pending_tool_uses.iter() {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
if tool.needs_confirmation(&tool_use.input, cx)
if tool.needs_confirmation()
&& !AssistantSettings::get_global(cx).always_allow_tool_actions
{
self.tool_use.confirm_tool_use(
@@ -1825,12 +1842,6 @@ impl Thread {
}
}
pub fn system_prompt_reminder(prompt_builder: &prompt_store::PromptBuilder) -> String {
prompt_builder
.generate_assistant_system_prompt_reminder()
.unwrap_or_default()
}
#[derive(Debug, Clone)]
pub enum ThreadError {
PaymentRequired,
@@ -1900,7 +1911,7 @@ mod tests {
)
.await;
let (_workspace, _thread_store, thread, context_store, prompt_builder) =
let (_workspace, _thread_store, thread, context_store) =
setup_test_environment(cx, project.clone()).await;
add_file_to_context(&project, &context_store, "test/code.rs", cx)
@@ -1954,14 +1965,8 @@ fn main() {{
});
assert_eq!(request.messages.len(), 1);
let actual_message = request.messages[0].string_contents();
let expected_content = format!(
"{}Please explain this code{}",
expected_context,
system_prompt_reminder(&prompt_builder)
);
assert_eq!(actual_message, expected_content);
let expected_full_message = format!("{}Please explain this code", expected_context);
assert_eq!(request.messages[0].string_contents(), expected_full_message);
}
#[gpui::test]
@@ -1978,7 +1983,7 @@ fn main() {{
)
.await;
let (_, _thread_store, thread, context_store, _prompt_builder) =
let (_, _thread_store, thread, context_store) =
setup_test_environment(cx, project.clone()).await;
// Open files individually
@@ -2078,7 +2083,7 @@ fn main() {{
)
.await;
let (_, _thread_store, thread, _context_store, prompt_builder) =
let (_, _thread_store, thread, _context_store) =
setup_test_environment(cx, project.clone()).await;
// Insert user message without any context (empty context vector)
@@ -2104,14 +2109,11 @@ fn main() {{
});
assert_eq!(request.messages.len(), 1);
let actual_message = request.messages[0].string_contents();
let expected_content = format!(
"What is the best way to learn Rust?{}",
system_prompt_reminder(&prompt_builder)
assert_eq!(
request.messages[0].string_contents(),
"What is the best way to learn Rust?"
);
assert_eq!(actual_message, expected_content);
// Add second message, also without context
let message2_id = thread.update(cx, |thread, cx| {
thread.insert_user_message("Are there any good books?", vec![], None, cx)
@@ -2127,17 +2129,14 @@ fn main() {{
});
assert_eq!(request.messages.len(), 2);
// First message should be the system prompt
assert_eq!(request.messages[0].role, Role::User);
// Second message should be the user message with prompt reminder
let actual_message = request.messages[1].string_contents();
let expected_content = format!(
"Are there any good books?{}",
system_prompt_reminder(&prompt_builder)
assert_eq!(
request.messages[0].string_contents(),
"What is the best way to learn Rust?"
);
assert_eq!(
request.messages[1].string_contents(),
"Are there any good books?"
);
assert_eq!(actual_message, expected_content);
}
#[gpui::test]
@@ -2150,7 +2149,7 @@ fn main() {{
)
.await;
let (_workspace, _thread_store, thread, context_store, prompt_builder) =
let (_workspace, _thread_store, thread, context_store) =
setup_test_environment(cx, project.clone()).await;
// Open buffer and add it to context
@@ -2210,14 +2209,11 @@ fn main() {{
// The last message should be the stale buffer notification
assert_eq!(last_message.role, Role::User);
let actual_message = last_message.string_contents();
let expected_content = format!(
"These files changed since last read:\n- code.rs\n{}",
system_prompt_reminder(&prompt_builder)
);
// Check the exact content of the message
let expected_content = "These files changed since last read:\n- code.rs\n";
assert_eq!(
actual_message, expected_content,
last_message.string_contents(),
expected_content,
"Last message should be exactly the stale buffer notification"
);
}
@@ -2255,27 +2251,24 @@ fn main() {{
Entity<ThreadStore>,
Entity<Thread>,
Entity<ContextStore>,
Arc<PromptBuilder>,
) {
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let thread_store = cx.update(|_, cx| {
ThreadStore::new(project.clone(), Arc::default(), prompt_builder.clone(), cx).unwrap()
ThreadStore::new(
project.clone(),
Arc::default(),
Arc::new(PromptBuilder::new(None).unwrap()),
cx,
)
.unwrap()
});
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
let context_store = cx.new(|_cx| ContextStore::new(workspace.downgrade(), None));
(
workspace,
thread_store,
thread,
context_store,
prompt_builder,
)
(workspace, thread_store, thread, context_store)
}
async fn add_file_to_context(

View File

@@ -431,6 +431,17 @@ impl RenderOnce for PastThread {
.end_slot(
h_flex()
.gap_1p5()
.child(
Label::new("Thread")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
div()
.size(px(3.))
.rounded_full()
.bg(cx.theme().colors().text_disabled),
)
.child(
Label::new(thread_timestamp)
.color(Color::Muted)
@@ -441,7 +452,12 @@ impl RenderOnce for PastThread {
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
Tooltip::for_action(
"Delete Thread",
&RemoveSelectedThread,
window,
cx,
)
})
.on_click({
let assistant_panel = self.assistant_panel.clone();
@@ -522,6 +538,17 @@ impl RenderOnce for PastContext {
.end_slot(
h_flex()
.gap_1p5()
.child(
Label::new("Prompt Editor")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
div()
.size(px(3.))
.rounded_full()
.bg(cx.theme().colors().text_disabled),
)
.child(
Label::new(context_timestamp)
.color(Color::Muted)
@@ -532,7 +559,12 @@ impl RenderOnce for PastContext {
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
Tooltip::for_action(
"Delete Prompt Editor",
&RemoveSelectedThread,
window,
cx,
)
})
.on_click({
let assistant_panel = self.assistant_panel.clone();

View File

@@ -6,6 +6,7 @@ use collections::HashMap;
use futures::FutureExt as _;
use futures::future::Shared;
use gpui::{App, SharedString, Task};
use language_model::ToolOutput;
use language_model::{
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
@@ -32,7 +33,7 @@ pub enum ToolUseStatus {
NeedsConfirmation,
Pending,
Running,
Finished(SharedString),
Finished(ToolOutput),
Error(SharedString),
}
@@ -131,6 +132,7 @@ impl ToolUseState {
tool_name: tool_use.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
tool_output: None,
},
);
}
@@ -153,6 +155,7 @@ impl ToolUseState {
tool_name: tool_use.name.clone(),
content: "Tool canceled by user".into(),
is_error: true,
tool_output: None,
},
);
pending_tools.push(tool_use.clone());
@@ -201,7 +204,7 @@ impl ToolUseState {
let (icon, needs_confirmation) = if let Some(tool) = self.tools.tool(&tool_use.name, cx)
{
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
(tool.icon(), tool.needs_confirmation())
} else {
(IconName::Cog, false)
};
@@ -331,11 +334,9 @@ impl ToolUseState {
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
output: Result<String>,
output: Result<ToolOutput>,
cx: &App,
) -> Option<PendingToolUse> {
telemetry::event!("Agent Tool Finished", tool_name, success = output.is_ok());
match output {
Ok(tool_result) => {
let model_registry = LanguageModelRegistry::read_global(cx);
@@ -348,16 +349,19 @@ impl ToolUseState {
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
.unwrap_or(usize::MAX);
let tool_result = if tool_result.len() <= tool_output_limit {
tool_result
} else {
let truncated = truncate_lines_to_byte_limit(&tool_result, tool_output_limit);
// Get string representation of the tool result
let response_text = tool_result.response_for_model();
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
// Check length and truncate if needed
let final_tool_result = if response_text.len() <= tool_output_limit {
response_text.to_string()
} else {
let response_string = response_text.to_string();
let truncated =
truncate_lines_to_byte_limit(&response_string, tool_output_limit);
let truncated_len = truncated.len();
format!("Tool result too long. The first {truncated_len} bytes:\n\n{truncated}")
};
self.tool_results.insert(
@@ -365,8 +369,9 @@ impl ToolUseState {
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: tool_result.into(),
content: Arc::from(final_tool_result),
is_error: false,
tool_output: Some(Arc::new(tool_result)),
},
);
self.pending_tool_uses_by_id.remove(&tool_use_id)
@@ -379,6 +384,7 @@ impl ToolUseState {
tool_name,
content: err.to_string().into(),
is_error: true,
tool_output: None,
},
);
@@ -453,6 +459,7 @@ impl ToolUseState {
} else {
tool_result.content.clone()
},
tool_output: tool_result.tool_output.clone(),
},
));
}

View File

@@ -1,25 +0,0 @@
[package]
name = "agent_rules"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/agent_rules.rs"
doctest = false
[dependencies]
anyhow.workspace = true
fs.workspace = true
gpui.workspace = true
prompt_store.workspace = true
util.workspace = true
worktree.workspace = true
workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
[dev-dependencies]
indoc.workspace = true

View File

@@ -1 +0,0 @@
../../LICENSE-GPL

View File

@@ -1,51 +0,0 @@
use std::sync::Arc;
use anyhow::{Context as _, Result};
use fs::Fs;
use gpui::{App, AppContext, Task};
use prompt_store::SystemPromptRulesFile;
use util::maybe;
use worktree::Worktree;
const RULES_FILE_NAMES: [&'static str; 6] = [
".rules",
".cursorrules",
".windsurfrules",
".clinerules",
".github/copilot-instructions.md",
"CLAUDE.md",
];
pub fn load_worktree_rules_file(
fs: Arc<dyn Fs>,
worktree: &Worktree,
cx: &App,
) -> Option<Task<Result<SystemPromptRulesFile>>> {
let selected_rules_file = RULES_FILE_NAMES
.into_iter()
.filter_map(|name| {
worktree
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| (entry.path.clone(), worktree.absolutize(&entry.path)))
})
.next();
// Note that Cline supports `.clinerules` being a directory, but that is not currently
// supported. This doesn't seem to occur often in GitHub repositories.
selected_rules_file.map(|(path_in_worktree, abs_path)| {
let fs = fs.clone();
cx.background_spawn(maybe!(async move {
let abs_path = abs_path?;
let text = fs
.load(&abs_path)
.await
.with_context(|| format!("Failed to load assistant rules file {:?}", abs_path))?;
anyhow::Ok(SystemPromptRulesFile {
path_in_worktree,
abs_path: abs_path.into(),
text: text.trim().to_string(),
})
}))
})
}

View File

@@ -2793,7 +2793,7 @@ fn render_thought_process_fold_icon_button(
let button = match status {
ThoughtProcessStatus::Pending => button
.child(
Icon::new(IconName::LightBulb)
Icon::new(IconName::Brain)
.size(IconSize::Small)
.color(Color::Muted),
)
@@ -2808,7 +2808,7 @@ fn render_thought_process_fold_icon_button(
),
ThoughtProcessStatus::Completed => button
.style(ButtonStyle::Filled)
.child(Icon::new(IconName::LightBulb).size(IconSize::Small))
.child(Icon::new(IconName::Brain).size(IconSize::Small))
.child(Label::new("Thought Process").single_line()),
};

View File

@@ -120,7 +120,7 @@ impl SlashCommandCompletionProvider {
) as Arc<_>
});
Some(project::Completion {
replace_range: name_range.clone(),
old_range: name_range.clone(),
documentation: Some(CompletionDocumentation::SingleLine(
command.description().into(),
)),
@@ -219,7 +219,7 @@ impl SlashCommandCompletionProvider {
}
project::Completion {
replace_range: if new_argument.replace_previous_arguments {
old_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()

View File

@@ -8,13 +8,15 @@ use std::fmt::Formatter;
use std::sync::Arc;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use gpui::{self, App, Entity, SharedString, Task};
use icons::IconName;
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use language_model::ToolOutput;
use project::Project;
pub use crate::action_log::*;
// StringToolOutput is now directly imported from language_model
pub use crate::tool_registry::*;
pub use crate::tool_working_set::*;
@@ -48,7 +50,7 @@ pub trait Tool: 'static + Send + Sync {
/// Returns true iff the tool needs the users's confirmation
/// before having permission to run.
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool;
fn needs_confirmation(&self) -> bool;
/// Returns the JSON schema that describes the tool's input.
fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> serde_json::Value {
@@ -66,7 +68,7 @@ pub trait Tool: 'static + Send + Sync {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>>;
) -> Task<Result<ToolOutput>>;
}
impl Debug for dyn Tool {

View File

@@ -23,18 +23,15 @@ http_client.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
lsp-types.workspace = true
open = { workspace = true }
project.workspace = true
regex.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
ui.workspace = true
util.workspace = true
worktree.workspace = true
open = { workspace = true }
workspace-hack.workspace = true
[dev-dependencies]

View File

@@ -1,6 +1,5 @@
mod bash_tool;
mod batch_tool;
mod code_action_tool;
mod code_symbols_tool;
mod copy_path_tool;
mod create_directory_tool;
@@ -14,10 +13,8 @@ mod move_path_tool;
mod now_tool;
mod open_tool;
mod path_search_tool;
mod quickfix_tool;
mod read_file_tool;
mod regex_search_tool;
mod rename_tool;
mod replace;
mod schema;
mod symbol_info_tool;
@@ -33,7 +30,6 @@ use move_path_tool::MovePathTool;
use crate::bash_tool::BashTool;
use crate::batch_tool::BatchTool;
use crate::code_action_tool::CodeActionTool;
use crate::code_symbols_tool::CodeSymbolsTool;
use crate::create_directory_tool::CreateDirectoryTool;
use crate::create_file_tool::CreateFileTool;
@@ -45,10 +41,8 @@ use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::open_tool::OpenTool;
use crate::path_search_tool::PathSearchTool;
use crate::quickfix_tool::QuickfixTool;
use crate::read_file_tool::ReadFileTool;
use crate::regex_search_tool::RegexSearchTool;
use crate::rename_tool::RenameTool;
use crate::symbol_info_tool::SymbolInfoTool;
use crate::thinking_tool::ThinkingTool;
@@ -64,7 +58,6 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(DeletePathTool);
registry.register_tool(FindReplaceFileTool);
registry.register_tool(SymbolInfoTool);
registry.register_tool(CodeActionTool);
registry.register_tool(MovePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(ListDirectoryTool);
@@ -72,10 +65,8 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(OpenTool);
registry.register_tool(CodeSymbolsTool);
registry.register_tool(PathSearchTool);
registry.register_tool(QuickfixTool);
registry.register_tool(ReadFileTool);
registry.register_tool(RegexSearchTool);
registry.register_tool(RenameTool);
registry.register_tool(ThinkingTool);
registry.register_tool(FetchTool::new(http_client));
}

View File

@@ -1,9 +1,10 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use futures::io::BufReader;
use futures::{AsyncBufReadExt, AsyncReadExt};
use gpui::{App, AppContext, Entity, Task};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -29,7 +30,7 @@ impl Tool for BashTool {
"bash".to_string()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -76,7 +77,7 @@ impl Tool for BashTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input: BashToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -123,184 +124,90 @@ impl Tool for BashTool {
worktree.read(cx).abs_path()
};
cx.background_spawn(run_command_limited(working_dir, input.command))
}
}
const LIMIT: usize = 16 * 1024;
async fn run_command_limited(working_dir: Arc<Path>, command: String) -> Result<String> {
// Add 2>&1 to merge stderr into stdout for proper interleaving.
let command = format!("({}) 2>&1", command);
let mut cmd = new_smol_command("bash")
.arg("-c")
.arg(&command)
.current_dir(working_dir)
.stdout(std::process::Stdio::piped())
.spawn()
.context("Failed to execute bash command")?;
// Capture stdout with a limit
let stdout = cmd.stdout.take().unwrap();
let mut reader = BufReader::new(stdout);
// Read one more byte to determine whether the output was truncated
let mut buffer = vec![0; LIMIT + 1];
let mut bytes_read = 0;
// Read until we reach the limit
loop {
let read = reader.read(&mut buffer[bytes_read..]).await?;
if read == 0 {
break;
}
bytes_read += read;
if bytes_read > LIMIT {
bytes_read = LIMIT + 1;
break;
}
}
// Repeatedly fill the output reader's buffer without copying it.
loop {
let skipped_bytes = reader.fill_buf().await?;
if skipped_bytes.is_empty() {
break;
}
let skipped_bytes_len = skipped_bytes.len();
reader.consume_unpin(skipped_bytes_len);
}
let output_bytes = &buffer[..bytes_read.min(LIMIT)];
let status = cmd.status().await.context("Failed to get command status")?;
let output_string = if bytes_read > LIMIT {
// Valid to find `\n` in UTF-8 since 0-127 ASCII characters are not used in
// multi-byte characters.
let last_line_ix = output_bytes.iter().rposition(|b| *b == b'\n');
let until_last_line = &output_bytes[..last_line_ix.unwrap_or(output_bytes.len())];
let output_string = String::from_utf8_lossy(until_last_line);
format!(
"Command output too long. The first {} bytes:\n\n{}",
output_string.len(),
output_block(&output_string),
)
} else {
output_block(&String::from_utf8_lossy(&output_bytes))
};
let output_with_status = if status.success() {
if output_string.is_empty() {
"Command executed successfully.".to_string()
} else {
output_string.to_string()
}
} else {
format!(
"Command failed with exit code {}\n\n{}",
status.code().unwrap_or(-1),
output_string,
)
};
Ok(output_with_status)
}
fn output_block(output: &str) -> String {
format!(
"```\n{}{}```",
output,
if output.ends_with('\n') { "" } else { "\n" }
)
}
#[cfg(test)]
#[cfg(not(windows))]
mod tests {
use gpui::TestAppContext;
use super::*;
#[gpui::test]
async fn test_run_command_simple(cx: &mut TestAppContext) {
cx.executor().allow_parking();
let result =
run_command_limited(Path::new(".").into(), "echo 'Hello, World!'".to_string()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "```\nHello, World!\n```");
}
#[gpui::test]
async fn test_interleaved_stdout_stderr(cx: &mut TestAppContext) {
cx.executor().allow_parking();
let command =
"echo 'stdout 1' && echo 'stderr 1' >&2 && echo 'stdout 2' && echo 'stderr 2' >&2";
let result = run_command_limited(Path::new(".").into(), command.to_string()).await;
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
"```\nstdout 1\nstderr 1\nstdout 2\nstderr 2\n```"
);
}
#[gpui::test]
async fn test_multiple_output_reads(cx: &mut TestAppContext) {
cx.executor().allow_parking();
// Command with multiple outputs that might require multiple reads
let result = run_command_limited(
Path::new(".").into(),
"echo '1'; sleep 0.01; echo '2'; sleep 0.01; echo '3'".to_string(),
)
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "```\n1\n2\n3\n```");
}
#[gpui::test]
async fn test_output_truncation_single_line(cx: &mut TestAppContext) {
cx.executor().allow_parking();
let cmd = format!("echo '{}';", "X".repeat(LIMIT * 2));
let result = run_command_limited(Path::new(".").into(), cmd).await;
assert!(result.is_ok());
let output = result.unwrap();
let content_start = output.find("```\n").map(|i| i + 4).unwrap_or(0);
let content_end = output.rfind("\n```").unwrap_or(output.len());
let content_length = content_end - content_start;
// Output should be exactly the limit
assert_eq!(content_length, LIMIT);
}
#[gpui::test]
async fn test_output_truncation_multiline(cx: &mut TestAppContext) {
cx.executor().allow_parking();
let cmd = format!("echo '{}'; ", "X".repeat(120)).repeat(160);
let result = run_command_limited(Path::new(".").into(), cmd).await;
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.starts_with("Command output too long. The first 16334 bytes:\n\n"));
let content_start = output.find("```\n").map(|i| i + 4).unwrap_or(0);
let content_end = output.rfind("\n```").unwrap_or(output.len());
let content_length = content_end - content_start;
assert!(content_length <= LIMIT);
cx.spawn(async move |_| -> Result<ToolOutput> {
// Add 2>&1 to merge stderr into stdout for proper interleaving.
let command = format!("({}) 2>&1", input.command);
let mut cmd = new_smol_command("bash")
.arg("-c")
.arg(&command)
.current_dir(working_dir)
.stdout(std::process::Stdio::piped())
.spawn()
.context("Failed to execute bash command")?;
// Capture stdout with a limit
let stdout = cmd.stdout.take().unwrap();
let mut reader = BufReader::new(stdout);
const MESSAGE_1: &str = "Command output too long. The first ";
const MESSAGE_2: &str = " bytes:\n\n";
const ERR_MESSAGE_1: &str = "Command failed with exit code ";
const ERR_MESSAGE_2: &str = "\n\n";
const STDOUT_LIMIT: usize = 8192;
const LIMIT: usize = STDOUT_LIMIT
- (MESSAGE_1.len()
+ (STDOUT_LIMIT.ilog10() as usize + 1) // byte count
+ MESSAGE_2.len()
+ ERR_MESSAGE_1.len()
+ 3 // status code
+ ERR_MESSAGE_2.len());
// Read one more byte to determine whether the output was truncated
let mut buffer = vec![0; LIMIT + 1];
let bytes_read = reader.read(&mut buffer).await?;
// Repeatedly fill the output reader's buffer without copying it.
loop {
let skipped_bytes = reader.fill_buf().await?;
if skipped_bytes.is_empty() {
break;
}
let skipped_bytes_len = skipped_bytes.len();
reader.consume_unpin(skipped_bytes_len);
}
let output_bytes = &buffer[..bytes_read];
// Let the process continue running
let status = cmd.status().await.context("Failed to get command status")?;
let output_string = if bytes_read > LIMIT {
// Valid to find `\n` in UTF-8 since 0-127 ASCII characters are not used in
// multi-byte characters.
let last_line_ix = output_bytes.iter().rposition(|b| *b == b'\n');
let output_string = String::from_utf8_lossy(
&output_bytes[..last_line_ix.unwrap_or(output_bytes.len())],
);
format!(
"{}{}{}{}",
MESSAGE_1,
output_string.len(),
MESSAGE_2,
output_string
)
} else {
String::from_utf8_lossy(&output_bytes).into()
};
if status.success() {
if output_string.is_empty() {
Ok(StringToolOutput::new("Command executed successfully."))
} else {
Ok(StringToolOutput::new(output_string))
}
} else {
Ok(StringToolOutput::new(format!(
"{}{}{}{}",
ERR_MESSAGE_1,
status.code().unwrap_or(-1),
ERR_MESSAGE_2,
output_string,
)))
}
})
}
}

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
use language_model::{ToolOutput, StringToolOutput};
use futures::future::join_all;
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -151,17 +152,8 @@ impl Tool for BatchTool {
"batch_tool".into()
}
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool {
serde_json::from_value::<BatchToolInput>(input.clone())
.map(|input| {
let working_set = ToolWorkingSet::default();
input.invocations.iter().any(|invocation| {
working_set
.tool(&invocation.name, cx)
.map_or(false, |tool| tool.needs_confirmation(&invocation.input, cx))
})
})
.unwrap_or(false)
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
@@ -219,7 +211,7 @@ impl Tool for BatchTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<BatchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -289,7 +281,7 @@ impl Tool for BatchTool {
match result {
Ok(output) => {
formatted_results
.push_str(&format!("Tool '{}' result:\n{}\n\n", tool_name, output));
.push_str(&format!("Tool '{}' result:\n{}\n\n", tool_name, output.response_for_model()));
}
Err(err) => {
error_occurred = true;
@@ -304,7 +296,7 @@ impl Tool for BatchTool {
.push_str("Note: Some tool invocations failed. See individual results above.");
}
Ok(formatted_results.trim().to_string())
Ok(StringToolOutput::new(formatted_results.trim().to_string()))
})
}
}

View File

@@ -1,389 +0,0 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, Entity, Task};
use language::{self, Anchor, Buffer, ToPointUtf16};
use language_model::LanguageModelRequestMessage;
use project::{self, LspAction, Project};
use regex::Regex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{ops::Range, sync::Arc};
use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CodeActionToolInput {
/// The relative path to the file containing the text range.
///
/// WARNING: you MUST start this path with one of the project's root directories.
pub path: String,
/// The specific code action to execute.
///
/// If this field is provided, the tool will execute the specified action.
/// If omitted, the tool will list all available code actions for the text range.
///
/// Here are some actions that are commonly supported (but may not be for this particular
/// text range; you can omit this field to list all the actions, if you want to know
/// what your options are, or you can just try an action and if it fails I'll tell you
/// what the available actions were instead):
/// - "quickfix.all" - applies all available quick fixes in the range
/// - "source.organizeImports" - sorts and cleans up import statements
/// - "source.fixAll" - applies all available auto fixes
/// - "refactor.extract" - extracts selected code into a new function or variable
/// - "refactor.inline" - inlines a variable by replacing references with its value
/// - "refactor.rewrite" - general code rewriting operations
/// - "source.addMissingImports" - adds imports for references that lack them
/// - "source.removeUnusedImports" - removes imports that aren't being used
/// - "source.implementInterface" - generates methods required by an interface/trait
/// - "source.generateAccessors" - creates getter/setter methods
/// - "source.convertToAsyncFunction" - converts callback-style code to async/await
///
/// Also, there is a special case: if you specify exactly "textDocument/rename" as the action,
/// then this will rename the symbol to whatever string you specified for the `arguments` field.
pub action: Option<String>,
/// Optional arguments to pass to the code action.
///
/// For rename operations (when action="textDocument/rename"), this should contain the new name.
/// For other code actions, these arguments may be passed to the language server.
pub arguments: Option<serde_json::Value>,
/// The text that comes immediately before the text range in the file.
pub context_before_range: String,
/// The text range. This text must appear in the file right between `context_before_range`
/// and `context_after_range`.
///
/// The file must contain exactly one occurrence of `context_before_range` followed by
/// `text_range` followed by `context_after_range`. If the file contains zero occurrences,
/// or if it contains more than one occurrence, the tool will fail, so it is absolutely
/// critical that you verify ahead of time that the string is unique. You can search
/// the file's contents to verify this ahead of time.
///
/// To make the string more likely to be unique, include a minimum of 1 line of context
/// before the text range, as well as a minimum of 1 line of context after the text range.
/// If these lines of context are not enough to obtain a string that appears only once
/// in the file, then double the number of context lines until the string becomes unique.
/// (Start with 1 line before and 1 line after though, because too much context is
/// needlessly costly.)
///
/// Do not alter the context lines of code in any way, and make sure to preserve all
/// whitespace and indentation for all lines of code. The combined string must be exactly
/// as it appears in the file, or else this tool call will fail.
pub text_range: String,
/// The text that comes immediately after the text range in the file.
pub context_after_range: String,
}
pub struct CodeActionTool;
impl Tool for CodeActionTool {
fn name(&self) -> String {
"code_actions".into()
}
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./code_action_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Wand
}
fn input_schema(
&self,
_format: language_model::LanguageModelToolSchemaFormat,
) -> serde_json::Value {
let schema = schemars::schema_for!(CodeActionToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CodeActionToolInput>(input.clone()) {
Ok(input) => {
if let Some(action) = &input.action {
if action == "textDocument/rename" {
let new_name = match &input.arguments {
Some(serde_json::Value::String(new_name)) => new_name.clone(),
Some(value) => {
if let Ok(new_name) =
serde_json::from_value::<String>(value.clone())
{
new_name
} else {
"invalid name".to_string()
}
}
None => "missing name".to_string(),
};
format!("Rename '{}' to '{}'", input.text_range, new_name)
} else {
format!(
"Execute code action '{}' for '{}'",
action, input.text_range
)
}
} else {
format!("List available code actions for '{}'", input.text_range)
}
}
Err(_) => "Perform code action".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<CodeActionToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
cx.spawn(async move |cx| {
let buffer = {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
.context("Path not found in project")
})??;
project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
};
action_log.update(cx, |action_log, cx| {
action_log.buffer_read(buffer.clone(), cx);
})?;
let range = {
let Some(range) = buffer.read_with(cx, |buffer, _cx| {
find_text_range(&buffer, &input.context_before_range, &input.text_range, &input.context_after_range)
})? else {
return Err(anyhow!(
"Failed to locate the text specified by context_before_range, text_range, and context_after_range. Make sure context_before_range and context_after_range each match exactly once in the file."
));
};
range
};
if let Some(action_type) = &input.action {
// Special-case the `rename` operation
let response = if action_type == "textDocument/rename" {
let Some(new_name) = input.arguments.and_then(|args| serde_json::from_value::<String>(args).ok()) else {
return Err(anyhow!("For rename operations, 'arguments' must be a string containing the new name"));
};
let position = buffer.read_with(cx, |buffer, _| {
range.start.to_point_utf16(&buffer.snapshot())
})?;
project
.update(cx, |project, cx| {
project.perform_rename(buffer.clone(), position, new_name.clone(), cx)
})?
.await?;
format!("Renamed '{}' to '{}'", input.text_range, new_name)
} else {
// Get code actions for the range
let actions = project
.update(cx, |project, cx| {
project.code_actions(&buffer, range.clone(), None, cx)
})?
.await?;
if actions.is_empty() {
return Err(anyhow!("No code actions available for this range"));
}
// Find all matching actions
let regex = match Regex::new(action_type) {
Ok(regex) => regex,
Err(err) => return Err(anyhow!("Invalid regex pattern: {}", err)),
};
let mut matching_actions = actions
.into_iter()
.filter(|action| { regex.is_match(action.lsp_action.title()) });
let Some(action) = matching_actions.next() else {
return Err(anyhow!("No code actions match the pattern: {}", action_type));
};
// There should have been exactly one matching action.
if let Some(second) = matching_actions.next() {
let mut all_matches = vec![action, second];
all_matches.extend(matching_actions);
return Err(anyhow!(
"Pattern '{}' matches multiple code actions: {}",
action_type,
all_matches.into_iter().map(|action| action.lsp_action.title().to_string()).collect::<Vec<_>>().join(", ")
));
}
let title = action.lsp_action.title().to_string();
project
.update(cx, |project, cx| {
project.apply_code_action(buffer.clone(), action, true, cx)
})?
.await?;
format!("Completed code action: {}", title)
};
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
})?;
Ok(response)
} else {
// No action specified, so list the available ones.
let (position_start, position_end) = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
(
range.start.to_point_utf16(&snapshot),
range.end.to_point_utf16(&snapshot)
)
})?;
// Convert position to display coordinates (1-based)
let position_start_display = language::Point {
row: position_start.row + 1,
column: position_start.column + 1,
};
let position_end_display = language::Point {
row: position_end.row + 1,
column: position_end.column + 1,
};
// Get code actions for the range
let actions = project
.update(cx, |project, cx| {
project.code_actions(&buffer, range.clone(), None, cx)
})?
.await?;
let mut response = format!(
"Available code actions for text range '{}' at position {}:{} to {}:{} (UTF-16 coordinates):\n\n",
input.text_range,
position_start_display.row, position_start_display.column,
position_end_display.row, position_end_display.column
);
if actions.is_empty() {
response.push_str("No code actions available for this range.");
} else {
for (i, action) in actions.iter().enumerate() {
let title = match &action.lsp_action {
LspAction::Action(code_action) => code_action.title.as_str(),
LspAction::Command(command) => command.title.as_str(),
LspAction::CodeLens(code_lens) => {
if let Some(cmd) = &code_lens.command {
cmd.title.as_str()
} else {
"Unknown code lens"
}
},
};
let kind = match &action.lsp_action {
LspAction::Action(code_action) => {
if let Some(kind) = &code_action.kind {
kind.as_str()
} else {
"unknown"
}
},
LspAction::Command(_) => "command",
LspAction::CodeLens(_) => "code_lens",
};
response.push_str(&format!("{}. {title} ({kind})\n", i + 1));
}
}
Ok(response)
}
})
}
}
/// Finds the range of the text in the buffer, if it appears between context_before_range
/// and context_after_range, and if that combined string has one unique result in the buffer.
///
/// If an exact match fails, it tries adding a newline to the end of context_before_range and
/// to the beginning of context_after_range to accommodate line-based context matching.
fn find_text_range(
buffer: &Buffer,
context_before_range: &str,
text_range: &str,
context_after_range: &str,
) -> Option<Range<Anchor>> {
let snapshot = buffer.snapshot();
let text = snapshot.text();
// First try with exact match
let search_string = format!("{context_before_range}{text_range}{context_after_range}");
let mut positions = text.match_indices(&search_string);
let position_result = positions.next();
if let Some(position) = position_result {
// Check if the matched string is unique
if positions.next().is_none() {
let range_start = position.0 + context_before_range.len();
let range_end = range_start + text_range.len();
let range_start_anchor = snapshot.anchor_before(snapshot.offset_to_point(range_start));
let range_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(range_end));
return Some(range_start_anchor..range_end_anchor);
}
}
// If exact match fails or is not unique, try with line-based context
// Add a newline to the end of before context and beginning of after context
let line_based_before = if context_before_range.ends_with('\n') {
context_before_range.to_string()
} else {
format!("{context_before_range}\n")
};
let line_based_after = if context_after_range.starts_with('\n') {
context_after_range.to_string()
} else {
format!("\n{context_after_range}")
};
let line_search_string = format!("{line_based_before}{text_range}{line_based_after}");
let mut line_positions = text.match_indices(&line_search_string);
let line_position = line_positions.next()?;
// The line-based search string must also appear exactly once
if line_positions.next().is_some() {
return None;
}
let line_range_start = line_position.0 + line_based_before.len();
let line_range_end = line_range_start + text_range.len();
let line_range_start_anchor =
snapshot.anchor_before(snapshot.offset_to_point(line_range_start));
let line_range_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(line_range_end));
Some(line_range_start_anchor..line_range_end_anchor)
}

View File

@@ -1,19 +0,0 @@
A tool for applying code actions to specific sections of your code. It uses language servers to provide refactoring capabilities similar to what you'd find in an IDE.
This tool can:
- List all available code actions for a selected text range
- Execute a specific code action on that range
- Rename symbols across your codebase. This tool is the preferred way to rename things, and you should always prefer to rename code symbols using this tool rather than using textual find/replace when both are available.
Use this tool when you want to:
- Discover what code actions are available for a piece of code
- Apply automatic fixes and code transformations
- Rename variables, functions, or other symbols consistently throughout your project
- Clean up imports, implement interfaces, or perform other language-specific operations
- If unsure what actions are available, call the tool without specifying an action to get a list
- For common operations, you can directly specify actions like "quickfix.all" or "source.organizeImports"
- For renaming, use the special "textDocument/rename" action and provide the new name in the arguments field
- Be specific with your text range and context to ensure the tool identifies the correct code location
The tool will automatically save any changes it makes to your files.

View File

@@ -5,6 +5,7 @@ use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use collections::IndexMap;
use gpui::{App, AsyncApp, Entity, Task};
use language::{OutlineItem, ParseStatus, Point};
@@ -79,7 +80,7 @@ impl Tool for CodeSymbolsTool {
"code_symbols".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -129,7 +130,7 @@ impl Tool for CodeSymbolsTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -147,8 +148,8 @@ impl Tool for CodeSymbolsTool {
};
cx.spawn(async move |cx| match input.path {
Some(path) => file_outline(project, path, action_log, regex, input.offset, cx).await,
None => project_symbols(project, regex, input.offset, cx).await,
Some(path) => file_outline(project, path, action_log, regex, input.offset, cx).await.map(StringToolOutput::new),
None => project_symbols(project, regex, input.offset, cx).await.map(StringToolOutput::new),
})
}
}

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -43,7 +44,7 @@ impl Tool for CopyPathTool {
"copy_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -77,7 +78,7 @@ impl Tool for CopyPathTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<CopyPathToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -105,10 +106,10 @@ impl Tool for CopyPathTool {
cx.background_spawn(async move {
match copy_task.await {
Ok(_) => Ok(format!(
Ok(_) => Ok(StringToolOutput::new(format!(
"Copied {} to {}",
input.source_path, input.destination_path
)),
))),
Err(err) => Err(anyhow!(
"Failed to copy {} to {}: {}",
input.source_path,

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -33,7 +34,7 @@ impl Tool for CreateDirectoryTool {
"create_directory".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -68,7 +69,7 @@ impl Tool for CreateDirectoryTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -87,7 +88,7 @@ impl Tool for CreateDirectoryTool {
.await
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
Ok(format!("Created directory {destination_path}"))
Ok(StringToolOutput::new(format!("Created directory {destination_path}")))
})
}
}

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -40,7 +41,7 @@ impl Tool for CreateFileTool {
"create_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -73,7 +74,7 @@ impl Tool for CreateFileTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -104,7 +105,7 @@ impl Tool for CreateFileTool {
.await
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
Ok(format!("Created file {destination_path}"))
Ok(StringToolOutput::new(format!("Created file {destination_path}")))
})
}
}

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -33,7 +34,7 @@ impl Tool for DeletePathTool {
"delete_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -63,7 +64,7 @@ impl Tool for DeletePathTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
Ok(input) => input.path,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -124,7 +125,7 @@ impl Tool for DeletePathTool {
match delete {
Some(deletion_task) => match deletion_task.await {
Ok(()) => Ok(format!("Deleted {path_str}")),
Ok(()) => Ok(StringToolOutput::new(format!("Deleted {path_str}"))),
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
},
None => Err(anyhow!(

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -11,58 +12,32 @@ use std::{fmt::Write, path::Path, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, Default, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsToolInput {
/// The specific paths to get detailed diagnostics for (including individual line numbers).
/// The path to get diagnostics for. If not provided, returns a project-wide summary.
///
/// Regardless of whether any paths are specified here, a count of the total number of warnings
/// and errors in the project will be reported, so providing paths here gets you strictly
/// more information.
///
/// These paths should never be absolute, and the first component
/// of each path should always be a root directory in a project.
/// This path should never be absolute, and the first component
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following root directories:
///
/// - lorem
/// - ipsum
/// - amet
///
/// If you want detailed diagnostics with line numbers for `dolor.txt` in `ipsum` and `consectetur.txt` in `amet`, you should use:
///
/// "paths": ["ipsum/dolor.txt", "amet/consectetur.txt"]
/// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
/// </example>
#[serde(deserialize_with = "deserialize_path")]
#[serde(default)]
pub paths: Vec<String>,
/// Which severity levels to show. Default is all.
/// To show only errors and warnings, you should use:
///
/// "severity": ["error", "warning"]
#[serde(default)]
pub severity: Vec<Severity>,
pub path: Option<String>,
}
#[derive(
Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Copy, Clone, strum::Display, Hash,
)]
#[serde(rename_all = "camelCase")]
pub enum Severity {
Error,
Warning,
Information,
Hint,
}
fn deserialize_path<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
fn deserialize_path<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let paths = Vec::<String>::deserialize(deserializer)?;
// The model passes an empty string for some paths
Ok(paths.into_iter().filter(|s| !s.is_empty()).collect())
let opt = Option::<String>::deserialize(deserializer)?;
// The model passes an empty string sometimes
Ok(opt.filter(|s| !s.is_empty()))
}
pub struct DiagnosticsTool;
@@ -72,7 +47,7 @@ impl Tool for DiagnosticsTool {
"diagnostics".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -89,21 +64,17 @@ impl Tool for DiagnosticsTool {
}
fn ui_text(&self, input: &serde_json::Value) -> String {
serde_json::from_value::<DiagnosticsToolInput>(input.clone())
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
.ok()
.and_then(|input| {
input.paths.first().map(|first_path| {
if input.paths.len() > 1 {
format!("Check diagnostics for {} paths", input.paths.len())
} else {
format!(
"Check diagnostics for {}",
MarkdownString::inline_code(first_path)
)
}
})
.and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(MarkdownString::inline_code(&path)),
_ => None,
})
.unwrap_or_else(|| "Check project diagnostics".to_string())
{
format!("Check diagnostics for {path}")
} else {
"Check project diagnostics".to_string()
}
}
fn run(
@@ -113,21 +84,63 @@ impl Tool for DiagnosticsTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = serde_json::from_value::<DiagnosticsToolInput>(input).unwrap_or_default();
let severity_filter = input.severity;
let mut summary_output = String::new();
let mut has_diagnostics = false;
// Always report the global diagnostics summary.
) -> Task<Result<ToolOutput>> {
match serde_json::from_value::<DiagnosticsToolInput>(input)
.ok()
.and_then(|input| input.path)
{
let project = project.read(cx);
Some(path) if !path.is_empty() => {
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
};
let buffer =
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(async move |cx| {
let mut output = String::new();
let buffer = buffer.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
let range = entry.range.to_point(&snapshot);
let severity = match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => "error",
DiagnosticSeverity::WARNING => "warning",
_ => continue,
};
writeln!(
output,
"{} at line {}: {}",
severity,
range.start.row + 1,
entry.diagnostic.message
)?;
}
if output.is_empty() {
Ok(StringToolOutput::new("File doesn't have errors or warnings!"))
} else {
Ok(StringToolOutput::new(output))
}
})
}
_ => {
let project = project.read(cx);
let mut output = String::new();
let mut has_diagnostics = false;
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
if summary.error_count > 0 || summary.warning_count > 0 {
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
else {
continue;
};
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
if summary.error_count > 0 || summary.warning_count > 0 {
if let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) {
has_diagnostics = true;
summary_output.push_str(&format!(
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
Path::new(worktree.read(cx).root_name())
.join(project_path.path)
@@ -137,113 +150,17 @@ impl Tool for DiagnosticsTool {
));
}
}
}
action_log.update(cx, |action_log, _cx| {
action_log.checked_project_diagnostics();
});
}
if input.paths.is_empty() {
// If no paths specified, just return the summary
if has_diagnostics {
Task::ready(Ok(summary_output))
} else {
Task::ready(Ok("No errors or warnings found.".to_string()))
}
} else {
let buffer_tasks = input
.paths
.into_iter()
.filter_map(|path| {
project
.read(cx)
.find_project_path(&path, cx)
.map(|project_path| {
(
project_path.clone(),
project.update(cx, |project, cx| {
project.open_buffer(project_path, cx)
}),
)
})
})
.collect::<Vec<_>>();
cx.spawn(async move |cx| {
let mut output = String::new();
output.push_str("# Project Summary\n\n");
action_log.update(cx, |action_log, _cx| {
action_log.checked_project_diagnostics();
});
if has_diagnostics {
output.push_str(&summary_output);
output.push_str("\n");
Task::ready(Ok(StringToolOutput::new(output)))
} else {
output.push_str("No errors or warnings found in the project.\n");
Task::ready(Ok(StringToolOutput::new("No errors or warnings found in the project.")))
}
output.push('\n');
let mut header_printed = false;
for (project_path, buffer_task) in buffer_tasks {
let mut path_printed = false;
let buffer = buffer_task.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
let range = entry.range.to_point(&snapshot);
if let Ok(severity) = Severity::try_from(&entry.diagnostic.severity) {
if severity_filter.is_empty() || severity_filter.contains(&severity) {
if !header_printed {
output.push_str("# Per-Path Diagnostics\n\n");
header_printed = true;
}
if !path_printed {
writeln!(output, "## {}", project_path.path.display())?;
path_printed = true;
}
writeln!(
output,
"\n### {severity} at line {}\n{}",
range.start.row + 1,
entry.diagnostic.message
)?;
}
}
}
}
if !header_printed {
output.push_str("No specific diagnostics found for the requested paths.");
}
Ok(output)
})
}
}
}
impl TryFrom<&DiagnosticSeverity> for Severity {
type Error = ();
fn try_from(
value: &DiagnosticSeverity,
) -> Result<Self, <Severity as TryFrom<&DiagnosticSeverity>>::Error> {
if *value == DiagnosticSeverity::ERROR {
Ok(Self::Error)
} else if *value == DiagnosticSeverity::WARNING {
Ok(Self::Warning)
} else if *value == DiagnosticSeverity::INFORMATION {
Ok(Self::Information)
} else if *value == DiagnosticSeverity::HINT {
Ok(Self::Hint)
} else {
Err(())
}
}
}
}

View File

@@ -5,6 +5,7 @@ use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use futures::AsyncReadExt as _;
use gpui::{App, AppContext as _, Entity, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
@@ -116,7 +117,7 @@ impl Tool for FetchTool {
"fetch".to_string()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -146,7 +147,7 @@ impl Tool for FetchTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<FetchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -164,7 +165,7 @@ impl Tool for FetchTool {
bail!("no textual content found");
}
Ok(text)
Ok(StringToolOutput::new(text))
})
}
}

View File

@@ -1,6 +1,7 @@
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -63,16 +64,6 @@ pub struct FindReplaceFileToolInput {
/// even one character in this string is different in any way from how it appears
/// in the file, then the tool call will fail.
///
/// If you get an error that the `find` string was not found, this means that either
/// you made a mistake, or that the file has changed since you last looked at it.
/// Either way, when this happens, you should retry doing this tool call until it
/// succeeds, up to 3 times. Each time you retry, you should take another look at
/// the exact text of the file in question, to make sure that you are searching for
/// exactly the right string. Regardless of whether it was because you made a mistake
/// or because the file changed since you last looked at it, you should be extra
/// careful when retrying in this way. It's a bad experience for the user if
/// this `find` string isn't found, so be super careful to get it exactly right!
///
/// <example>
/// If a file contains this code:
///
@@ -139,7 +130,7 @@ impl Tool for FindReplaceFileTool {
"find_replace_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -169,7 +160,7 @@ impl Tool for FindReplaceFileTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -261,7 +252,7 @@ impl Tool for FindReplaceFileTool {
}).await;
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
Ok(StringToolOutput::new(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str)))
})
}

View File

@@ -1,13 +1,9 @@
Find one unique part of a file in the project and replace that text with new text.
This tool is the preferred way to make edits to files *except* when making a rename. When making a rename specifically, the rename tool must always be used instead.
This tool is the preferred way to make edits to files. If you have multiple edits to make, including edits across multiple files, then make a plan to respond with a single message containing multiple calls to this tool - one call for each find/replace operation.
If you have multiple edits to make, including edits across multiple files, then make a plan to respond with a single message containing a batch of calls to this tool - one call for each find/replace operation.
You should only use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents. You also should not use this tool when you want to move or rename a file. You absolutely must NEVER use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
You should use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents. You also should not use this tool when you want to move or rename a file. You absolutely must NEVER use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
DO NOT call this tool until the code to be edited appears in the conversation! You must use another tool to read the file's contents into the conversation, or ask the user to add it to context first.
Never call this tool with identical "find" and "replace" strings. Instead, stop and think about what you actually want to do.
REMEMBER: You can use this tool after you just used the `create_file` tool. It's better to edit the file you just created than to recreate a new file from scratch.

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -44,7 +45,7 @@ impl Tool for ListDirectoryTool {
"list_directory".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -77,7 +78,7 @@ impl Tool for ListDirectoryTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -101,7 +102,7 @@ impl Tool for ListDirectoryTool {
.collect::<Vec<_>>()
.join("\n");
return Task::ready(Ok(output));
return Task::ready(Ok(StringToolOutput::new(output)));
}
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
@@ -133,8 +134,8 @@ impl Tool for ListDirectoryTool {
.unwrap();
}
if output.is_empty() {
return Task::ready(Ok(format!("{} is empty.", input.path)));
return Task::ready(Ok(StringToolOutput::new(format!("{} is empty.", input.path))));
}
Task::ready(Ok(output))
Task::ready(Ok(StringToolOutput::new(output)))
}
}

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -42,7 +43,7 @@ impl Tool for MovePathTool {
"move_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -90,7 +91,7 @@ impl Tool for MovePathTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<MovePathToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -116,10 +117,10 @@ impl Tool for MovePathTool {
cx.background_spawn(async move {
match rename_task.await {
Ok(_) => Ok(format!(
Ok(_) => Ok(StringToolOutput::new(format!(
"Moved {} to {}",
input.source_path, input.destination_path
)),
))),
Err(err) => Err(anyhow!(
"Failed to move {} to {}: {}",
input.source_path,

View File

@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use chrono::{Local, Utc};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -33,7 +34,7 @@ impl Tool for NowTool {
"now".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -60,7 +61,7 @@ impl Tool for NowTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input: NowToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -72,6 +73,6 @@ impl Tool for NowTool {
};
let text = format!("The current datetime is {now}.");
Task::ready(Ok(text))
Task::ready(Ok(StringToolOutput::new(text)))
}
}

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -23,7 +24,7 @@ impl Tool for OpenTool {
"open".to_string()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -53,7 +54,7 @@ impl Tool for OpenTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input: OpenToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -62,7 +63,7 @@ impl Tool for OpenTool {
cx.background_spawn(async move {
open::that(&input.path_or_url).context("Failed to open URL or file path")?;
Ok(format!("Successfully opened {}", input.path_or_url))
Ok(StringToolOutput::new(format!("Successfully opened {}", input.path_or_url)))
})
}
}

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -41,7 +42,7 @@ impl Tool for PathSearchTool {
"path_search".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -71,7 +72,7 @@ impl Tool for PathSearchTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
Ok(input) => (input.offset, input.glob),
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -110,7 +111,7 @@ impl Tool for PathSearchTool {
}
if matches.is_empty() {
Ok(format!("No paths in the project matched the glob {glob:?}"))
Ok(StringToolOutput::new(format!("No paths in the project matched the glob {glob:?}")))
} else {
// Sort to group entries in the same directory together.
matches.sort();
@@ -134,7 +135,7 @@ impl Tool for PathSearchTool {
matches.join("\n")
};
Ok(response)
Ok(StringToolOutput::new(response))
}
})
}

View File

@@ -1,361 +0,0 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use gpui::AppContext;
use gpui::{App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use lsp_types::CodeActionKind;
use project::LspAction;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::f32::consts::E;
use std::{fmt::Write, path::Path, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct QuickfixToolInput {
/// The path to get diagnostics for and apply quickfixes. If not provided, checks the entire project.
///
/// This path should never be absolute, and the first component
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following root directories:
///
/// - lorem
/// - ipsum
///
/// If you want to apply quickfixes for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
/// </example>
pub path: Option<String>,
}
pub struct QuickfixTool;
impl Tool for QuickfixTool {
fn name(&self) -> String {
"quickfix".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./quickfix_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Check
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
json_schema_for::<QuickfixToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
if let Some(path) = serde_json::from_value::<QuickfixToolInput>(input.clone())
.ok()
.and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(MarkdownString::inline_code(&path)),
_ => None,
})
{
format!("Apply quickfixes for {path}")
} else {
"Apply project-wide quickfixes".to_string()
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
match serde_json::from_value::<QuickfixToolInput>(input)
.ok()
.and_then(|input| input.path)
{
Some(path) if !path.is_empty() => {
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!("Could not find path {path} in project")));
};
let buffer =
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(async move |cx| {
let mut output = String::new();
let mut fixes_applied = 0;
let buffer = buffer.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let mut unfixed = Vec::new();
// Collect diagnostics to apply quickfixes for
for entry in snapshot.diagnostic_groups(None).into_iter().flat_map(
|(_, group)| group.entries.into_iter()
) {
let range = entry.range.to_point(&snapshot);
let actions = project
.update(cx, |project, cx| {
project.code_actions(
&buffer,
range.start..range.end,
None,
cx
)
})?.await?;
let quickfixes = actions.into_iter().filter(|action| {
if let LspAction::Action(code_action) = &action.lsp_action {
code_action.kind == Some(CodeActionKind::QUICKFIX)
} else {
false
}
}).collect::<Vec<_>>();
match quickfixes.first() {
Some(default_quickfix) => {
// If there's a quickfix marked as preferred, use that.
// Otherwise fall back on the first available quickfix in the list.
let preferred_action = quickfixes.iter().find(|action| {
if let LspAction::Action(code_action) = &action.lsp_action {
code_action.is_preferred.unwrap_or(false)
} else {
false
}
}).unwrap_or(default_quickfix);
project
.update(cx, |project, cx| {
project.apply_code_action(buffer.clone(), preferred_action.clone(), true, cx)
})?
.await?;
log::info!("Applied quickfix: {}", preferred_action.lsp_action.title());
fixes_applied += 1;
}
None => {
unfixed.push(entry);
}
}
}
// Save the buffer after applying fixes
if fixes_applied > 0 {
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
})?;
}
// Generate summary
if output.is_empty() {
Ok("No issues found in the file!".to_string())
} else {
writeln!(output, "\nSummary: Applied {} quickfixes. Remaining issues: {} - use the diagnostics tool to see them.",
fixes_applied, unfixed.len())?;
Ok(output)
}
})
}
_ => {
todo!("Share code with the other branch.");
let mut output = String::new();
let mut files_with_diagnostics = Vec::new();
// Collect all files with diagnostics for processing
{
let project_ref = project.read(cx);
for (project_path, _, summary) in project_ref.diagnostic_summaries(true, cx) {
if summary.error_count > 0 || summary.warning_count > 0 {
if let Some(worktree) =
project_ref.worktree_for_id(project_path.worktree_id, cx)
{
let path_str = Path::new(worktree.read(cx).root_name())
.join(project_path.path)
.display()
.to_string();
files_with_diagnostics.push(path_str);
}
}
}
}
// Create a task to process all files with diagnostics
let project = project.clone();
let action_log = action_log.clone();
let files_to_process = files_with_diagnostics.clone();
cx.spawn(async move |cx| {
let mut total_fixes_applied = 0;
let mut total_errors_unfixed = 0;
let mut total_warnings_unfixed = 0;
// Process each file with diagnostics
for file_path in files_to_process {
writeln!(output, "Processing {}...", file_path)?;
let Ok(Some(project_path)) = project.read_with(cx, |project, cx| project.find_project_path(&file_path, cx)) else {
writeln!(output, " Could not resolve project path for {}", file_path)?;
continue;
};
let buffer_open_result = project.update(cx, |project, cx|
project.open_buffer(project_path, cx));
let Ok(buffer_handle) = buffer_open_result else {
writeln!(output, " Failed to open buffer for {}", file_path)?;
continue;
};
let buffer = match buffer_handle.await {
Ok(buffer) => buffer,
Err(err) => {
writeln!(output, " Failed to load buffer for {}: {}", file_path, err)?;
continue;
}
};
let mut file_fixes_applied = 0;
let mut file_errors_unfixed = 0;
let mut file_warnings_unfixed = 0;
let mut needs_save = false;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
// Process diagnostics for this file
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
let range = entry.range.to_point(&snapshot);
let severity = entry.diagnostic.severity;
// Skip informational and hint diagnostics
if severity != DiagnosticSeverity::ERROR &&
severity != DiagnosticSeverity::WARNING {
continue;
}
// Get code actions (quickfixes) for this diagnostic
let actions = project
.update(cx, |project, cx| {
project.code_actions(
&buffer,
range.start..range.end,
None,
cx
)
})?;
let actions_result = actions.await;
match actions_result {
Ok(actions) => {
// Find quickfix actions
let quickfixes = actions.into_iter().filter(|action| {
if let LspAction::Action(code_action) = &action.lsp_action {
if let Some(kind) = &code_action.kind {
kind.as_str().starts_with("quickfix")
} else {
false
}
} else {
false
}
}).collect::<Vec<_>>();
if !quickfixes.is_empty() {
// Find the preferred quickfix (marked as isPreferred or the first one)
let preferred_action = quickfixes.iter().find(|action| {
if let LspAction::Action(code_action) = &action.lsp_action {
code_action.is_preferred.unwrap_or(false)
} else {
false
}
}).unwrap_or(&quickfixes[0]);
// Apply the quickfix
let title = preferred_action.lsp_action.title().to_string();
project
.update(cx, |project, cx| {
project.apply_code_action(buffer.clone(), preferred_action.clone(), true, cx)
})?
.await?;
writeln!(output, " Applied quickfix: {title}")?;
file_fixes_applied += 1;
needs_save = true;
} else {
// Track unfixed diagnostics
match severity {
DiagnosticSeverity::ERROR => file_errors_unfixed += 1,
DiagnosticSeverity::WARNING => file_warnings_unfixed += 1,
_ => {}
}
}
},
Err(_) => {
// Track unfixed diagnostics
match severity {
DiagnosticSeverity::ERROR => file_errors_unfixed += 1,
DiagnosticSeverity::WARNING => file_warnings_unfixed += 1,
_ => {}
}
}
}
}
// Save the buffer after applying fixes
if needs_save {
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
})?;
}
// Update totals
total_fixes_applied += file_fixes_applied;
total_errors_unfixed += file_errors_unfixed;
total_warnings_unfixed += file_warnings_unfixed;
// File summary
if file_fixes_applied > 0 || file_errors_unfixed > 0 || file_warnings_unfixed > 0 {
writeln!(output, " {} quickfixes applied. Remaining: {} errors, {} warnings\n",
file_fixes_applied, file_errors_unfixed, file_warnings_unfixed)?;
} else {
writeln!(output, " No issues fixed or found\n")?;
}
}
// Mark that we've checked diagnostics
action_log.update(cx, |action_log, _cx| {
action_log.checked_project_diagnostics();
})?;
// Generate overall summary
if files_with_diagnostics.is_empty() {
Ok("No issues found in the project!".to_string())
} else {
writeln!(output, "\nProject-wide summary: Applied {} quickfixes. Remaining issues: {} errors, {} warnings.",
total_fixes_applied, total_errors_unfixed, total_warnings_unfixed)?;
Ok(output)
}
})
}
}
}
}

View File

@@ -1,16 +0,0 @@
Get errors and warnings for the project or a specific file and apply quickfixes automatically where possible.
This tool can be invoked to find diagnostics in your code and automatically apply available quickfixes to resolve them. Quickfixes are code edits suggested by language servers that can automatically fix common issues.
When a path is provided, it checks that specific file for diagnostics and applies quickfixes.
When no path is provided, it finds diagnostics project-wide and applies quickfixes where possible.
<example>
To automatically fix issues in a specific file:
{
"path": "src/main.rs"
}
To find and fix issues across the entire project:
{}
</example>

View File

@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -51,7 +52,7 @@ impl Tool for ReadFileTool {
"read_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -88,7 +89,7 @@ impl Tool for ReadFileTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -114,9 +115,9 @@ impl Tool for ReadFileTool {
let lines = text.split('\n').skip(start - 1);
if let Some(end) = input.end_line {
let count = end.saturating_sub(start).max(1); // Ensure at least 1 line
Itertools::intersperse(lines.take(count), "\n").collect()
Itertools::intersperse(lines.take(count), "\n").collect::<String>()
} else {
Itertools::intersperse(lines, "\n").collect()
Itertools::intersperse(lines, "\n").collect::<String>()
}
})?;
@@ -124,7 +125,7 @@ impl Tool for ReadFileTool {
log.buffer_read(buffer, cx);
})?;
Ok(result)
Ok(StringToolOutput::new(result))
} else {
// No line ranges specified, so check file size to see if it's too big.
let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
@@ -137,13 +138,13 @@ impl Tool for ReadFileTool {
log.buffer_read(buffer, cx);
})?;
Ok(result)
Ok(StringToolOutput::new(result))
} else {
// File is too big, so return an error with the outline
// and a suggestion to read again with line numbers.
let outline = file_outline(project, file_path, action_log, None, 0, cx).await?;
Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start_line and end_line fields to see the implementations of symbols in the outline."))
Ok(StringToolOutput::new(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start_line and end_line fields to see the implementations of symbols in the outline.")))
}
}
})

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use futures::StreamExt;
use gpui::{App, Entity, Task};
use language::OffsetRangeExt;
@@ -44,7 +45,7 @@ impl Tool for RegexSearchTool {
"regex_search".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -83,7 +84,7 @@ impl Tool for RegexSearchTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
const CONTEXT_LINES: u32 = 2;
let (offset, regex) = match serde_json::from_value::<RegexSearchToolInput>(input) {
@@ -179,16 +180,16 @@ impl Tool for RegexSearchTool {
}
if matches_found == 0 {
Ok("No matches found".to_string())
Ok(StringToolOutput::new("No matches found".to_string()))
} else if has_more_matches {
Ok(format!(
Ok(StringToolOutput::new(format!(
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
offset + 1,
offset + matches_found,
offset + RESULTS_PER_PAGE,
))
)))
} else {
Ok(format!("Found {matches_found} matches:\n{output}"))
Ok(StringToolOutput::new(format!("Found {matches_found} matches:\n{output}")))
}
})
}

View File

@@ -1,205 +0,0 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, Entity, Task};
use language::{self, Buffer, ToPointUtf16};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct RenameToolInput {
/// The relative path to the file containing the symbol to rename.
///
/// WARNING: you MUST start this path with one of the project's root directories.
pub path: String,
/// The new name to give to the symbol.
pub new_name: String,
/// The text that comes immediately before the symbol in the file.
pub context_before_symbol: String,
/// The symbol to rename. This text must appear in the file right between
/// `context_before_symbol` and `context_after_symbol`.
///
/// The file must contain exactly one occurrence of `context_before_symbol` followed by
/// `symbol` followed by `context_after_symbol`. If the file contains zero occurrences,
/// or if it contains more than one occurrence, the tool will fail, so it is absolutely
/// critical that you verify ahead of time that the string is unique. You can search
/// the file's contents to verify this ahead of time.
///
/// To make the string more likely to be unique, include a minimum of 1 line of context
/// before the symbol, as well as a minimum of 1 line of context after the symbol.
/// If these lines of context are not enough to obtain a string that appears only once
/// in the file, then double the number of context lines until the string becomes unique.
/// (Start with 1 line before and 1 line after though, because too much context is
/// needlessly costly.)
///
/// Do not alter the context lines of code in any way, and make sure to preserve all
/// whitespace and indentation for all lines of code. The combined string must be exactly
/// as it appears in the file, or else this tool call will fail.
pub symbol: String,
/// The text that comes immediately after the symbol in the file.
pub context_after_symbol: String,
}
pub struct RenameTool;
impl Tool for RenameTool {
fn name(&self) -> String {
"rename".into()
}
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./rename_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Pencil
}
fn input_schema(
&self,
_format: language_model::LanguageModelToolSchemaFormat,
) -> serde_json::Value {
let schema = schemars::schema_for!(RenameToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<RenameToolInput>(input.clone()) {
Ok(input) => {
format!("Rename '{}' to '{}'", input.symbol, input.new_name)
}
Err(_) => "Rename symbol".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<RenameToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
cx.spawn(async move |cx| {
let buffer = {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
.context("Path not found in project")
})??;
project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
};
action_log.update(cx, |action_log, cx| {
action_log.buffer_read(buffer.clone(), cx);
})?;
let position = {
let Some(position) = buffer.read_with(cx, |buffer, _cx| {
find_symbol_position(&buffer, &input.context_before_symbol, &input.symbol, &input.context_after_symbol)
})? else {
return Err(anyhow!(
"Failed to locate the symbol specified by context_before_symbol, symbol, and context_after_symbol. Make sure context_before_symbol and context_after_symbol each match exactly once in the file."
));
};
buffer.read_with(cx, |buffer, _| {
position.to_point_utf16(&buffer.snapshot())
})?
};
project
.update(cx, |project, cx| {
project.perform_rename(buffer.clone(), position, input.new_name.clone(), cx)
})?
.await?;
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
})?;
Ok(format!("Renamed '{}' to '{}'", input.symbol, input.new_name))
})
}
}
/// Finds the position of the symbol in the buffer, if it appears between context_before_symbol
/// and context_after_symbol, and if that combined string has one unique result in the buffer.
///
/// If an exact match fails, it tries adding a newline to the end of context_before_symbol and
/// to the beginning of context_after_symbol to accommodate line-based context matching.
fn find_symbol_position(
buffer: &Buffer,
context_before_symbol: &str,
symbol: &str,
context_after_symbol: &str,
) -> Option<language::Anchor> {
let snapshot = buffer.snapshot();
let text = snapshot.text();
// First try with exact match
let search_string = format!("{context_before_symbol}{symbol}{context_after_symbol}");
let mut positions = text.match_indices(&search_string);
let position_result = positions.next();
if let Some(position) = position_result {
// Check if the matched string is unique
if positions.next().is_none() {
let symbol_start = position.0 + context_before_symbol.len();
let symbol_start_anchor =
snapshot.anchor_before(snapshot.offset_to_point(symbol_start));
return Some(symbol_start_anchor);
}
}
// If exact match fails or is not unique, try with line-based context
// Add a newline to the end of before context and beginning of after context
let line_based_before = if context_before_symbol.ends_with('\n') {
context_before_symbol.to_string()
} else {
format!("{context_before_symbol}\n")
};
let line_based_after = if context_after_symbol.starts_with('\n') {
context_after_symbol.to_string()
} else {
format!("\n{context_after_symbol}")
};
let line_search_string = format!("{line_based_before}{symbol}{line_based_after}");
let mut line_positions = text.match_indices(&line_search_string);
let line_position = line_positions.next()?;
// The line-based search string must also appear exactly once
if line_positions.next().is_some() {
return None;
}
let line_symbol_start = line_position.0 + line_based_before.len();
let line_symbol_start_anchor =
snapshot.anchor_before(snapshot.offset_to_point(line_symbol_start));
Some(line_symbol_start_anchor)
}

View File

@@ -1,15 +0,0 @@
Renames a symbol across your codebase using the language server's semantic knowledge.
This tool performs a rename refactoring operation on a specified symbol. It uses the project's language server to analyze the code and perform the rename correctly across all files where the symbol is referenced.
Unlike a simple find and replace, this tool understands the semantic meaning of the code, so it only renames the specific symbol you specify and not unrelated text that happens to have the same name.
Examples of symbols you can rename:
- Variables
- Functions
- Classes/structs
- Fields/properties
- Methods
- Interfaces/traits
The language server handles updating all references to the renamed symbol throughout the codebase.

View File

@@ -1,5 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AsyncApp, Entity, Task};
use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -72,7 +73,7 @@ impl Tool for SymbolInfoTool {
"symbol_info".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -122,7 +123,7 @@ impl Tool for SymbolInfoTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -203,7 +204,7 @@ impl Tool for SymbolInfoTool {
if output.is_empty() {
Err(anyhow!("None found."))
} else {
Ok(output)
Ok(StringToolOutput::new(output))
}
})
}

View File

@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -24,7 +25,7 @@ impl Tool for ThinkingTool {
"thinking".to_string()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -33,7 +34,7 @@ impl Tool for ThinkingTool {
}
fn icon(&self) -> IconName {
IconName::LightBulb
IconName::Brain
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
@@ -51,10 +52,10 @@ impl Tool for ThinkingTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
// This tool just "thinks out loud" and doesn't perform any actions.
Task::ready(match serde_json::from_value::<ThinkingToolInput>(input) {
Ok(_input) => Ok("Finished thinking.".to_string()),
Ok(_input) => Ok(StringToolOutput::new("Finished thinking.")),
Err(err) => Err(anyhow!(err)),
})
}

View File

@@ -318,7 +318,6 @@ impl Server {
.add_request_handler(forward_read_only_project_request::<proto::OpenUncommittedDiff>)
.add_request_handler(forward_read_only_project_request::<proto::LspExtExpandMacro>)
.add_request_handler(forward_read_only_project_request::<proto::LspExtOpenDocs>)
.add_request_handler(forward_mutating_project_request::<proto::LspExtRunnables>)
.add_request_handler(
forward_read_only_project_request::<proto::LspExtSwitchSourceHeader>,
)

View File

@@ -309,7 +309,7 @@ impl MessageEditor {
.map(|mat| {
let (new_text, label) = completion_fn(&mat);
Completion {
replace_range: range.clone(),
old_range: range.clone(),
new_text,
label,
icon_path: None,

View File

@@ -3,62 +3,37 @@ use std::ops::{Deref, DerefMut};
use std::sync::LazyLock;
use collections::HashMap;
use gpui::{
AnyElement, App, IntoElement, RenderOnce, SharedString, Window, div, pattern_slash, prelude::*,
px, rems,
};
use gpui::{AnyElement, App, IntoElement, RenderOnce, SharedString, Window, div, prelude::*, px};
use linkme::distributed_slice;
use parking_lot::RwLock;
use theme::ActiveTheme;
pub trait Component {
fn scope() -> ComponentScope {
ComponentScope::None
}
fn scope() -> Option<ComponentScope>;
fn name() -> &'static str {
std::any::type_name::<Self>()
}
/// Returns a name that the component should be sorted by.
///
/// Implement this if the component should be sorted in an alternate order than its name.
///
/// Example:
///
/// For example, to group related components together when sorted:
///
/// - Button -> ButtonA
/// - IconButton -> ButtonBIcon
/// - ToggleButton -> ButtonCToggle
///
/// This naming scheme keeps these components together and allows them to /// be sorted in a logical order.
fn sort_name() -> &'static str {
Self::name()
}
fn description() -> Option<&'static str> {
None
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
None
}
}
pub trait ComponentPreview: Component {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement;
}
#[distributed_slice]
pub static __ALL_COMPONENTS: [fn()] = [..];
#[distributed_slice]
pub static __ALL_PREVIEWS: [fn()] = [..];
pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
pub struct ComponentRegistry {
components: Vec<(
ComponentScope,
// name
&'static str,
// sort name
&'static str,
// description
Option<&'static str>,
)>,
previews: HashMap<&'static str, fn(&mut Window, &mut App) -> Option<AnyElement>>,
components: Vec<(Option<ComponentScope>, &'static str, Option<&'static str>)>,
previews: HashMap<&'static str, fn(&mut Window, &mut App) -> AnyElement>,
}
impl ComponentRegistry {
@@ -72,16 +47,30 @@ impl ComponentRegistry {
pub fn init() {
let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
let preview_fns: Vec<_> = __ALL_PREVIEWS.iter().cloned().collect();
for f in component_fns {
f();
}
for f in preview_fns {
f();
}
}
pub fn register_component<T: Component>() {
let component_data = (T::scope(), T::name(), T::sort_name(), T::description());
let mut data = COMPONENT_DATA.write();
data.components.push(component_data);
data.previews.insert(T::name(), T::preview);
let component_data = (T::scope(), T::name(), T::description());
COMPONENT_DATA.write().components.push(component_data);
}
pub fn register_preview<T: ComponentPreview>() {
let preview_data = (
T::name(),
T::preview as fn(&mut Window, &mut App) -> AnyElement,
);
COMPONENT_DATA
.write()
.previews
.insert(preview_data.0, preview_data.1);
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -91,41 +80,29 @@ pub struct ComponentId(pub &'static str);
pub struct ComponentMetadata {
id: ComponentId,
name: SharedString,
sort_name: SharedString,
scope: ComponentScope,
scope: Option<ComponentScope>,
description: Option<SharedString>,
preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
preview: Option<fn(&mut Window, &mut App) -> AnyElement>,
}
impl ComponentMetadata {
pub fn id(&self) -> ComponentId {
self.id.clone()
}
pub fn name(&self) -> SharedString {
self.name.clone()
}
pub fn sort_name(&self) -> SharedString {
self.sort_name.clone()
}
pub fn scopeless_name(&self) -> SharedString {
self.name
.clone()
.split("::")
.last()
.unwrap_or(&self.name)
.to_string()
.into()
}
pub fn scope(&self) -> ComponentScope {
pub fn scope(&self) -> Option<ComponentScope> {
self.scope.clone()
}
pub fn description(&self) -> Option<SharedString> {
self.description.clone()
}
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> AnyElement> {
self.preview
}
}
@@ -136,18 +113,26 @@ impl AllComponents {
pub fn new() -> Self {
AllComponents(HashMap::default())
}
/// Returns all components with previews
pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
self.0.values().filter(|c| c.preview.is_some()).collect()
}
/// Returns all components with previews sorted by name
pub fn all_previews_sorted(&self) -> Vec<ComponentMetadata> {
let mut previews: Vec<ComponentMetadata> =
self.all_previews().into_iter().cloned().collect();
previews.sort_by_key(|a| a.name());
previews
}
/// Returns all components
pub fn all(&self) -> Vec<&ComponentMetadata> {
self.0.values().collect()
}
/// Returns all components sorted by name
pub fn all_sorted(&self) -> Vec<ComponentMetadata> {
let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
components.sort_by_key(|a| a.name());
@@ -157,6 +142,7 @@ impl AllComponents {
impl Deref for AllComponents {
type Target = HashMap<ComponentId, ComponentMetadata>;
fn deref(&self) -> &Self::Target {
&self.0
}
@@ -171,127 +157,139 @@ impl DerefMut for AllComponents {
pub fn components() -> AllComponents {
let data = COMPONENT_DATA.read();
let mut all_components = AllComponents::new();
for (scope, name, sort_name, description) in &data.components {
for (scope, name, description) in &data.components {
let preview = data.previews.get(name).cloned();
let component_name = SharedString::new_static(name);
let sort_name = SharedString::new_static(sort_name);
let id = ComponentId(name);
all_components.insert(
id.clone(),
ComponentMetadata {
id,
name: component_name,
sort_name,
scope: scope.clone(),
description: description.map(Into::into),
preview,
},
);
}
all_components
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ComponentScope {
Collaboration,
DataDisplay,
Editor,
Images,
Input,
Layout,
Loading,
Navigation,
None,
Input,
Notification,
Overlays,
Status,
Typography,
Editor,
Collaboration,
VersionControl,
Unknown(SharedString),
}
impl Display for ComponentScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ComponentScope::Collaboration => write!(f, "Collaboration"),
ComponentScope::DataDisplay => write!(f, "Data Display"),
ComponentScope::Editor => write!(f, "Editor"),
ComponentScope::Images => write!(f, "Images & Icons"),
ComponentScope::Input => write!(f, "Forms & Input"),
ComponentScope::Layout => write!(f, "Layout & Structure"),
ComponentScope::Loading => write!(f, "Loading & Progress"),
ComponentScope::Navigation => write!(f, "Navigation"),
ComponentScope::None => write!(f, "Unsorted"),
ComponentScope::Layout => write!(f, "Layout"),
ComponentScope::Input => write!(f, "Input"),
ComponentScope::Notification => write!(f, "Notification"),
ComponentScope::Overlays => write!(f, "Overlays & Layering"),
ComponentScope::Status => write!(f, "Status"),
ComponentScope::Typography => write!(f, "Typography"),
ComponentScope::Editor => write!(f, "Editor"),
ComponentScope::Collaboration => write!(f, "Collaboration"),
ComponentScope::VersionControl => write!(f, "Version Control"),
ComponentScope::Unknown(name) => write!(f, "Unknown: {}", name),
}
}
}
impl From<&str> for ComponentScope {
fn from(value: &str) -> Self {
match value {
"Layout" => ComponentScope::Layout,
"Input" => ComponentScope::Input,
"Notification" => ComponentScope::Notification,
"Editor" => ComponentScope::Editor,
"Collaboration" => ComponentScope::Collaboration,
"Version Control" | "VersionControl" => ComponentScope::VersionControl,
_ => ComponentScope::Unknown(SharedString::new(value)),
}
}
}
impl From<String> for ComponentScope {
fn from(value: String) -> Self {
match value.as_str() {
"Layout" => ComponentScope::Layout,
"Input" => ComponentScope::Input,
"Notification" => ComponentScope::Notification,
"Editor" => ComponentScope::Editor,
"Collaboration" => ComponentScope::Collaboration,
"Version Control" | "VersionControl" => ComponentScope::VersionControl,
_ => ComponentScope::Unknown(SharedString::new(value)),
}
}
}
/// Which side of the preview to show labels on
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExampleLabelSide {
/// Left side
Left,
/// Right side
Right,
/// Top side
#[default]
Top,
/// Bottom side
Bottom,
}
/// A single example of a component.
#[derive(IntoElement)]
pub struct ComponentExample {
pub variant_name: SharedString,
pub description: Option<SharedString>,
pub element: AnyElement,
variant_name: SharedString,
element: AnyElement,
label_side: ExampleLabelSide,
grow: bool,
}
impl RenderOnce for ComponentExample {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.w_full()
.flex()
.flex_col()
.gap_3()
.child(
div()
.child(self.variant_name.clone())
.text_size(rems(1.25))
.text_color(cx.theme().colors().text),
)
.when_some(self.description, |this, description| {
this.child(
div()
.text_size(rems(0.9375))
.text_color(cx.theme().colors().text_muted)
.child(description.clone()),
)
})
.child(
div()
.flex()
.w_full()
.rounded_xl()
.min_h(px(100.))
.justify_center()
.p_8()
.border_1()
.border_color(cx.theme().colors().border)
.bg(pattern_slash(
cx.theme().colors().surface_background.opacity(0.5),
24.0,
24.0,
))
.shadow_sm()
.child(self.element),
)
let base = div().flex();
let base = match self.label_side {
ExampleLabelSide::Right => base.flex_row(),
ExampleLabelSide::Left => base.flex_row_reverse(),
ExampleLabelSide::Bottom => base.flex_col(),
ExampleLabelSide::Top => base.flex_col_reverse(),
};
base.gap_2()
.p_2()
.text_size(px(10.))
.text_color(cx.theme().colors().text_muted)
.when(self.grow, |this| this.flex_1())
.when(!self.grow, |this| this.flex_none())
.child(self.element)
.child(self.variant_name)
.into_any_element()
}
}
impl ComponentExample {
/// Create a new example with the given variant name and example value.
pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
Self {
variant_name: variant_name.into(),
element,
description: None,
label_side: ExampleLabelSide::default(),
grow: false,
}
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
/// Set the example to grow to fill the available horizontal space.
pub fn grow(mut self) -> Self {
self.grow = true;
self
}
}
@@ -311,7 +309,7 @@ impl RenderOnce for ComponentExampleGroup {
.flex_col()
.text_sm()
.text_color(cx.theme().colors().text_muted)
.w_full()
.when(self.grow, |this| this.w_full().flex_1())
.when_some(self.title, |this, title| {
this.gap_4().child(
div()
@@ -338,7 +336,7 @@ impl RenderOnce for ComponentExampleGroup {
.child(
div()
.flex()
.flex_col()
.when(self.vertical, |this| this.flex_col())
.items_start()
.w_full()
.gap_6()
@@ -350,6 +348,7 @@ impl RenderOnce for ComponentExampleGroup {
}
impl ComponentExampleGroup {
/// Create a new group of examples with the given title.
pub fn new(examples: Vec<ComponentExample>) -> Self {
Self {
title: None,
@@ -358,6 +357,8 @@ impl ComponentExampleGroup {
vertical: false,
}
}
/// Create a new group of examples with the given title.
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
Self {
title: Some(title.into()),
@@ -366,16 +367,21 @@ impl ComponentExampleGroup {
vertical: false,
}
}
/// Set the group to grow to fill the available horizontal space.
pub fn grow(mut self) -> Self {
self.grow = true;
self
}
/// Lay the group out vertically.
pub fn vertical(mut self) -> Self {
self.vertical = true;
self
}
}
/// Create a single example
pub fn single_example(
variant_name: impl Into<SharedString>,
example: AnyElement,
@@ -383,10 +389,12 @@ pub fn single_example(
ComponentExample::new(variant_name, example)
}
/// Create a group of examples without a title
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
ComponentExampleGroup::new(examples)
}
/// Create a group of examples with a title
pub fn example_group_with_title(
title: impl Into<SharedString>,
examples: Vec<ComponentExample>,

View File

@@ -43,7 +43,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
language_registry,
user_store,
None,
None,
cx,
)
});
@@ -107,12 +106,10 @@ impl ComponentPreview {
language_registry: Arc<LanguageRegistry>,
user_store: Entity<UserStore>,
selected_index: impl Into<Option<usize>>,
active_page: Option<PreviewPage>,
cx: &mut Context<Self>,
) -> Self {
let sorted_components = components().all_sorted();
let selected_index = selected_index.into().unwrap_or(0);
let active_page = active_page.unwrap_or(PreviewPage::AllComponents);
let component_list = ListState::new(
sorted_components.len(),
@@ -138,7 +135,7 @@ impl ComponentPreview {
language_registry,
user_store,
workspace,
active_page,
active_page: PreviewPage::AllComponents,
component_map: components().0,
components: sorted_components,
component_list,
@@ -172,7 +169,8 @@ impl ComponentPreview {
fn scope_ordered_entries(&self) -> Vec<PreviewEntry> {
use std::collections::HashMap;
let mut scope_groups: HashMap<ComponentScope, Vec<ComponentMetadata>> = HashMap::default();
let mut scope_groups: HashMap<Option<ComponentScope>, Vec<ComponentMetadata>> =
HashMap::default();
for component in &self.components {
scope_groups
@@ -194,7 +192,6 @@ impl ComponentPreview {
ComponentScope::Notification,
ComponentScope::Collaboration,
ComponentScope::VersionControl,
ComponentScope::None,
];
// Always show all components first
@@ -202,27 +199,38 @@ impl ComponentPreview {
entries.push(PreviewEntry::Separator);
for scope in known_scopes.iter() {
if let Some(components) = scope_groups.remove(scope) {
let scope_key = Some(scope.clone());
if let Some(components) = scope_groups.remove(&scope_key) {
if !components.is_empty() {
entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
let mut sorted_components = components;
sorted_components.sort_by_key(|component| component.sort_name());
for component in sorted_components {
for component in components {
entries.push(PreviewEntry::Component(component));
}
}
}
}
if let Some(components) = scope_groups.get(&ComponentScope::None) {
for (scope, components) in &scope_groups {
if let Some(ComponentScope::Unknown(_)) = scope {
if !components.is_empty() {
if let Some(scope_value) = scope {
entries.push(PreviewEntry::SectionHeader(scope_value.to_string().into()));
}
for component in components {
entries.push(PreviewEntry::Component(component.clone()));
}
}
}
}
if let Some(components) = scope_groups.get(&None) {
if !components.is_empty() {
entries.push(PreviewEntry::Separator);
entries.push(PreviewEntry::SectionHeader("Uncategorized".into()));
let mut sorted_components = components.clone();
sorted_components.sort_by_key(|c| c.sort_name());
for component in sorted_components {
for component in components {
entries.push(PreviewEntry::Component(component.clone()));
}
}
@@ -242,10 +250,7 @@ impl ComponentPreview {
let id = component_metadata.id();
let selected = self.active_page == PreviewPage::Component(id.clone());
ListItem::new(ix)
.child(
Label::new(component_metadata.scopeless_name().clone())
.color(Color::Default),
)
.child(Label::new(component_metadata.name().clone()).color(Color::Default))
.selectable(true)
.toggle_state(selected)
.inset(true)
@@ -328,7 +333,7 @@ impl ComponentPreview {
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let name = component.scopeless_name();
let name = component.name();
let scope = component.scope();
let description = component.description();
@@ -349,12 +354,13 @@ impl ComponentPreview {
v_flex()
.gap_1()
.child(
h_flex().gap_1().text_xl().child(div().child(name)).when(
!matches!(scope, ComponentScope::None),
|this| {
h_flex()
.gap_1()
.text_xl()
.child(div().child(name))
.when_some(scope, |this, scope| {
this.child(div().opacity(0.5).child(format!("({})", scope)))
},
),
}),
)
.when_some(description, |this, description| {
this.child(
@@ -367,7 +373,7 @@ impl ComponentPreview {
}),
)
.when_some(component.preview(), |this, preview| {
this.children(preview(window, cx))
this.child(preview(window, cx))
}),
)
.into_any_element()
@@ -389,16 +395,17 @@ impl ComponentPreview {
fn render_component_page(
&mut self,
component_id: &ComponentId,
_window: &mut Window,
_cx: &mut Context<Self>,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let component = self.component_map.get(&component_id);
if let Some(component) = component {
v_flex()
.id("render-component-page")
.size_full()
.child(ComponentPreviewPage::new(component.clone()))
.w_full()
.flex_initial()
.min_h_full()
.child(self.render_preview(component, window, cx))
.into_any_element()
} else {
v_flex()
@@ -438,11 +445,10 @@ impl Render for ComponentPreview {
.overflow_hidden()
.size_full()
.track_focus(&self.focus_handle)
.px_2()
.bg(cx.theme().colors().editor_background)
.child(
v_flex()
.border_r_1()
.border_color(cx.theme().colors().border)
.h_full()
.child(
uniform_list(
@@ -459,7 +465,6 @@ impl Render for ComponentPreview {
)
.track_scroll(self.nav_scroll_handle.clone())
.pt_4()
.px_4()
.w(px(240.))
.h_full()
.flex_1(),
@@ -522,7 +527,6 @@ impl Item for ComponentPreview {
let user_store = self.user_store.clone();
let weak_workspace = self.workspace.clone();
let selected_index = self.cursor_index;
let active_page = self.active_page.clone();
Some(cx.new(|cx| {
Self::new(
@@ -530,7 +534,6 @@ impl Item for ComponentPreview {
language_registry,
user_store,
selected_index,
Some(active_page),
cx,
)
}))
@@ -563,14 +566,7 @@ impl SerializableItem for ComponentPreview {
let weak_workspace = workspace.clone();
cx.update(|_, cx| {
Ok(cx.new(|cx| {
ComponentPreview::new(
weak_workspace,
language_registry,
user_store,
None,
None,
cx,
)
ComponentPreview::new(weak_workspace, language_registry, user_store, None, cx)
}))
})?
})
@@ -604,76 +600,3 @@ impl SerializableItem for ComponentPreview {
false
}
}
#[derive(IntoElement)]
pub struct ComponentPreviewPage {
// languages: Arc<LanguageRegistry>,
component: ComponentMetadata,
}
impl ComponentPreviewPage {
pub fn new(
component: ComponentMetadata,
// languages: Arc<LanguageRegistry>
) -> Self {
Self {
// languages,
component,
}
}
fn render_header(&self, _: &Window, cx: &App) -> impl IntoElement {
v_flex()
.px_12()
.pt_16()
.pb_12()
.gap_6()
.bg(cx.theme().colors().surface_background)
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.gap_0p5()
.child(
Label::new(self.component.scope().to_string())
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Headline::new(self.component.scopeless_name()).size(HeadlineSize::XLarge),
),
)
.when_some(self.component.description(), |this, description| {
this.child(div().text_sm().child(description))
})
}
fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.flex_1()
.px_12()
.py_6()
.bg(cx.theme().colors().editor_background)
.child(if let Some(preview) = self.component.preview() {
preview(window, cx).unwrap_or_else(|| {
div()
.child("Failed to load preview. This path should be unreachable")
.into_any_element()
})
} else {
div().child("No preview available").into_any_element()
})
}
}
impl RenderOnce for ComponentPreviewPage {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.id("component-preview-page")
.overflow_y_scroll()
.overflow_x_hidden()
.w_full()
.child(self.render_header(window, cx))
.child(self.render_preview(window, cx))
}
}

View File

@@ -2,6 +2,7 @@ use std::sync::Arc;
use anyhow::{Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolSource};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use icons::IconName;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -49,7 +50,7 @@ impl Tool for ContextServerTool {
}
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -76,7 +77,7 @@ impl Tool for ContextServerTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
let tool_name = self.tool.name.clone();
let server_clone = server.clone();
@@ -114,7 +115,7 @@ impl Tool for ContextServerTool {
}
}
}
Ok(result)
Ok(StringToolOutput::new(result))
})
} else {
Task::ready(Err(anyhow!("Context server not found")))

View File

@@ -93,7 +93,7 @@ pub struct TcpArguments {
pub port: u16,
pub timeout: Option<u64>,
}
#[derive(Default, Debug, Clone)]
#[derive(Debug, Clone)]
pub struct DebugAdapterBinary {
pub command: String,
pub arguments: Option<Vec<OsString>>,
@@ -102,7 +102,6 @@ pub struct DebugAdapterBinary {
pub connection: Option<TcpArguments>,
}
#[derive(Debug)]
pub struct AdapterVersion {
pub tag_name: String,
pub url: String,

View File

@@ -1,141 +0,0 @@
use std::{path::PathBuf, sync::OnceLock};
use anyhow::{Result, bail};
use async_trait::async_trait;
use dap::adapters::latest_github_release;
use gpui::AsyncApp;
use task::{DebugAdapterConfig, DebugRequestType, DebugTaskDefinition};
use crate::*;
#[derive(Default)]
pub(crate) struct CodeLldbDebugAdapter {
last_known_version: OnceLock<String>,
}
impl CodeLldbDebugAdapter {
const ADAPTER_NAME: &'static str = "CodeLLDB";
}
#[async_trait(?Send)]
impl DebugAdapter for CodeLldbDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn install_binary(
&self,
version: AdapterVersion,
delegate: &dyn DapDelegate,
) -> Result<()> {
adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::Vsix,
delegate,
)
.await?;
Ok(())
}
async fn fetch_latest_adapter_version(
&self,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let release =
latest_github_release("vadimcn/codelldb", true, false, delegate.http_client()).await?;
let arch = match std::env::consts::ARCH {
"aarch64" => "arm64",
"x86_64" => "x64",
_ => {
return Err(anyhow!(
"unsupported architecture {}",
std::env::consts::ARCH
));
}
};
let platform = match std::env::consts::OS {
"macos" => "darwin",
"linux" => "linux",
"windows" => "win32",
_ => {
return Err(anyhow!(
"unsupported operating system {}",
std::env::consts::OS
));
}
};
let asset_name = format!("codelldb-{platform}-{arch}.vsix");
let _ = self.last_known_version.set(release.tag_name.clone());
let ret = AdapterVersion {
tag_name: release.tag_name,
url: release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?
.browser_download_url
.clone(),
};
Ok(ret)
}
async fn get_installed_binary(
&self,
_: &dyn DapDelegate,
_: &DebugAdapterConfig,
_: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let Some(version) = self.last_known_version.get() else {
bail!("Could not determine latest CodeLLDB version");
};
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
let version_path = adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version));
let adapter_dir = version_path.join("extension").join("adapter");
let command = adapter_dir.join("codelldb");
let command = command
.to_str()
.map(ToOwned::to_owned)
.ok_or_else(|| anyhow!("Adapter path is expected to be valid UTF-8"))?;
Ok(DebugAdapterBinary {
command,
cwd: Some(adapter_dir),
..Default::default()
})
}
fn request_args(&self, config: &DebugTaskDefinition) -> Value {
let mut args = json!({
"request": match config.request {
DebugRequestType::Launch(_) => "launch",
DebugRequestType::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequestType::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequestType::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
args
}
}

View File

@@ -1,4 +1,3 @@
mod codelldb;
mod gdb;
mod go;
mod javascript;
@@ -10,7 +9,6 @@ use std::{net::Ipv4Addr, sync::Arc};
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use codelldb::CodeLldbDebugAdapter;
use dap::{
DapRegistry,
adapters::{
@@ -28,7 +26,6 @@ use serde_json::{Value, json};
use task::{DebugAdapterConfig, TCPHost};
pub fn init(registry: Arc<DapRegistry>) {
registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default()));
registry.add_adapter(Arc::from(PythonDebugAdapter));
registry.add_adapter(Arc::from(PhpDebugAdapter));
registry.add_adapter(Arc::from(JsDebugAdapter::default()));

View File

@@ -25,9 +25,7 @@ use project::{
};
use rpc::proto::{self};
use settings::Settings;
use std::any::TypeId;
use std::path::Path;
use std::sync::Arc;
use std::{any::TypeId, path::PathBuf};
use task::DebugTaskDefinition;
use terminal_view::terminal_panel::TerminalPanel;
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
@@ -274,7 +272,7 @@ impl DebugPanel {
fn handle_run_in_terminal_request(
&self,
title: Option<String>,
cwd: Option<Arc<Path>>,
cwd: PathBuf,
command: Option<String>,
args: Vec<String>,
envs: HashMap<String, String>,

View File

@@ -356,7 +356,7 @@ impl ConsoleQueryBarCompletionProvider {
let variable_value = variables.get(&string_match.string)?;
Some(project::Completion {
replace_range: buffer_position..buffer_position,
old_range: buffer_position..buffer_position,
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),
@@ -428,10 +428,10 @@ impl ConsoleQueryBarCompletionProvider {
let buffer_offset = buffer_position.to_offset(&snapshot);
let start = buffer_offset - word_bytes_length;
let start = snapshot.anchor_before(start);
let replace_range = start..buffer_position;
let old_range = start..buffer_position;
project::Completion {
replace_range,
old_range,
new_text,
label: CodeLabel {
filter_range: 0..completion.label.len(),

View File

@@ -3,7 +3,6 @@ use super::*;
use gpui::{action_as, action_with_deprecated_aliases, actions};
use schemars::JsonSchema;
use util::serde::default_true;
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SelectNext {
@@ -263,8 +262,6 @@ actions!(
Cancel,
CancelLanguageServerWork,
ConfirmRename,
ConfirmCompletionInsert,
ConfirmCompletionReplace,
ContextMenuFirst,
ContextMenuLast,
ContextMenuNext,
@@ -420,7 +417,6 @@ actions!(
Tab,
Backtab,
ToggleBreakpoint,
ToggleCase,
DisableBreakpoint,
EnableBreakpoint,
EditLogBreakpoint,

View File

@@ -230,7 +230,7 @@ impl CompletionsMenu {
let completions = choices
.iter()
.map(|choice| Completion {
replace_range: selection.start.text_anchor..selection.end.text_anchor,
old_range: selection.start.text_anchor..selection.end.text_anchor,
new_text: choice.to_string(),
label: CodeLabel {
text: choice.to_string(),

View File

@@ -109,8 +109,8 @@ use language::{
IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
self, InlayHintSettings, RewrapBehavior, WordsCompletionMode, all_language_settings,
language_settings,
},
point_from_lsp, text_diff_with_options,
};
@@ -131,7 +131,7 @@ pub use proposed_changes_editor::{
};
use smallvec::smallvec;
use std::{cell::OnceCell, iter::Peekable};
use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
use task::{ResolvedTask, TaskTemplate, TaskVariables};
pub use lsp::CompletionContext;
use lsp::{
@@ -140,7 +140,6 @@ use lsp::{
};
use language::BufferSnapshot;
pub use lsp_ext::lsp_tasks;
use movement::TextLayoutDetails;
pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, RowInfo,
@@ -1262,7 +1261,6 @@ impl Editor {
clone.selections.clone_state(&self.selections);
clone.scroll_manager.clone_state(&self.scroll_manager);
clone.searchable = self.searchable;
clone.read_only = self.read_only;
clone
}
@@ -4272,7 +4270,6 @@ impl Editor {
buffer
.update(cx, |buffer, _| {
buffer.push_transaction(transaction, Instant::now());
buffer.finalize_last_transaction();
})
.ok();
}
@@ -4464,7 +4461,7 @@ impl Editor {
words.remove(&lsp_completion.new_text);
}
completions.extend(words.into_iter().map(|(word, word_range)| Completion {
replace_range: old_range.clone(),
old_range: old_range.clone(),
new_text: word.clone(),
label: CodeLabel::plain(word, None),
icon_path: None,
@@ -4571,26 +4568,6 @@ impl Editor {
self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx)
}
pub fn confirm_completion_insert(
&mut self,
_: &ConfirmCompletionInsert,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx)
}
pub fn confirm_completion_replace(
&mut self,
_: &ConfirmCompletionReplace,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx)
}
pub fn compose_completion(
&mut self,
action: &ComposeCompletion,
@@ -4610,10 +4587,12 @@ impl Editor {
) -> Option<Task<Result<()>>> {
use language::ToOffset as _;
let CodeContextMenu::Completions(completions_menu) = self.hide_context_menu(window, cx)?
else {
return None;
};
let completions_menu =
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(window, cx)? {
menu
} else {
return None;
};
let candidate_id = {
let entries = completions_menu.entries.borrow();
@@ -4642,12 +4621,9 @@ impl Editor {
new_text = completion.new_text.clone();
};
let selections = self.selections.all::<usize>(cx);
let replace_range = choose_completion_range(&completion, intent, &buffer_handle, cx);
let buffer = buffer_handle.read(cx);
let old_text = buffer
.text_for_range(replace_range.clone())
.collect::<String>();
let old_range = completion.old_range.to_offset(buffer);
let old_text = buffer.text_for_range(old_range.clone()).collect::<String>();
let newest_selection = self.selections.newest_anchor();
if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
@@ -4658,8 +4634,8 @@ impl Editor {
.start
.text_anchor
.to_offset(buffer)
.saturating_sub(replace_range.start);
let lookahead = replace_range
.saturating_sub(old_range.start);
let lookahead = old_range
.end
.saturating_sub(newest_selection.end.text_anchor.to_offset(buffer));
let mut common_prefix_len = 0;
@@ -4688,8 +4664,8 @@ impl Editor {
ranges.clear();
ranges.extend(selections.iter().map(|s| {
if s.id == newest_selection.id {
range_to_replace = Some(replace_range.clone());
replace_range.clone()
range_to_replace = Some(old_range.clone());
old_range.clone()
} else {
s.start..s.end
}
@@ -9143,17 +9119,6 @@ impl Editor {
});
}
pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context<Self>) {
self.manipulate_text(window, cx, |text| {
let has_upper_case_characters = text.chars().any(|c| c.is_uppercase());
if has_upper_case_characters {
text.to_lowercase()
} else {
text.to_uppercase()
}
})
}
pub fn convert_to_upper_case(
&mut self,
_: &ConvertToUpperCase,
@@ -12484,13 +12449,12 @@ impl Editor {
return Task::ready(());
}
let project = self.project.as_ref().map(Entity::downgrade);
let task_sources = self.lsp_task_sources(cx);
cx.spawn_in(window, async move |editor, cx| {
cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
let Some(project) = project.and_then(|p| p.upgrade()) else {
return;
};
let Ok(display_snapshot) = editor.update(cx, |this, cx| {
let Ok(display_snapshot) = this.update(cx, |this, cx| {
this.display_map.update(cx, |map, cx| map.snapshot(cx))
}) else {
return;
@@ -12513,77 +12477,15 @@ impl Editor {
}
})
.await;
let Ok(lsp_tasks) =
cx.update(|_, cx| crate::lsp_tasks(project.clone(), &task_sources, None, cx))
else {
return;
};
let lsp_tasks = lsp_tasks.await;
let Ok(mut lsp_tasks_by_rows) = cx.update(|_, cx| {
lsp_tasks
.into_iter()
.flat_map(|(kind, tasks)| {
tasks.into_iter().filter_map(move |(location, task)| {
Some((kind.clone(), location?, task))
})
})
.fold(HashMap::default(), |mut acc, (kind, location, task)| {
let buffer = location.target.buffer;
let buffer_snapshot = buffer.read(cx).snapshot();
let offset = display_snapshot.buffer_snapshot.excerpts().find_map(
|(excerpt_id, snapshot, _)| {
if snapshot.remote_id() == buffer_snapshot.remote_id() {
display_snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, location.target.range.start)
} else {
None
}
},
);
if let Some(offset) = offset {
let task_buffer_range =
location.target.range.to_point(&buffer_snapshot);
let context_buffer_range =
task_buffer_range.to_offset(&buffer_snapshot);
let context_range = BufferOffset(context_buffer_range.start)
..BufferOffset(context_buffer_range.end);
acc.entry((buffer_snapshot.remote_id(), task_buffer_range.start.row))
.or_insert_with(|| RunnableTasks {
templates: Vec::new(),
offset,
column: task_buffer_range.start.column,
extra_variables: HashMap::default(),
context_range,
})
.templates
.push((kind, task.original_task().clone()));
}
acc
})
}) else {
return;
};
let rows = Self::runnable_rows(project, display_snapshot, new_rows, cx.clone());
editor
.update(cx, |editor, _| {
editor.clear_tasks();
for (key, mut value) in rows {
if let Some(lsp_tasks) = lsp_tasks_by_rows.remove(&key) {
value.templates.extend(lsp_tasks.templates);
}
editor.insert_tasks(key, value);
}
for (key, value) in lsp_tasks_by_rows {
editor.insert_tasks(key, value);
}
})
.ok();
this.update(cx, |this, _| {
this.clear_tasks();
for (key, value) in rows {
this.insert_tasks(key, value);
}
})
.ok();
})
}
fn fetch_runnable_ranges(
@@ -12598,7 +12500,7 @@ impl Editor {
snapshot: DisplaySnapshot,
runnable_ranges: Vec<RunnableRange>,
mut cx: AsyncWindowContext,
) -> Vec<((BufferId, BufferRow), RunnableTasks)> {
) -> Vec<((BufferId, u32), RunnableTasks)> {
runnable_ranges
.into_iter()
.filter_map(|mut runnable| {
@@ -12655,9 +12557,11 @@ impl Editor {
)
});
let mut templates_with_tags = mem::take(&mut runnable.tags)
let tags = mem::take(&mut runnable.tags);
let mut tags: Vec<_> = tags
.into_iter()
.flat_map(|RunnableTag(tag)| {
.flat_map(|tag| {
let tag = tag.0.clone();
inventory
.as_ref()
.into_iter()
@@ -12674,20 +12578,20 @@ impl Editor {
})
})
.sorted_by_key(|(kind, _)| kind.to_owned())
.collect::<Vec<_>>();
if let Some((leading_tag_source, _)) = templates_with_tags.first() {
.collect();
if let Some((leading_tag_source, _)) = tags.first() {
// Strongest source wins; if we have worktree tag binding, prefer that to
// global and language bindings;
// if we have a global binding, prefer that to language binding.
let first_mismatch = templates_with_tags
let first_mismatch = tags
.iter()
.position(|(tag_source, _)| tag_source != leading_tag_source);
if let Some(index) = first_mismatch {
templates_with_tags.truncate(index);
tags.truncate(index);
}
}
templates_with_tags
tags
}
pub fn move_to_enclosing_bracket(
@@ -18014,81 +17918,6 @@ impl Editor {
}
}
// Consider user intent and default settings
fn choose_completion_range(
completion: &Completion,
intent: CompletionIntent,
buffer: &Entity<Buffer>,
cx: &mut Context<Editor>,
) -> Range<usize> {
fn should_replace(
completion: &Completion,
insert_range: &Range<text::Anchor>,
intent: CompletionIntent,
completion_mode_setting: LspInsertMode,
buffer: &Buffer,
) -> bool {
// specific actions take precedence over settings
match intent {
CompletionIntent::CompleteWithInsert => return false,
CompletionIntent::CompleteWithReplace => return true,
CompletionIntent::Complete | CompletionIntent::Compose => {}
}
match completion_mode_setting {
LspInsertMode::Insert => false,
LspInsertMode::Replace => true,
LspInsertMode::ReplaceSubsequence => {
let mut text_to_replace = buffer.chars_for_range(
buffer.anchor_before(completion.replace_range.start)
..buffer.anchor_after(completion.replace_range.end),
);
let mut completion_text = completion.new_text.chars();
// is `text_to_replace` a subsequence of `completion_text`
text_to_replace
.all(|needle_ch| completion_text.any(|haystack_ch| haystack_ch == needle_ch))
}
LspInsertMode::ReplaceSuffix => {
let range_after_cursor = insert_range.end..completion.replace_range.end;
let text_after_cursor = buffer
.text_for_range(
buffer.anchor_before(range_after_cursor.start)
..buffer.anchor_after(range_after_cursor.end),
)
.collect::<String>();
completion.new_text.ends_with(&text_after_cursor)
}
}
}
let buffer = buffer.read(cx);
if let CompletionSource::Lsp {
insert_range: Some(insert_range),
..
} = &completion.source
{
let completion_mode_setting =
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
.completions
.lsp_insert_mode;
if !should_replace(
completion,
&insert_range,
intent,
completion_mode_setting,
buffer,
) {
return insert_range.to_offset(buffer);
}
}
completion.replace_range.to_offset(buffer)
}
fn insert_extra_newline_brackets(
buffer: &MultiBufferSnapshot,
range: Range<usize>,
@@ -18810,10 +18639,9 @@ fn snippet_completions(
end: lsp_end,
};
Some(Completion {
replace_range: range,
old_range: range,
new_text: snippet.body.clone(),
source: CompletionSource::Lsp {
insert_range: None,
server_id: LanguageServerId(usize::MAX),
resolved: true,
lsp_completion: Box::new(lsp::CompletionItem {

View File

@@ -3875,41 +3875,6 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_toggle_case(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
// If all lower case -> upper case
cx.set_state(indoc! {"
«hello worldˇ»
"});
cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
cx.assert_editor_state(indoc! {"
«HELLO WORLDˇ»
"});
// If all upper case -> lower case
cx.set_state(indoc! {"
«HELLO WORLDˇ»
"});
cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
cx.assert_editor_state(indoc! {"
«hello worldˇ»
"});
// If any upper case characters are identified -> lower case
// This matches JetBrains IDEs
cx.set_state(indoc! {"
«hEllo worldˇ»
"});
cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
cx.assert_editor_state(indoc! {"
«hello worldˇ»
"});
}
#[gpui::test]
async fn test_manipulate_text(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -9253,7 +9218,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: String,
buffer_marked_text: String,
completion_text: &'static str,
expected_with_insert_mode: String,
expected_with_insertion_mode: String,
expected_with_replace_mode: String,
expected_with_replace_subsequence_mode: String,
expected_with_replace_suffix_mode: String,
@@ -9265,7 +9230,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "before ediˇ after".into(),
buffer_marked_text: "before <edi|> after".into(),
completion_text: "editor",
expected_with_insert_mode: "before editorˇ after".into(),
expected_with_insertion_mode: "before editorˇ after".into(),
expected_with_replace_mode: "before editorˇ after".into(),
expected_with_replace_subsequence_mode: "before editorˇ after".into(),
expected_with_replace_suffix_mode: "before editorˇ after".into(),
@@ -9275,7 +9240,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "before ediˇtor after".into(),
buffer_marked_text: "before <edi|tor> after".into(),
completion_text: "editor",
expected_with_insert_mode: "before editorˇtor after".into(),
expected_with_insertion_mode: "before editorˇtor after".into(),
expected_with_replace_mode: "before ediˇtor after".into(),
expected_with_replace_subsequence_mode: "before ediˇtor after".into(),
expected_with_replace_suffix_mode: "before ediˇtor after".into(),
@@ -9285,7 +9250,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "before torˇ after".into(),
buffer_marked_text: "before <tor|> after".into(),
completion_text: "editor",
expected_with_insert_mode: "before editorˇ after".into(),
expected_with_insertion_mode: "before editorˇ after".into(),
expected_with_replace_mode: "before editorˇ after".into(),
expected_with_replace_subsequence_mode: "before editorˇ after".into(),
expected_with_replace_suffix_mode: "before editorˇ after".into(),
@@ -9295,7 +9260,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "before ˇtor after".into(),
buffer_marked_text: "before <|tor> after".into(),
completion_text: "editor",
expected_with_insert_mode: "before editorˇtor after".into(),
expected_with_insertion_mode: "before editorˇtor after".into(),
expected_with_replace_mode: "before editorˇ after".into(),
expected_with_replace_subsequence_mode: "before editorˇ after".into(),
expected_with_replace_suffix_mode: "before editorˇ after".into(),
@@ -9305,7 +9270,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "pˇfield: bool".into(),
buffer_marked_text: "<p|field>: bool".into(),
completion_text: "pub ",
expected_with_insert_mode: "pub ˇfield: bool".into(),
expected_with_insertion_mode: "pub ˇfield: bool".into(),
expected_with_replace_mode: "pub ˇ: bool".into(),
expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
@@ -9315,7 +9280,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "[element_ˇelement_2]".into(),
buffer_marked_text: "[<element_|element_2>]".into(),
completion_text: "element_1",
expected_with_insert_mode: "[element_1ˇelement_2]".into(),
expected_with_insertion_mode: "[element_1ˇelement_2]".into(),
expected_with_replace_mode: "[element_1ˇ]".into(),
expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
@@ -9325,7 +9290,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "[elˇelement]".into(),
buffer_marked_text: "[<el|element>]".into(),
completion_text: "element",
expected_with_insert_mode: "[elementˇelement]".into(),
expected_with_insertion_mode: "[elementˇelement]".into(),
expected_with_replace_mode: "[elˇement]".into(),
expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
expected_with_replace_suffix_mode: "[elˇement]".into(),
@@ -9335,7 +9300,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "SubˇError".into(),
buffer_marked_text: "<Sub|Error>".into(),
completion_text: "SubscriptionError",
expected_with_insert_mode: "SubscriptionErrorˇError".into(),
expected_with_insertion_mode: "SubscriptionErrorˇError".into(),
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
@@ -9345,7 +9310,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "SubˇErr".into(),
buffer_marked_text: "<Sub|Err>".into(),
completion_text: "SubscriptionError",
expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
expected_with_insertion_mode: "SubscriptionErrorˇErr".into(),
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
@@ -9355,7 +9320,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "Suˇscrirr".into(),
buffer_marked_text: "<Su|scrirr>".into(),
completion_text: "SubscriptionError",
expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
expected_with_insertion_mode: "SubscriptionErrorˇscrirr".into(),
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
@@ -9365,7 +9330,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
initial_state: "foo(indˇix)".into(),
buffer_marked_text: "foo(<ind|ix>)".into(),
completion_text: "node_index",
expected_with_insert_mode: "foo(node_indexˇix)".into(),
expected_with_insertion_mode: "foo(node_indexˇix)".into(),
expected_with_replace_mode: "foo(node_indexˇ)".into(),
expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
@@ -9374,7 +9339,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
for run in runs {
let run_variations = [
(LspInsertMode::Insert, run.expected_with_insert_mode),
(LspInsertMode::Insert, run.expected_with_insertion_mode),
(LspInsertMode::Replace, run.expected_with_replace_mode),
(
LspInsertMode::ReplaceSubsequence,
@@ -9430,98 +9395,6 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
}
}
#[gpui::test]
async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
resolve_provider: Some(true),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
let initial_state = "SubˇError";
let buffer_marked_text = "<Sub|Error>";
let completion_text = "SubscriptionError";
let expected_with_insert_mode = "SubscriptionErrorˇError";
let expected_with_replace_mode = "SubscriptionErrorˇ";
update_test_language_settings(&mut cx, |settings| {
settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled,
// set the opposite here to ensure that the action is overriding the default behavior
lsp_insert_mode: LspInsertMode::Insert,
lsp: true,
lsp_fetch_timeout_ms: 0,
});
});
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
let counter = Arc::new(AtomicUsize::new(0));
handle_completion_request_with_insert_and_replace(
&mut cx,
&buffer_marked_text,
vec![completion_text],
counter.clone(),
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
let apply_additional_edits = cx.update_editor(|editor, window, cx| {
editor
.confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
.unwrap()
});
cx.assert_editor_state(&expected_with_replace_mode);
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
update_test_language_settings(&mut cx, |settings| {
settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled,
// set the opposite here to ensure that the action is overriding the default behavior
lsp_insert_mode: LspInsertMode::Replace,
lsp: true,
lsp_fetch_timeout_ms: 0,
});
});
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
handle_completion_request_with_insert_and_replace(
&mut cx,
&buffer_marked_text,
vec![completion_text],
counter.clone(),
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
let apply_additional_edits = cx.update_editor(|editor, window, cx| {
editor
.confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
.unwrap()
});
cx.assert_editor_state(&expected_with_insert_mode);
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
}
#[gpui::test]
async fn test_completion(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -12666,7 +12539,6 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
initialization_options: Some(json!({
"some other init value": false
})),
enable_lsp_tasks: false,
},
);
});
@@ -12686,7 +12558,6 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
initialization_options: Some(json!({
"anotherInitValue": false
})),
enable_lsp_tasks: false,
},
);
});
@@ -12706,7 +12577,6 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
initialization_options: Some(json!({
"anotherInitValue": false
})),
enable_lsp_tasks: false,
},
);
});
@@ -12724,7 +12594,6 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
binary: None,
settings: None,
initialization_options: None,
enable_lsp_tasks: false,
},
);
});

View File

@@ -211,7 +211,6 @@ impl EditorElement {
register_action(editor, window, Editor::sort_lines_case_insensitive);
register_action(editor, window, Editor::reverse_lines);
register_action(editor, window, Editor::shuffle_lines);
register_action(editor, window, Editor::toggle_case);
register_action(editor, window, Editor::convert_to_upper_case);
register_action(editor, window, Editor::convert_to_lower_case);
register_action(editor, window, Editor::convert_to_title_case);
@@ -462,20 +461,6 @@ impl EditorElement {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_completion_replace(action, window, cx) {
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_completion_insert(action, window, cx) {
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.compose_completion(action, window, cx) {
task.detach_and_notify_err(window, cx);

View File

@@ -1,25 +1,12 @@
use std::sync::Arc;
use crate::Editor;
use collections::HashMap;
use futures::stream::FuturesUnordered;
use gpui::{App, AppContext as _, Entity, Task};
use itertools::Itertools;
use language::Buffer;
use language::Language;
use lsp::LanguageServerId;
use lsp::LanguageServerName;
use multi_buffer::Anchor;
use project::LanguageServerToQuery;
use project::LocationLink;
use project::Project;
use project::TaskSourceKind;
use project::lsp_store::lsp_ext_command::GetLspRunnables;
use smol::stream::StreamExt;
use task::ResolvedTask;
use task::TaskContext;
use text::BufferId;
use util::ResultExt as _;
pub(crate) fn find_specific_language_server_in_selection<F>(
editor: &Editor,
@@ -73,83 +60,3 @@ where
None
})
}
pub fn lsp_tasks(
project: Entity<Project>,
task_sources: &HashMap<LanguageServerName, Vec<BufferId>>,
for_position: Option<text::Anchor>,
cx: &mut App,
) -> Task<Vec<(TaskSourceKind, Vec<(Option<LocationLink>, ResolvedTask)>)>> {
let mut lsp_task_sources = task_sources
.iter()
.map(|(name, buffer_ids)| {
let buffers = buffer_ids
.iter()
.filter_map(|&buffer_id| project.read(cx).buffer_for_id(buffer_id, cx))
.collect::<Vec<_>>();
language_server_for_buffers(project.clone(), name.clone(), buffers, cx)
})
.collect::<FuturesUnordered<_>>();
cx.spawn(async move |cx| {
let mut lsp_tasks = Vec::new();
let lsp_task_context = TaskContext::default();
while let Some(server_to_query) = lsp_task_sources.next().await {
if let Some((server_id, buffers)) = server_to_query {
let source_kind = TaskSourceKind::Lsp(server_id);
let id_base = source_kind.to_id_base();
let mut new_lsp_tasks = Vec::new();
for buffer in buffers {
if let Ok(runnables_task) = project.update(cx, |project, cx| {
let buffer_id = buffer.read(cx).remote_id();
project.request_lsp(
buffer,
LanguageServerToQuery::Other(server_id),
GetLspRunnables {
buffer_id,
position: for_position,
},
cx,
)
}) {
if let Some(new_runnables) = runnables_task.await.log_err() {
new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map(
|(location, runnable)| {
let resolved_task =
runnable.resolve_task(&id_base, &lsp_task_context)?;
Some((location, resolved_task))
},
));
}
}
}
lsp_tasks.push((source_kind, new_lsp_tasks));
}
}
lsp_tasks
})
}
fn language_server_for_buffers(
project: Entity<Project>,
name: LanguageServerName,
candidates: Vec<Entity<Buffer>>,
cx: &mut App,
) -> Task<Option<(LanguageServerId, Vec<Entity<Buffer>>)>> {
cx.spawn(async move |cx| {
for buffer in &candidates {
let server_id = buffer
.update(cx, |buffer, cx| {
project.update(cx, |project, cx| {
project.language_server_id_for_name(buffer, &name.0, cx)
})
})
.ok()?
.await;
if let Some(server_id) = server_id {
return Some((server_id, candidates));
}
}
None
})
}

View File

@@ -1,12 +1,9 @@
use crate::Editor;
use collections::HashMap;
use gpui::{App, Task, Window};
use lsp::LanguageServerName;
use project::{Location, project_settings::ProjectSettings};
use settings::Settings as _;
use project::Location;
use task::{TaskContext, TaskVariables, VariableName};
use text::{BufferId, ToOffset, ToPoint};
use text::{ToOffset, ToPoint};
impl Editor {
pub fn task_context(&self, window: &mut Window, cx: &mut App) -> Task<Option<TaskContext>> {
@@ -73,38 +70,4 @@ impl Editor {
})
})
}
pub fn lsp_task_sources(&self, cx: &App) -> HashMap<LanguageServerName, Vec<BufferId>> {
let lsp_settings = &ProjectSettings::get_global(cx).lsp;
self.buffer()
.read(cx)
.all_buffers()
.into_iter()
.filter_map(|buffer| {
let lsp_tasks_source = buffer
.read(cx)
.language()?
.context_provider()?
.lsp_task_source()?;
if lsp_settings
.get(&lsp_tasks_source)
.map_or(true, |s| s.enable_lsp_tasks)
{
let buffer_id = buffer.read(cx).remote_id();
Some((lsp_tasks_source, buffer_id))
} else {
None
}
})
.fold(
HashMap::default(),
|mut acc, (lsp_task_source, buffer_id)| {
acc.entry(lsp_task_source)
.or_insert_with(Vec::new)
.push(buffer_id);
acc
},
)
}
}

View File

@@ -3953,7 +3953,8 @@ impl Render for GitPanelMessageTooltip {
}
}
#[derive(IntoElement, RegisterComponent)]
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
pub struct PanelRepoFooter {
active_repository: SharedString,
branch: Option<Branch>,
@@ -4133,12 +4134,8 @@ impl RenderOnce for PanelRepoFooter {
}
}
impl Component for PanelRepoFooter {
fn scope() -> ComponentScope {
ComponentScope::VersionControl
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
impl ComponentPreview for PanelRepoFooter {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
let unknown_upstream = None;
let no_remote_upstream = Some(UpstreamTracking::Gone);
let ahead_of_upstream = Some(
@@ -4210,180 +4207,192 @@ impl Component for PanelRepoFooter {
}
let example_width = px(340.);
Some(
v_flex()
.gap_6()
.w_full()
.flex_none()
.children(vec![
example_group_with_title(
"Action Button States",
vec![
single_example(
"No Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(1).clone(),
None,
))
.into_any_element(),
),
single_example(
"Remote status unknown",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(2).clone(),
Some(branch(unknown_upstream)),
))
.into_any_element(),
),
single_example(
"No Remote Upstream",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(3).clone(),
Some(branch(no_remote_upstream)),
))
.into_any_element(),
),
single_example(
"Not Ahead or Behind",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(4).clone(),
Some(branch(not_ahead_or_behind_upstream)),
))
.into_any_element(),
),
single_example(
"Behind remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(5).clone(),
Some(branch(behind_upstream)),
))
.into_any_element(),
),
single_example(
"Ahead of remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(6).clone(),
Some(branch(ahead_of_upstream)),
))
.into_any_element(),
),
single_example(
"Ahead and behind remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(7).clone(),
Some(branch(ahead_and_behind_upstream)),
))
.into_any_element(),
),
],
)
.grow()
.vertical(),
])
.children(vec![
example_group_with_title(
"Labels",
vec![
single_example(
"Short Branch & Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom("main", behind_upstream)),
))
.into_any_element(),
),
single_example(
"Long Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
),
single_example(
"Long Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed-industries-community-examples"),
Some(custom("gpui", ahead_of_upstream)),
))
.into_any_element(),
),
single_example(
"Long Repo & Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed-industries-community-examples"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
),
single_example(
"Uppercase Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("LICENSES"),
Some(custom("main", ahead_of_upstream)),
))
.into_any_element(),
),
single_example(
"Uppercase Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom("update-README", behind_upstream)),
))
.into_any_element(),
),
],
)
.grow()
.vertical(),
])
.into_any_element(),
)
v_flex()
.gap_6()
.w_full()
.flex_none()
.children(vec![
example_group_with_title(
"Action Button States",
vec![
single_example(
"No Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(1).clone(),
None,
))
.into_any_element(),
)
.grow(),
single_example(
"Remote status unknown",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(2).clone(),
Some(branch(unknown_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"No Remote Upstream",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(3).clone(),
Some(branch(no_remote_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Not Ahead or Behind",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(4).clone(),
Some(branch(not_ahead_or_behind_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Behind remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(5).clone(),
Some(branch(behind_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Ahead of remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(6).clone(),
Some(branch(ahead_of_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Ahead and behind remote",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
active_repository(7).clone(),
Some(branch(ahead_and_behind_upstream)),
))
.into_any_element(),
)
.grow(),
],
)
.grow()
.vertical(),
])
.children(vec![
example_group_with_title(
"Labels",
vec![
single_example(
"Short Branch & Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom("main", behind_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed-industries-community-examples"),
Some(custom("gpui", ahead_of_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Repo & Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed-industries-community-examples"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
)
.grow(),
single_example(
"Uppercase Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("LICENSES"),
Some(custom("main", ahead_of_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Uppercase Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
SharedString::from("zed"),
Some(custom("update-README", behind_upstream)),
))
.into_any_element(),
)
.grow(),
],
)
.grow()
.vertical(),
])
.into_any_element()
}
}

View File

@@ -441,8 +441,8 @@ mod remote_button {
}
}
/// A visual representation of a file's Git status.
#[derive(IntoElement, RegisterComponent)]
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
pub struct GitStatusIcon {
status: FileStatus,
}
@@ -484,12 +484,8 @@ impl RenderOnce for GitStatusIcon {
}
// View this component preview using `workspace: open component-preview`
impl Component for GitStatusIcon {
fn scope() -> ComponentScope {
ComponentScope::VersionControl
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
impl ComponentPreview for GitStatusIcon {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
fn tracked_file_status(code: StatusCode) -> FileStatus {
FileStatus::Tracked(git::status::TrackedStatus {
index_status: code,
@@ -506,19 +502,17 @@ impl Component for GitStatusIcon {
}
.into();
Some(
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
single_example("Added", GitStatusIcon::new(added).into_any_element()),
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
single_example(
"Conflicted",
GitStatusIcon::new(conflict).into_any_element(),
),
])])
.into_any_element(),
)
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
single_example("Added", GitStatusIcon::new(added).into_any_element()),
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
single_example(
"Conflicted",
GitStatusIcon::new(conflict).into_any_element(),
),
])])
.into_any_element()
}
}

View File

@@ -1005,7 +1005,8 @@ impl Render for ProjectDiffToolbar {
}
}
#[derive(IntoElement, RegisterComponent)]
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
pub struct ProjectDiffEmptyState {
pub no_repo: bool,
pub can_push_and_pull: bool,
@@ -1177,12 +1178,8 @@ mod preview {
use super::ProjectDiffEmptyState;
// View this component preview using `workspace: open component-preview`
impl Component for ProjectDiffEmptyState {
fn scope() -> ComponentScope {
ComponentScope::VersionControl
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
impl ComponentPreview for ProjectDiffEmptyState {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
let unknown_upstream: Option<UpstreamTracking> = None;
let ahead_of_upstream: Option<UpstreamTracking> = Some(
UpstreamTrackingStatus {
@@ -1247,48 +1244,46 @@ mod preview {
let (width, height) = (px(480.), px(320.));
Some(
v_flex()
.gap_6()
.children(vec![
example_group(vec![
single_example(
"No Repo",
div()
.w(width)
.h(height)
.child(no_repo_state)
.into_any_element(),
),
single_example(
"No Changes",
div()
.w(width)
.h(height)
.child(no_changes_state)
.into_any_element(),
),
single_example(
"Unknown Upstream",
div()
.w(width)
.h(height)
.child(unknown_upstream_state)
.into_any_element(),
),
single_example(
"Ahead of Remote",
div()
.w(width)
.h(height)
.child(ahead_of_upstream_state)
.into_any_element(),
),
])
.vertical(),
v_flex()
.gap_6()
.children(vec![
example_group(vec![
single_example(
"No Repo",
div()
.w(width)
.h(height)
.child(no_repo_state)
.into_any_element(),
),
single_example(
"No Changes",
div()
.w(width)
.h(height)
.child(no_changes_state)
.into_any_element(),
),
single_example(
"Unknown Upstream",
div()
.w(width)
.h(height)
.child(unknown_upstream_state)
.into_any_element(),
),
single_example(
"Ahead of Remote",
div()
.w(width)
.h(height)
.child(ahead_of_upstream_state)
.into_any_element(),
),
])
.into_any_element(),
)
.vertical(),
])
.into_any_element()
}
}
}

View File

@@ -3,7 +3,6 @@ use std::time::{Duration, Instant};
use crate::{AnyElement, App, Element, ElementId, GlobalElementId, IntoElement, Window};
pub use easing::*;
use smallvec::SmallVec;
/// An animation that can be applied to an element.
pub struct Animation {
@@ -51,24 +50,6 @@ pub trait AnimationExt {
animation: Animation,
animator: impl Fn(Self, f32) -> Self + 'static,
) -> AnimationElement<Self>
where
Self: Sized,
{
AnimationElement {
id: id.into(),
element: Some(self),
animator: Box::new(move |this, _, value| animator(this, value)),
animations: smallvec::smallvec![animation],
}
}
/// Render this component or element with a chain of animations
fn with_animations(
self,
id: impl Into<ElementId>,
animations: Vec<Animation>,
animator: impl Fn(Self, usize, f32) -> Self + 'static,
) -> AnimationElement<Self>
where
Self: Sized,
{
@@ -76,7 +57,7 @@ pub trait AnimationExt {
id: id.into(),
element: Some(self),
animator: Box::new(animator),
animations: animations.into(),
animation,
}
}
}
@@ -87,8 +68,8 @@ impl<E> AnimationExt for E {}
pub struct AnimationElement<E> {
id: ElementId,
element: Option<E>,
animations: SmallVec<[Animation; 1]>,
animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
animation: Animation,
animator: Box<dyn Fn(E, f32) -> E + 'static>,
}
impl<E> AnimationElement<E> {
@@ -110,7 +91,6 @@ impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
struct AnimationState {
start: Instant,
animation_ix: usize,
}
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
@@ -128,30 +108,22 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
cx: &mut App,
) -> (crate::LayoutId, Self::RequestLayoutState) {
window.with_element_state(global_id.unwrap(), |state, window| {
let mut state = state.unwrap_or_else(|| AnimationState {
let state = state.unwrap_or_else(|| AnimationState {
start: Instant::now(),
animation_ix: 0,
});
let animation_ix = state.animation_ix;
let mut delta = state.start.elapsed().as_secs_f32()
/ self.animations[animation_ix].duration.as_secs_f32();
let mut delta =
state.start.elapsed().as_secs_f32() / self.animation.duration.as_secs_f32();
let mut done = false;
if delta > 1.0 {
if self.animations[animation_ix].oneshot {
if animation_ix >= self.animations.len() - 1 {
done = true;
} else {
state.start = Instant::now();
state.animation_ix += 1;
}
if self.animation.oneshot {
done = true;
delta = 1.0;
} else {
delta %= 1.0;
}
}
let delta = (self.animations[animation_ix].easing)(delta);
let delta = (self.animation.easing)(delta);
debug_assert!(
(0.0..=1.0).contains(&delta),
@@ -159,7 +131,7 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
);
let element = self.element.take().expect("should only be called once");
let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
let mut element = (self.animator)(element, delta).into_any_element();
if !done {
window.request_animation_frame();

View File

@@ -698,7 +698,7 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
if (is_near_rounded_corner) {
let radians = atan2(corner_center_to_point.y,
corner_center_to_point.x);
let corner_t = radians * corner_radius;
let corner_t = radians * corner_radius * dash_velocity;
if (center_to_point.x >= 0.0) {
if (center_to_point.y < 0.0) {
@@ -706,12 +706,12 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
// Subtracted because radians is pi/2 to 0 when
// going clockwise around the top right corner,
// since the y axis has been flipped
t = upto_r - corner_t * dash_velocity;
t = upto_r - corner_t;
} else {
dash_velocity = corner_dash_velocity_br;
// Added because radians is 0 to pi/2 when going
// clockwise around the bottom-right corner
t = upto_br + corner_t * dash_velocity;
t = upto_br + corner_t;
}
} else {
if (center_to_point.y >= 0.0) {
@@ -719,13 +719,13 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
// Subtracted because radians is pi/2 to 0 when
// going clockwise around the bottom-left corner,
// since the x axis has been flipped
t = upto_l - corner_t * dash_velocity;
t = upto_l - corner_t;
} else {
dash_velocity = corner_dash_velocity_tl;
// Added because radians is 0 to pi/2 when going
// clockwise around the top-left corner, since both
// axis were flipped
t = upto_tl + corner_t * dash_velocity;
t = upto_tl + corner_t;
}
}
} else {

View File

@@ -298,7 +298,7 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
if (is_near_rounded_corner) {
float radians = atan2(corner_center_to_point.y, corner_center_to_point.x);
float corner_t = radians * corner_radius;
float corner_t = radians * corner_radius * dash_velocity;
if (center_to_point.x >= 0.0) {
if (center_to_point.y < 0.0) {
@@ -306,12 +306,12 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
// Subtracted because radians is pi/2 to 0 when
// going clockwise around the top right corner,
// since the y axis has been flipped
t = upto_r - corner_t * dash_velocity;
t = upto_r - corner_t;
} else {
dash_velocity = corner_dash_velocity_br;
// Added because radians is 0 to pi/2 when going
// clockwise around the bottom-right corner
t = upto_br + corner_t * dash_velocity;
t = upto_br + corner_t;
}
} else {
if (center_to_point.y >= 0.0) {
@@ -319,13 +319,13 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
// Subtracted because radians is pi/1 to 0 when
// going clockwise around the bottom-left corner,
// since the x axis has been flipped
t = upto_l - corner_t * dash_velocity;
t = upto_l - corner_t;
} else {
dash_velocity = corner_dash_velocity_tl;
// Added because radians is 0 to pi/2 when going
// clockwise around the top-left corner, since both
// axis were flipped
t = upto_tl + corner_t * dash_velocity;
t = upto_tl + corner_t;
}
}
} else {

View File

@@ -141,7 +141,6 @@ pub enum IconName {
InlayHint,
Keyboard,
Library,
LightBulb,
LineHeight,
Link,
ListTree,

View File

@@ -2015,16 +2015,11 @@ impl Buffer {
}
/// Manually remove a transaction from the buffer's undo history
pub fn forget_transaction(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
self.text.forget_transaction(transaction_id)
pub fn forget_transaction(&mut self, transaction_id: TransactionId) {
self.text.forget_transaction(transaction_id);
}
/// Retrieve a transaction from the buffer's undo history
pub fn get_transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> {
self.text.get_transaction(transaction_id)
}
/// Manually merge two transactions in the buffer's undo history.
/// Manually merge two adjacent transactions in the buffer's undo history.
pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
self.text.merge_transactions(transaction, destination);
}

View File

@@ -572,11 +572,7 @@ pub trait LspAdapter: 'static + Send + Sync {
}
/// Support custom initialize params.
fn prepare_initialize_params(
&self,
original: InitializeParams,
_: &App,
) -> Result<InitializeParams> {
fn prepare_initialize_params(&self, original: InitializeParams) -> Result<InitializeParams> {
Ok(original)
}

View File

@@ -370,7 +370,7 @@ fn default_words_completion_mode() -> WordsCompletionMode {
}
fn default_lsp_insert_mode() -> LspInsertMode {
LspInsertMode::ReplaceSuffix
LspInsertMode::Insert
}
fn default_lsp_fetch_timeout_ms() -> u64 {
@@ -1029,10 +1029,7 @@ fn scroll_debounce_ms() -> u64 {
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct LanguageTaskConfig {
/// Extra task variables to set for a particular language.
#[serde(default)]
pub variables: HashMap<String, String>,
#[serde(default = "default_true")]
pub enabled: bool,
}
impl InlayHintSettings {

View File

@@ -5,7 +5,6 @@ use crate::{LanguageToolchainStore, Location, Runnable};
use anyhow::Result;
use collections::HashMap;
use gpui::{App, Task};
use lsp::LanguageServerName;
use task::{TaskTemplates, TaskVariables};
use text::BufferId;
@@ -16,7 +15,6 @@ pub struct RunnableRange {
pub runnable: Runnable,
pub extra_captures: HashMap<String, String>,
}
/// Language Contexts are used by Zed tasks to extract information about the source file where the tasks are supposed to be scheduled from.
/// Multiple context providers may be used together: by default, Zed provides a base [`BasicContextProvider`] context that fills all non-custom [`VariableName`] variants.
///
@@ -42,9 +40,4 @@ pub trait ContextProvider: Send + Sync {
) -> Option<TaskTemplates> {
None
}
/// A language server name, that can return tasks using LSP (ext) for this language.
fn lsp_task_source(&self) -> Option<LanguageServerName> {
None
}
}

View File

@@ -4,6 +4,7 @@ mod registry;
mod request;
mod role;
mod telemetry;
mod tool_output;
#[cfg(any(test, feature = "test-support"))]
pub mod fake_provider;
@@ -26,6 +27,7 @@ use util::serde::is_default;
pub use crate::model::*;
pub use crate::rate_limiter::*;
pub use crate::tool_output::{ToolOutput, StringToolOutput};
pub use crate::registry::*;
pub use crate::request::*;
pub use crate::role::*;

View File

@@ -2,6 +2,7 @@ use std::io::{Cursor, Write};
use std::sync::Arc;
use crate::role::Role;
use crate::tool_output::ToolOutput;
use crate::{LanguageModelToolUse, LanguageModelToolUseId};
use base64::write::EncoderWriter;
use gpui::{
@@ -164,12 +165,13 @@ impl LanguageModelImage {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct LanguageModelToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub tool_name: Arc<str>,
pub is_error: bool,
pub content: Arc<str>,
pub tool_output: Option<Arc<ToolOutput>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]

View File

@@ -0,0 +1,33 @@
use gpui::{AnyElement, App, SharedString, Window};
use serde::{Deserialize, Serialize};
/// An enum that represents different types of tool outputs that can be provided to the language model
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ToolOutput {
/// A simple string output
String {
string: SharedString,
rendered: Entity<Markdown>,
},
// Add other tool output types here as variants
}
impl ToolOutput {
/// Returns a string that will be given to the model
/// as the tool output.
pub fn response_for_model(&self) -> SharedString {
match self {
ToolOutput::String(output) => output.0.clone(),
// Handle other variants here
}
}
/// Returns a custom UI element to render the tool's output.
/// Returns None by default to indicate that rendering has not yet been
/// implemented for this tool, and the caller should do some default rendering.
pub fn render(&self, _window: &mut Window, _cx: &App) -> Option<AnyElement> {
match self {
ToolOutput::String { string, rendered } => todo!(),
}
}
}

View File

@@ -1,7 +1,7 @@
use anyhow::{Context, Result, anyhow, bail};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::{App, AsyncApp};
use gpui::AsyncApp;
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
pub use language::*;
use lsp::{DiagnosticTag, InitializeParams, LanguageServerBinary, LanguageServerName};
@@ -273,7 +273,6 @@ impl super::LspAdapter for CLspAdapter {
fn prepare_initialize_params(
&self,
mut original: InitializeParams,
_: &App,
) -> Result<InitializeParams> {
let experimental = json!({
"textDocument": {

View File

@@ -7,11 +7,8 @@ use gpui::{App, AsyncApp, SharedString, Task};
use http_client::github::AssetKind;
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
pub use language::*;
use lsp::{InitializeParams, LanguageServerBinary};
use project::project_settings::ProjectSettings;
use lsp::LanguageServerBinary;
use regex::Regex;
use serde_json::json;
use settings::Settings as _;
use smol::fs::{self};
use std::fmt::Display;
use std::{
@@ -21,7 +18,6 @@ use std::{
sync::{Arc, LazyLock},
};
use task::{TaskTemplate, TaskTemplates, TaskType, TaskVariables, VariableName};
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
use crate::language_settings::language_settings;
@@ -52,9 +48,9 @@ impl RustLspAdapter {
const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
}
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
impl RustLspAdapter {
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
fn build_asset_name() -> String {
let extension = match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz => "tar.gz",
@@ -64,7 +60,7 @@ impl RustLspAdapter {
format!(
"{}-{}-{}.{}",
SERVER_NAME,
Self::SERVER_NAME,
std::env::consts::ARCH,
Self::ARCH_SERVER_NAME,
extension
@@ -102,7 +98,7 @@ impl ManifestProvider for CargoManifestProvider {
#[async_trait(?Send)]
impl LspAdapter for RustLspAdapter {
fn name(&self) -> LanguageServerName {
SERVER_NAME.clone()
Self::SERVER_NAME.clone()
}
fn manifest_name(&self) -> Option<ManifestName> {
@@ -477,30 +473,6 @@ impl LspAdapter for RustLspAdapter {
filter_range,
})
}
fn prepare_initialize_params(
&self,
mut original: InitializeParams,
cx: &App,
) -> Result<InitializeParams> {
let enable_lsp_tasks = ProjectSettings::get_global(cx)
.lsp
.get(&SERVER_NAME)
.map_or(false, |s| s.enable_lsp_tasks);
if enable_lsp_tasks {
let experimental = json!({
"runnables": {
"kinds": [ "cargo", "shell" ],
},
});
if let Some(ref mut original_experimental) = original.capabilities.experimental {
merge_json_value_into(experimental, original_experimental);
} else {
original.capabilities.experimental = Some(experimental);
}
}
Ok(original)
}
}
pub(crate) struct RustContextProvider;
@@ -804,10 +776,6 @@ impl ContextProvider for RustContextProvider {
Some(TaskTemplates(task_templates))
}
fn lsp_task_source(&self) -> Option<LanguageServerName> {
Some(SERVER_NAME)
}
}
/// Part of the data structure of Cargo metadata

View File

@@ -22,7 +22,7 @@ collections.workspace = true
futures.workspace = true
gpui.workspace = true
log.workspace = true
lsp-types.workspace = true
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "1fff0dd12e2071c5667327394cfec163d2a466ab" }
parking_lot.workspace = true
postage.workspace = true
serde.workspace = true

View File

@@ -98,6 +98,19 @@ pub fn parse_markdown(
// HTML entities or smart punctuation has occurred. When these substitutions occur,
// `parsed` only consists of the result of a single substitution.
if !cow_str_points_inside(&parsed, text) {
// Attempt to detect cases where the assumptions here are not valid or the
// behavior has changed.
if parsed.len() > 4 {
log::error!(
"Bug in markdown parser. \
pulldown_cmark::Event::Text expected to a substituted HTML entity, \
but it was longer than expected.\n\
Source: {}\n\
Parsed: {}",
&text[range.clone()],
parsed
);
}
events.push((range, MarkdownEvent::SubstitutedText(parsed.into())));
} else {
// Automatically detect links in text if not already within a markdown link.
@@ -419,18 +432,12 @@ impl From<pulldown_cmark::Tag<'_>> for MarkdownTag {
/// more efficient - it fits within a `pulldown_cmark::InlineStr` in all known cases.
///
/// Same as `pulldown_cmark::CowStr` but without the `Borrow` case.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum CompactStr {
Boxed(Box<str>),
Inlined(InlineStr),
}
impl std::fmt::Debug for CompactStr {
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
self.deref().fmt(formatter)
}
}
impl Deref for CompactStr {
type Target = str;
@@ -544,10 +551,10 @@ mod tests {
#[test]
fn test_smart_punctuation() {
assert_eq!(
parse_markdown("-- --- ... \"double quoted\" 'single quoted' ----------"),
parse_markdown("-- --- ... \"double quoted\" 'single quoted'"),
(
vec![
(0..53, Start(Paragraph)),
(0..42, Start(Paragraph)),
(0..2, SubstitutedText("".into())),
(2..3, Text),
(3..6, SubstitutedText("".into())),
@@ -561,9 +568,7 @@ mod tests {
(27..28, SubstitutedText("".into())),
(28..41, Text),
(41..42, SubstitutedText("".into())),
(42..43, Text),
(43..53, SubstitutedText("".into())),
(0..53, End(MarkdownTagEnd::Paragraph))
(0..42, End(MarkdownTagEnd::Paragraph))
],
HashSet::new(),
HashSet::new()

View File

@@ -1718,25 +1718,21 @@ impl MultiBuffer {
(None, None) => break,
(None, Some(_)) => {
let existing_id = existing_iter.next().unwrap();
let locator = snapshot.excerpt_locator_for_id(existing_id);
let existing_excerpt = excerpts_cursor.item().unwrap();
excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &());
let existing_end = existing_excerpt
.range
.context
.end
.to_point(&buffer_snapshot);
if let Some((new_id, last)) = to_insert.last() {
let locator = snapshot.excerpt_locator_for_id(existing_id);
excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &());
if let Some(existing_excerpt) = excerpts_cursor
.item()
.filter(|e| e.buffer_id == buffer_snapshot.remote_id())
{
let existing_end = existing_excerpt
.range
.context
.end
.to_point(&buffer_snapshot);
if existing_end <= last.context.end {
self.snapshot
.borrow_mut()
.replaced_excerpts
.insert(existing_id, *new_id);
}
};
if existing_end <= last.context.end {
self.snapshot
.borrow_mut()
.replaced_excerpts
.insert(existing_id, *new_id);
}
}
to_remove.push(existing_id);
continue;
@@ -1749,14 +1745,16 @@ impl MultiBuffer {
};
let locator = snapshot.excerpt_locator_for_id(*existing);
excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &());
let Some(existing_excerpt) = excerpts_cursor
.item()
.filter(|e| e.buffer_id == buffer_snapshot.remote_id())
else {
let Some(existing_excerpt) = excerpts_cursor.item() else {
to_remove.push(existing_iter.next().unwrap());
to_insert.push((next_excerpt_id(), new_iter.next().unwrap()));
continue;
};
if existing_excerpt.buffer_id != buffer_snapshot.remote_id() {
to_remove.push(existing_iter.next().unwrap());
to_insert.push((next_excerpt_id(), new_iter.next().unwrap()));
continue;
}
let existing_start = existing_excerpt
.range

View File

@@ -1798,88 +1798,6 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_set_excerpts_for_buffer_rename(cx: &mut TestAppContext) {
let buf1 = cx.new(|cx| {
Buffer::local(
indoc! {
"zero
one
two
three
four
five
six
seven
",
},
cx,
)
});
let path: PathKey = PathKey::namespaced(0, Path::new("/").into());
let buf2 = cx.new(|cx| {
Buffer::local(
indoc! {
"000
111
222
333
"
},
cx,
)
});
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
path.clone(),
buf1.clone(),
vec![Point::row_range(1..1), Point::row_range(4..5)],
1,
cx,
);
});
assert_excerpts_match(
&multibuffer,
cx,
indoc! {
"-----
zero
one
two
-----
three
four
five
six
"
},
);
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
path.clone(),
buf2.clone(),
vec![Point::row_range(0..1)],
2,
cx,
);
});
assert_excerpts_match(
&multibuffer,
cx,
indoc! {"-----
000
111
222
333
"},
);
}
#[gpui::test]
fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
let base_text_1 = indoc!(

View File

@@ -33,7 +33,8 @@ impl From<IconName> for ToastIcon {
}
}
#[derive(RegisterComponent)]
#[derive(IntoComponent)]
#[component(scope = "Notification")]
pub struct StatusToast {
icon: Option<ToastIcon>,
text: SharedString,
@@ -134,12 +135,8 @@ impl Focusable for StatusToast {
impl EventEmitter<DismissEvent> for StatusToast {}
impl Component for StatusToast {
fn scope() -> ComponentScope {
ComponentScope::Notification
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
impl ComponentPreview for StatusToast {
fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
let text_example = StatusToast::new("Operation completed", cx, |this, _| this);
let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| {
@@ -178,40 +175,29 @@ impl Component for StatusToast {
})
});
Some(
v_flex()
.gap_6()
.p_4()
.children(vec![
example_group_with_title(
"Basic Toast",
vec![
single_example("Text", div().child(text_example).into_any_element()),
single_example(
"Action",
div().child(action_example).into_any_element(),
),
single_example("Icon", div().child(icon_example).into_any_element()),
],
),
example_group_with_title(
"Examples",
vec![
single_example(
"Success",
div().child(success_example).into_any_element(),
),
single_example("Error", div().child(error_example).into_any_element()),
single_example(
"Warning",
div().child(warning_example).into_any_element(),
),
single_example("Create PR", div().child(pr_example).into_any_element()),
],
)
.vertical(),
])
.into_any_element(),
)
v_flex()
.gap_6()
.p_4()
.children(vec![
example_group_with_title(
"Basic Toast",
vec![
single_example("Text", div().child(text_example).into_any_element()),
single_example("Action", div().child(action_example).into_any_element()),
single_example("Icon", div().child(icon_example).into_any_element()),
],
),
example_group_with_title(
"Examples",
vec![
single_example("Success", div().child(success_example).into_any_element()),
single_example("Error", div().child(error_example).into_any_element()),
single_example("Warning", div().child(warning_example).into_any_element()),
single_example("Create PR", div().child(pr_example).into_any_element()),
],
)
.vertical(),
])
.into_any_element()
}
}

View File

@@ -275,7 +275,6 @@ impl RemoteBufferStore {
if push_to_history {
buffer.update(cx, |buffer, _| {
buffer.push_transaction(transaction.clone(), Instant::now());
buffer.finalize_last_transaction();
})?;
}
}

View File

@@ -38,7 +38,7 @@ use std::{
borrow::Borrow,
collections::{BTreeMap, HashSet},
ffi::OsStr,
path::{Path, PathBuf},
path::PathBuf,
sync::{Arc, atomic::Ordering::SeqCst},
};
use std::{collections::VecDeque, sync::atomic::AtomicU32};
@@ -57,7 +57,7 @@ pub enum DapStoreEvent {
RunInTerminal {
session_id: SessionId,
title: Option<String>,
cwd: Option<Arc<Path>>,
cwd: PathBuf,
command: Option<String>,
args: Vec<String>,
envs: HashMap<String, String>,
@@ -339,7 +339,8 @@ impl DapStore {
local_store.language_registry.clone(),
local_store.toolchain_store.clone(),
local_store.environment.update(cx, |env, cx| {
env.get_worktree_environment(worktree.clone(), cx)
let worktree = worktree.read(cx);
env.get_environment(worktree.abs_path().into(), cx)
}),
);
let session_id = local_store.next_session_id();
@@ -413,7 +414,8 @@ impl DapStore {
local_store.language_registry.clone(),
local_store.toolchain_store.clone(),
local_store.environment.update(cx, |env, cx| {
env.get_worktree_environment(worktree.clone(), cx)
let worktree = worktree.read(cx);
env.get_environment(Some(worktree.abs_path()), cx)
}),
);
let session_id = local_store.next_session_id();
@@ -549,10 +551,10 @@ impl DapStore {
let seq = request.seq;
let cwd = Path::new(&request_args.cwd);
let cwd = PathBuf::from(request_args.cwd);
match cwd.try_exists() {
Ok(false) | Err(_) if !request_args.cwd.is_empty() => {
Ok(true) => (),
Ok(false) | Err(_) => {
return session.update(cx, |session, cx| {
session.respond_to_client(
seq,
@@ -574,8 +576,8 @@ impl DapStore {
)
});
}
_ => (),
}
let mut args = request_args.args.clone();
// Handle special case for NodeJS debug adapter
@@ -602,19 +604,7 @@ impl DapStore {
}
let (tx, mut rx) = mpsc::channel::<Result<u32>>(1);
let cwd = Some(cwd)
.filter(|cwd| cwd.as_os_str().len() > 0)
.map(Arc::from)
.or_else(|| {
self.session_by_id(session_id)
.and_then(|session| {
session
.read(cx)
.configuration()
.and_then(|config| config.request.cwd())
})
.map(Arc::from)
});
cx.emit(DapStoreEvent::RunInTerminal {
session_id,
title: request_args.title,

View File

@@ -1,21 +1,22 @@
use futures::{FutureExt, future::Shared};
use language::Buffer;
use futures::{
FutureExt,
future::{Shared, WeakShared},
};
use std::{path::Path, sync::Arc};
use util::ResultExt;
use worktree::Worktree;
use collections::HashMap;
use gpui::{AppContext as _, Context, Entity, EventEmitter, Task};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
use settings::Settings as _;
use crate::{
project_settings::{DirenvSettings, ProjectSettings},
worktree_store::WorktreeStore,
worktree_store::{WorktreeStore, WorktreeStoreEvent},
};
pub struct ProjectEnvironment {
cli_environment: Option<HashMap<String, String>>,
environments: HashMap<Arc<Path>, Shared<Task<Option<HashMap<String, String>>>>>,
environments: HashMap<Arc<Path>, WeakShared<Task<Option<HashMap<String, String>>>>>,
environment_error_messages: HashMap<Arc<Path>, EnvironmentErrorMessage>,
}
@@ -26,12 +27,27 @@ pub enum ProjectEnvironmentEvent {
impl EventEmitter<ProjectEnvironmentEvent> for ProjectEnvironment {}
impl ProjectEnvironment {
pub fn new(cli_environment: Option<HashMap<String, String>>) -> Self {
Self {
cli_environment,
environments: Default::default(),
environment_error_messages: Default::default(),
}
pub fn new(
worktree_store: &Entity<WorktreeStore>,
cli_environment: Option<HashMap<String, String>>,
cx: &mut App,
) -> Entity<Self> {
cx.new(|cx| {
cx.subscribe(worktree_store, |this: &mut Self, _, event, _| {
if let WorktreeStoreEvent::WorktreeRemoved(_, _) = event {
this.environments.retain(|_, weak| weak.upgrade().is_some());
this.environment_error_messages
.retain(|abs_path, _| this.environments.contains_key(abs_path));
}
})
.detach();
Self {
cli_environment,
environments: Default::default(),
environment_error_messages: Default::default(),
}
})
}
/// Returns the inherited CLI environment, if this project was opened from the Zed CLI.
@@ -57,85 +73,54 @@ impl ProjectEnvironment {
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated);
}
pub(crate) fn get_buffer_environment(
&mut self,
buffer: Entity<Buffer>,
worktree_store: Entity<WorktreeStore>,
cx: &mut Context<Self>,
) -> Shared<Task<Option<HashMap<String, String>>>> {
if cfg!(any(test, feature = "test-support")) {
return Task::ready(Some(HashMap::default())).shared();
}
if let Some(cli_environment) = self.get_cli_environment() {
log::info!("using project environment variables from CLI");
return Task::ready(Some(cli_environment)).shared();
}
let Some(worktree) = buffer
.read(cx)
.file()
.map(|f| f.worktree_id(cx))
.and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx))
else {
return Task::ready(None).shared();
};
self.get_worktree_environment(worktree, cx)
}
pub(crate) fn get_worktree_environment(
&mut self,
worktree: Entity<Worktree>,
cx: &mut Context<Self>,
) -> Shared<Task<Option<HashMap<String, String>>>> {
if cfg!(any(test, feature = "test-support")) {
return Task::ready(Some(HashMap::default())).shared();
}
if let Some(cli_environment) = self.get_cli_environment() {
log::info!("using project environment variables from CLI");
return Task::ready(Some(cli_environment)).shared();
}
let mut abs_path = worktree.read(cx).abs_path();
if !worktree.read(cx).is_local() {
log::error!(
"attempted to get project environment for a non-local worktree at {abs_path:?}"
);
return Task::ready(None).shared();
} else if worktree.read(cx).is_single_file() {
let Some(parent) = abs_path.parent() else {
return Task::ready(None).shared();
};
abs_path = parent.into();
}
self.get_directory_environment(abs_path, cx)
}
/// Returns the project environment, if possible.
/// If the project was opened from the CLI, then the inherited CLI environment is returned.
/// If it wasn't opened from the CLI, and an absolute path is given, then a shell is spawned in
/// that directory, to get environment variables as if the user has `cd`'d there.
pub(crate) fn get_directory_environment(
pub(crate) fn get_environment(
&mut self,
abs_path: Arc<Path>,
cx: &mut Context<Self>,
abs_path: Option<Arc<Path>>,
cx: &Context<Self>,
) -> Shared<Task<Option<HashMap<String, String>>>> {
if cfg!(any(test, feature = "test-support")) {
return Task::ready(Some(HashMap::default())).shared();
}
if let Some(cli_environment) = self.get_cli_environment() {
log::info!("using project environment variables from CLI");
return Task::ready(Some(cli_environment)).shared();
return cx
.spawn(async move |_, _| {
let path = cli_environment
.get("PATH")
.map(|path| path.as_str())
.unwrap_or_default();
log::info!(
"using project environment variables from CLI. PATH={:?}",
path
);
Some(cli_environment)
})
.shared();
}
self.environments
.entry(abs_path.clone())
.or_insert_with(|| get_directory_env_impl(abs_path.clone(), cx).shared())
.clone()
let Some(abs_path) = abs_path else {
return Task::ready(None).shared();
};
if let Some(existing) = self
.environments
.get(&abs_path)
.and_then(|weak| weak.upgrade())
{
existing
} else {
let env = get_directory_env(abs_path.clone(), cx).shared();
self.environments.insert(
abs_path.clone(),
env.downgrade()
.expect("environment task has not been polled yet"),
);
env
}
}
}
@@ -353,7 +338,7 @@ async fn load_shell_environment(
(Some(parsed_env), direnv_error)
}
fn get_directory_env_impl(
fn get_directory_env(
abs_path: Arc<Path>,
cx: &Context<ProjectEnvironment>,
) -> Task<Option<HashMap<String, String>>> {
@@ -383,7 +368,10 @@ fn get_directory_env_impl(
if let Some(error) = error_message {
this.update(cx, |this, cx| {
log::error!("{error}",);
log::error!(
"error fetching environment for path {abs_path:?}: {}",
error
);
this.environment_error_messages.insert(abs_path, error);
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated)
})

View File

@@ -3263,7 +3263,7 @@ impl Repository {
remote: SharedString,
options: Option<PushOptions>,
askpass: AskPassDelegate,
cx: &mut Context<Self>,
_cx: &mut App,
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
let askpass_delegates = self.askpass_delegates.clone();
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
@@ -3276,52 +3276,25 @@ impl Repository {
})
.unwrap_or("");
let updates_tx = self
.git_store()
.and_then(|git_store| match &git_store.read(cx).state {
GitStoreState::Local { downstream, .. } => downstream
.as_ref()
.map(|downstream| downstream.updates_tx.clone()),
_ => None,
});
let this = cx.weak_entity();
self.send_job(
Some(format!("git push {} {} {}", args, branch, remote).into()),
move |git_repo, mut cx| async move {
Some(format!("git push{} {} {}", args, branch, remote).into()),
move |git_repo, cx| async move {
match git_repo {
RepositoryState::Local {
backend,
environment,
..
} => {
let result = backend
backend
.push(
branch.to_string(),
remote.to_string(),
options,
askpass,
environment.clone(),
cx.clone(),
cx,
)
.await;
if result.is_ok() {
let branches = backend.branches().await?;
let branch = branches.into_iter().find(|branch| branch.is_head);
log::info!("head branch after scan is {branch:?}");
let snapshot = this.update(&mut cx, |this, cx| {
this.snapshot.branch = branch;
let snapshot = this.snapshot.clone();
cx.emit(RepositoryEvent::Updated { full_scan: false });
snapshot
})?;
if let Some(updates_tx) = updates_tx {
updates_tx
.unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
.ok();
}
}
result
.await
}
RepositoryState::Remote { project_id, client } => {
askpass_delegates.lock().insert(askpass_id, askpass);
@@ -3776,7 +3749,7 @@ impl Repository {
.upgrade()
.ok_or_else(|| anyhow!("missing project environment"))?
.update(cx, |project_environment, cx| {
project_environment.get_directory_environment(work_directory_abs_path.clone(), cx)
project_environment.get_environment(Some(work_directory_abs_path.clone()), cx)
})?
.await
.unwrap_or_else(|| {

View File

@@ -17,7 +17,9 @@ use gpui::{App, AsyncApp, Entity};
use language::{
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16,
ToOffset, ToPointUtf16, Transaction, Unclipped,
language_settings::{InlayHintKind, LanguageSettings, language_settings},
language_settings::{
AllLanguageSettings, InlayHintKind, LanguageSettings, LspInsertMode, language_settings,
},
point_from_lsp, point_to_lsp,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, range_to_lsp,
@@ -28,6 +30,7 @@ use lsp::{
LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, OneOf, RenameOptions,
ServerCapabilities,
};
use settings::Settings as _;
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc};
use text::{BufferId, LineEnding};
@@ -974,69 +977,62 @@ async fn location_links_from_proto(
let mut links = Vec::new();
for link in proto_links {
links.push(location_link_from_proto(link, &lsp_store, &mut cx).await?)
let origin = match link.origin {
Some(origin) => {
let buffer_id = BufferId::new(origin.buffer_id)?;
let buffer = lsp_store
.update(&mut cx, |lsp_store, cx| {
lsp_store.wait_for_remote_buffer(buffer_id, cx)
})?
.await?;
let start = origin
.start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin start"))?;
let end = origin
.end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin end"))?;
buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
.await?;
Some(Location {
buffer,
range: start..end,
})
}
None => None,
};
let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
let buffer_id = BufferId::new(target.buffer_id)?;
let buffer = lsp_store
.update(&mut cx, |lsp_store, cx| {
lsp_store.wait_for_remote_buffer(buffer_id, cx)
})?
.await?;
let start = target
.start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target start"))?;
let end = target
.end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?;
buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
.await?;
let target = Location {
buffer,
range: start..end,
};
links.push(LocationLink { origin, target })
}
Ok(links)
}
pub async fn location_link_from_proto(
link: proto::LocationLink,
lsp_store: &Entity<LspStore>,
cx: &mut AsyncApp,
) -> Result<LocationLink> {
let origin = match link.origin {
Some(origin) => {
let buffer_id = BufferId::new(origin.buffer_id)?;
let buffer = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.wait_for_remote_buffer(buffer_id, cx)
})?
.await?;
let start = origin
.start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin start"))?;
let end = origin
.end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin end"))?;
buffer
.update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
.await?;
Some(Location {
buffer,
range: start..end,
})
}
None => None,
};
let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
let buffer_id = BufferId::new(target.buffer_id)?;
let buffer = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.wait_for_remote_buffer(buffer_id, cx)
})?
.await?;
let start = target
.start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target start"))?;
let end = target
.end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?;
buffer
.update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
.await?;
let target = Location {
buffer,
range: start..end,
};
Ok(LocationLink { origin, target })
}
async fn location_links_from_lsp(
message: Option<lsp::GotoDefinitionResponse>,
lsp_store: Entity<LspStore>,
@@ -1119,65 +1115,6 @@ async fn location_links_from_lsp(
Ok(definitions)
}
pub async fn location_link_from_lsp(
link: lsp::LocationLink,
lsp_store: &Entity<LspStore>,
buffer: &Entity<Buffer>,
server_id: LanguageServerId,
cx: &mut AsyncApp,
) -> Result<LocationLink> {
let (lsp_adapter, language_server) =
language_server_for_buffer(&lsp_store, &buffer, server_id, cx)?;
let (origin_range, target_uri, target_range) = (
link.origin_selection_range,
link.target_uri,
link.target_selection_range,
);
let target_buffer_handle = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.open_local_buffer_via_lsp(
target_uri,
language_server.server_id(),
lsp_adapter.name.clone(),
cx,
)
})?
.await?;
cx.update(|cx| {
let origin_location = origin_range.map(|origin_range| {
let origin_buffer = buffer.read(cx);
let origin_start =
origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left);
let origin_end =
origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left);
Location {
buffer: buffer.clone(),
range: origin_buffer.anchor_after(origin_start)
..origin_buffer.anchor_before(origin_end),
}
});
let target_buffer = target_buffer_handle.read(cx);
let target_start =
target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left);
let target_end =
target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left);
let target_location = Location {
buffer: target_buffer_handle,
range: target_buffer.anchor_after(target_start)
..target_buffer.anchor_before(target_end),
};
LocationLink {
origin: origin_location,
target: target_location,
}
})
}
fn location_links_to_proto(
links: Vec<LocationLink>,
lsp_store: &mut LspStore,
@@ -1186,50 +1123,43 @@ fn location_links_to_proto(
) -> Vec<proto::LocationLink> {
links
.into_iter()
.map(|definition| location_link_to_proto(definition, lsp_store, peer_id, cx))
.collect()
}
.map(|definition| {
let origin = definition.origin.map(|origin| {
lsp_store
.buffer_store()
.update(cx, |buffer_store, cx| {
buffer_store.create_buffer_for_peer(&origin.buffer, peer_id, cx)
})
.detach_and_log_err(cx);
pub fn location_link_to_proto(
location: LocationLink,
lsp_store: &mut LspStore,
peer_id: PeerId,
cx: &mut App,
) -> proto::LocationLink {
let origin = location.origin.map(|origin| {
lsp_store
.buffer_store()
.update(cx, |buffer_store, cx| {
buffer_store.create_buffer_for_peer(&origin.buffer, peer_id, cx)
})
.detach_and_log_err(cx);
let buffer_id = origin.buffer.read(cx).remote_id().into();
proto::Location {
start: Some(serialize_anchor(&origin.range.start)),
end: Some(serialize_anchor(&origin.range.end)),
buffer_id,
}
});
let buffer_id = origin.buffer.read(cx).remote_id().into();
proto::Location {
start: Some(serialize_anchor(&origin.range.start)),
end: Some(serialize_anchor(&origin.range.end)),
buffer_id,
}
});
lsp_store
.buffer_store()
.update(cx, |buffer_store, cx| {
buffer_store.create_buffer_for_peer(&definition.target.buffer, peer_id, cx)
})
.detach_and_log_err(cx);
lsp_store
.buffer_store()
.update(cx, |buffer_store, cx| {
buffer_store.create_buffer_for_peer(&location.target.buffer, peer_id, cx)
let buffer_id = definition.target.buffer.read(cx).remote_id().into();
let target = proto::Location {
start: Some(serialize_anchor(&definition.target.range.start)),
end: Some(serialize_anchor(&definition.target.range.end)),
buffer_id,
};
proto::LocationLink {
origin,
target: Some(target),
}
})
.detach_and_log_err(cx);
let buffer_id = location.target.buffer.read(cx).remote_id().into();
let target = proto::Location {
start: Some(serialize_anchor(&location.target.range.start)),
end: Some(serialize_anchor(&location.target.range.end)),
buffer_id,
};
proto::LocationLink {
origin,
target: Some(target),
}
.collect()
}
#[async_trait(?Send)]
@@ -2158,7 +2088,7 @@ impl LspCommand for GetCompletions {
.map(Arc::new);
let mut completion_edits = Vec::new();
buffer.update(&mut cx, |buffer, _cx| {
buffer.update(&mut cx, |buffer, cx| {
let snapshot = buffer.snapshot();
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
@@ -2195,11 +2125,21 @@ impl LspCommand for GetCompletions {
// If the language server provides a range to overwrite, then
// check that the range is valid.
Some(completion_text_edit) => {
match parse_completion_text_edit(&completion_text_edit, &snapshot) {
let completion_mode = AllLanguageSettings::get_global(cx)
.defaults
.completions
.lsp_insert_mode;
match parse_completion_text_edit(
&completion_text_edit,
&snapshot,
completion_mode,
) {
Some(edit) => edit,
None => return false,
}
}
// If the language server does not provide a range, then infer
// the range based on the syntax tree.
None => {
@@ -2251,12 +2191,7 @@ impl LspCommand for GetCompletions {
.as_ref()
.unwrap_or(&lsp_completion.label)
.clone();
ParsedCompletionEdit {
replace_range: range,
insert_range: None,
new_text: text,
}
(range, text)
}
};
@@ -2272,8 +2207,8 @@ impl LspCommand for GetCompletions {
Ok(completions
.into_iter()
.zip(completion_edits)
.map(|(mut lsp_completion, mut edit)| {
LineEnding::normalize(&mut edit.new_text);
.map(|(mut lsp_completion, (old_range, mut new_text))| {
LineEnding::normalize(&mut new_text);
if lsp_completion.data.is_none() {
if let Some(default_data) = lsp_defaults
.as_ref()
@@ -2285,10 +2220,9 @@ impl LspCommand for GetCompletions {
}
}
CoreCompletion {
replace_range: edit.replace_range,
new_text: edit.new_text,
old_range,
new_text,
source: CompletionSource::Lsp {
insert_range: edit.insert_range,
server_id,
lsp_completion: Box::new(lsp_completion),
lsp_defaults: lsp_defaults.clone(),
@@ -2378,53 +2312,91 @@ impl LspCommand for GetCompletions {
}
}
pub struct ParsedCompletionEdit {
pub replace_range: Range<Anchor>,
pub insert_range: Option<Range<Anchor>>,
pub new_text: String,
}
pub(crate) fn parse_completion_text_edit(
edit: &lsp::CompletionTextEdit,
snapshot: &BufferSnapshot,
) -> Option<ParsedCompletionEdit> {
let (replace_range, insert_range, new_text) = match edit {
lsp::CompletionTextEdit::Edit(edit) => (edit.range, None, &edit.new_text),
lsp::CompletionTextEdit::InsertAndReplace(edit) => {
(edit.replace, Some(edit.insert), &edit.new_text)
}
};
let replace_range = {
let range = range_from_lsp(replace_range);
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start.0 || end != range.end.0 {
log::info!("completion out of expected range");
return None;
}
snapshot.anchor_before(start)..snapshot.anchor_after(end)
};
let insert_range = match insert_range {
None => None,
Some(insert_range) => {
let range = range_from_lsp(insert_range);
completion_mode: LspInsertMode,
) -> Option<(Range<Anchor>, String)> {
match edit {
lsp::CompletionTextEdit::Edit(edit) => {
let range = range_from_lsp(edit.range);
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start.0 || end != range.end.0 {
log::info!("completion (insert) out of expected range");
return None;
log::info!("completion out of expected range");
None
} else {
Some((
snapshot.anchor_before(start)..snapshot.anchor_after(end),
edit.new_text.clone(),
))
}
Some(snapshot.anchor_before(start)..snapshot.anchor_after(end))
}
};
Some(ParsedCompletionEdit {
insert_range: insert_range,
replace_range: replace_range,
new_text: new_text.clone(),
})
lsp::CompletionTextEdit::InsertAndReplace(edit) => {
let replace = match completion_mode {
LspInsertMode::Insert => false,
LspInsertMode::Replace => true,
LspInsertMode::ReplaceSubsequence => {
let range_to_replace = range_from_lsp(edit.replace);
let start = snapshot.clip_point_utf16(range_to_replace.start, Bias::Left);
let end = snapshot.clip_point_utf16(range_to_replace.end, Bias::Left);
if start != range_to_replace.start.0 || end != range_to_replace.end.0 {
false
} else {
let mut completion_text = edit.new_text.chars();
let mut text_to_replace = snapshot.chars_for_range(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
);
// is `text_to_replace` a subsequence of `completion_text`
text_to_replace.all(|needle_ch| {
completion_text.any(|haystack_ch| haystack_ch == needle_ch)
})
}
}
LspInsertMode::ReplaceSuffix => {
let range_after_cursor = lsp::Range {
start: edit.insert.end,
end: edit.replace.end,
};
let range_after_cursor = range_from_lsp(range_after_cursor);
let start = snapshot.clip_point_utf16(range_after_cursor.start, Bias::Left);
let end = snapshot.clip_point_utf16(range_after_cursor.end, Bias::Left);
if start != range_after_cursor.start.0 || end != range_after_cursor.end.0 {
false
} else {
let text_after_cursor = snapshot
.text_for_range(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
)
.collect::<String>();
edit.new_text.ends_with(&text_after_cursor)
}
}
};
let range = range_from_lsp(match replace {
true => edit.replace,
false => edit.insert,
});
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start.0 || end != range.end.0 {
log::info!("completion out of expected range");
None
} else {
Some((
snapshot.anchor_before(start)..snapshot.anchor_after(end),
edit.new_text.clone(),
))
}
}
}
}
#[async_trait(?Send)]

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,12 @@
use crate::{
LocationLink,
lsp_command::{
LspCommand, location_link_from_lsp, location_link_from_proto, location_link_to_proto,
},
lsp_store::LspStore,
make_text_document_identifier,
};
use crate::{lsp_command::LspCommand, lsp_store::LspStore, make_text_document_identifier};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use collections::HashMap;
use gpui::{App, AsyncApp, Entity};
use language::{
Buffer, point_to_lsp,
proto::{deserialize_anchor, serialize_anchor},
};
use language::{Buffer, point_to_lsp, proto::deserialize_anchor};
use lsp::{LanguageServer, LanguageServerId};
use rpc::proto::{self, PeerId};
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use task::TaskTemplate;
use std::{path::Path, sync::Arc};
use text::{BufferId, PointUtf16, ToPointUtf16};
pub enum LspExpandMacro {}
@@ -378,245 +363,3 @@ impl LspCommand for SwitchSourceHeader {
BufferId::new(message.buffer_id)
}
}
// https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#runnables
// Taken from https://github.com/rust-lang/rust-analyzer/blob/a73a37a757a58b43a796d3eb86a1f7dfd0036659/crates/rust-analyzer/src/lsp/ext.rs#L425-L489
pub enum Runnables {}
impl lsp::request::Request for Runnables {
type Params = RunnablesParams;
type Result = Vec<Runnable>;
const METHOD: &'static str = "experimental/runnables";
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RunnablesParams {
pub text_document: lsp::TextDocumentIdentifier,
pub position: Option<lsp::Position>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Runnable {
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<lsp::LocationLink>,
pub kind: RunnableKind,
pub args: RunnableArgs,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum RunnableArgs {
Cargo(CargoRunnableArgs),
Shell(ShellRunnableArgs),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum RunnableKind {
Cargo,
Shell,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CargoRunnableArgs {
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>,
pub cwd: PathBuf,
/// Command to be executed instead of cargo
pub override_cargo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_root: Option<PathBuf>,
// command, --package and --lib stuff
pub cargo_args: Vec<String>,
// stuff after --
pub executable_args: Vec<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ShellRunnableArgs {
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>,
pub cwd: PathBuf,
pub program: String,
pub args: Vec<String>,
}
#[derive(Debug)]
pub struct GetLspRunnables {
pub buffer_id: BufferId,
pub position: Option<text::Anchor>,
}
#[derive(Debug, Default)]
pub struct LspRunnables {
pub runnables: Vec<(Option<LocationLink>, TaskTemplate)>,
}
#[async_trait(?Send)]
impl LspCommand for GetLspRunnables {
type Response = LspRunnables;
type LspRequest = Runnables;
type ProtoRequest = proto::LspExtRunnables;
fn display_name(&self) -> &str {
"LSP Runnables"
}
fn to_lsp(
&self,
path: &Path,
buffer: &Buffer,
_: &Arc<LanguageServer>,
_: &App,
) -> Result<RunnablesParams> {
let url = match lsp::Url::from_file_path(path) {
Ok(url) => url,
Err(()) => anyhow::bail!("Failed to parse path {path:?} as lsp::Url"),
};
Ok(RunnablesParams {
text_document: lsp::TextDocumentIdentifier::new(url),
position: self
.position
.map(|anchor| point_to_lsp(anchor.to_point_utf16(&buffer.snapshot()))),
})
}
async fn response_from_lsp(
self,
lsp_runnables: Vec<Runnable>,
lsp_store: Entity<LspStore>,
buffer: Entity<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncApp,
) -> Result<LspRunnables> {
let mut runnables = Vec::with_capacity(lsp_runnables.len());
for runnable in lsp_runnables {
let location = match runnable.location {
Some(location) => Some(
location_link_from_lsp(location, &lsp_store, &buffer, server_id, &mut cx)
.await?,
),
None => None,
};
let mut task_template = TaskTemplate::default();
task_template.label = runnable.label;
match runnable.args {
RunnableArgs::Cargo(cargo) => {
match cargo.override_cargo {
Some(override_cargo) => {
let mut override_parts =
override_cargo.split(" ").map(|s| s.to_string());
task_template.command = override_parts
.next()
.unwrap_or_else(|| override_cargo.clone());
task_template.args.extend(override_parts);
}
None => task_template.command = "cargo".to_string(),
};
task_template.env = cargo.environment;
task_template.cwd = Some(
cargo
.workspace_root
.unwrap_or(cargo.cwd)
.to_string_lossy()
.to_string(),
);
task_template.args.extend(cargo.cargo_args);
if !cargo.executable_args.is_empty() {
task_template.args.push("--".to_string());
task_template.args.extend(cargo.executable_args);
}
}
RunnableArgs::Shell(shell) => {
task_template.command = shell.program;
task_template.args = shell.args;
task_template.env = shell.environment;
task_template.cwd = Some(shell.cwd.to_string_lossy().to_string());
}
}
runnables.push((location, task_template));
}
Ok(LspRunnables { runnables })
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtRunnables {
proto::LspExtRunnables {
project_id,
buffer_id: buffer.remote_id().to_proto(),
position: self.position.as_ref().map(serialize_anchor),
}
}
async fn from_proto(
message: proto::LspExtRunnables,
_: Entity<LspStore>,
_: Entity<Buffer>,
_: AsyncApp,
) -> Result<Self> {
let buffer_id = Self::buffer_id_from_proto(&message)?;
let position = message.position.and_then(deserialize_anchor);
Ok(Self {
buffer_id,
position,
})
}
fn response_to_proto(
response: LspRunnables,
lsp_store: &mut LspStore,
peer_id: PeerId,
_: &clock::Global,
cx: &mut App,
) -> proto::LspExtRunnablesResponse {
proto::LspExtRunnablesResponse {
runnables: response
.runnables
.into_iter()
.map(|(location, task_template)| proto::LspRunnable {
location: location
.map(|location| location_link_to_proto(location, lsp_store, peer_id, cx)),
task_template: serde_json::to_vec(&task_template).unwrap(),
})
.collect(),
}
}
async fn response_from_proto(
self,
message: proto::LspExtRunnablesResponse,
lsp_store: Entity<LspStore>,
_: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<LspRunnables> {
let mut runnables = LspRunnables {
runnables: Vec::new(),
};
for lsp_runnable in message.runnables {
let location = match lsp_runnable.location {
Some(location) => {
Some(location_link_from_proto(location, &lsp_store, &mut cx).await?)
}
None => None,
};
let task_template = serde_json::from_slice(&lsp_runnable.task_template)
.context("deserializing task template from proto")?;
runnables.runnables.push((location, task_template));
}
Ok(runnables)
}
fn buffer_id_from_proto(message: &proto::LspExtRunnables) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}

View File

@@ -8,7 +8,7 @@ pub const RUST_ANALYZER_NAME: &str = "rust-analyzer";
/// Experimental: Informs the end user about the state of the server
///
/// [Rust Analyzer Specification](https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#server-status)
/// [Rust Analyzer Specification](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#server-status)
#[derive(Debug)]
enum ServerStatus {}
@@ -38,10 +38,13 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
let name = language_server.name();
let server_id = language_server.server_id();
let this = lsp_store;
language_server
.on_notification::<ServerStatus, _>({
let name = name.to_string();
move |params, cx| {
let this = this.clone();
let name = name.to_string();
if let Some(ref message) = params.message {
let message = message.trim();
@@ -50,10 +53,10 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
"Language server {name} (id {server_id}) status update: {message}"
);
match params.health {
ServerHealthStatus::Ok => log::info!("{formatted_message}"),
ServerHealthStatus::Warning => log::warn!("{formatted_message}"),
ServerHealthStatus::Ok => log::info!("{}", formatted_message),
ServerHealthStatus::Warning => log::warn!("{}", formatted_message),
ServerHealthStatus::Error => {
log::error!("{formatted_message}");
log::error!("{}", formatted_message);
let (tx, _rx) = smol::channel::bounded(1);
let request = LanguageServerPromptRequest {
level: PromptLevel::Critical,
@@ -62,7 +65,7 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
response_channel: tx,
lsp_name: name.clone(),
};
lsp_store
let _ = this
.update(cx, |_, cx| {
cx.emit(LspStoreEvent::LanguageServerPrompt(request));
})

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