Compare commits
24 Commits
bash-timeo
...
update-wor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a5c16dcfe | ||
|
|
61ddcd516f | ||
|
|
a084f42985 | ||
|
|
f12a554f86 | ||
|
|
9dae4d8c59 | ||
|
|
f0b7f355a2 | ||
|
|
b687a5e56d | ||
|
|
e66a24edcf | ||
|
|
301fc7cd7b | ||
|
|
020a1071d5 | ||
|
|
38d2487630 | ||
|
|
79c9f2bbd9 | ||
|
|
c8caae03df | ||
|
|
dabc4d8ff5 | ||
|
|
c0ad3e8183 | ||
|
|
afde25a5cb | ||
|
|
9f708ee789 | ||
|
|
58731e2fd1 | ||
|
|
d0632a5332 | ||
|
|
64cea2f1f1 | ||
|
|
ac958d4a2d | ||
|
|
2df06cd2e4 | ||
|
|
0d4ca71e68 | ||
|
|
e2d6505d12 |
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -52,6 +52,7 @@ dependencies = [
|
||||
name = "agent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent_rules",
|
||||
"anyhow",
|
||||
"assistant_context_editor",
|
||||
"assistant_settings",
|
||||
@@ -161,6 +162,20 @@ 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"
|
||||
|
||||
@@ -3,6 +3,7 @@ resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/agent",
|
||||
"crates/agent_rules",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
@@ -209,6 +210,7 @@ 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" }
|
||||
|
||||
3
assets/icons/light_bulb.svg
Normal file
3
assets/icons/light_bulb.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 751 B |
@@ -150,7 +150,7 @@
|
||||
"context": "AgentDiff",
|
||||
"bindings": {
|
||||
"ctrl-y": "agent::Keep",
|
||||
"ctrl-k ctrl-r": "agent::Reject"
|
||||
"ctrl-n": "agent::Reject"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-y": "agent::Keep",
|
||||
"cmd-alt-z": "agent::Reject"
|
||||
"cmd-n": "agent::Reject"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -155,7 +155,7 @@ There are rules that apply to these root directories:
|
||||
{{#each worktrees}}
|
||||
{{#if rules_file}}
|
||||
|
||||
`{{root_name}}/{{rules_file.rel_path}}`:
|
||||
`{{root_name}}/{{rules_file.path_in_worktree}}`:
|
||||
|
||||
``````
|
||||
{{{rules_file.text}}}
|
||||
|
||||
@@ -19,6 +19,7 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
agent_rules.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_context_editor.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
|
||||
@@ -376,7 +376,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,6 +456,7 @@ fn render_markdown_code_block(
|
||||
.contains(&(message_id, ix));
|
||||
|
||||
let codeblock_header = h_flex()
|
||||
.group("codeblock_header")
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
@@ -465,45 +466,47 @@ fn render_markdown_code_block(
|
||||
.rounded_t_md()
|
||||
.children(label)
|
||||
.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));
|
||||
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));
|
||||
|
||||
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();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}),
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
@@ -1219,17 +1222,30 @@ impl ActiveThread {
|
||||
Label::new("Generating")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
.with_animations(
|
||||
"generating-label",
|
||||
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);
|
||||
vec![
|
||||
Animation::new(Duration::from_secs(1)),
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
],
|
||||
|mut label, animation_ix, delta| {
|
||||
match animation_ix {
|
||||
0 => {
|
||||
let chars_to_show = (delta * 10.).ceil() as usize;
|
||||
let text = &"Generating"[0..chars_to_show];
|
||||
label.set_text(text);
|
||||
}
|
||||
1 => {
|
||||
let text = match delta {
|
||||
d if d < 0.25 => "Generating",
|
||||
d if d < 0.5 => "Generating.",
|
||||
d if d < 0.75 => "Generating..",
|
||||
_ => "Generating...",
|
||||
};
|
||||
label.set_text(text);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
label
|
||||
},
|
||||
)
|
||||
@@ -1753,7 +1769,7 @@ impl ActiveThread {
|
||||
None
|
||||
};
|
||||
|
||||
div()
|
||||
v_flex()
|
||||
.text_ui(cx)
|
||||
.gap_2()
|
||||
.children(
|
||||
@@ -1838,177 +1854,225 @@ impl ActiveThread {
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
let editor_bg = cx.theme().colors().editor_background;
|
||||
let editor_bg = cx.theme().colors().panel_background;
|
||||
|
||||
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…")
|
||||
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)
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx)
|
||||
.with_animation(
|
||||
"generating-label",
|
||||
Animation::new(Duration::from_secs(1)).repeat(),
|
||||
|mut label, delta| {
|
||||
let text = match delta {
|
||||
d if d < 0.25 => "Thinking",
|
||||
d if d < 0.5 => "Thinking.",
|
||||
d if d < 0.75 => "Thinking..",
|
||||
_ => "Thinking...",
|
||||
};
|
||||
label.set_text(text);
|
||||
label
|
||||
},
|
||||
)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.alpha(delta),
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| {
|
||||
label.map_element(|label| label.alpha(delta))
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new("Thought Process")
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx)
|
||||
.into_any_element()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.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({
|
||||
let (icon_name, color, animated) = if pending {
|
||||
(IconName::ArrowCircle, Color::Accent, true)
|
||||
} else {
|
||||
(IconName::Check, Color::Success, false)
|
||||
};
|
||||
|
||||
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),
|
||||
)
|
||||
})
|
||||
.when(is_open, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.id(("thinking-content", ix))
|
||||
.h_full()
|
||||
.p_2()
|
||||
.rounded_b_lg()
|
||||
.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);
|
||||
}
|
||||
}),
|
||||
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()
|
||||
.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,
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.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);
|
||||
|
||||
*is_open = !*is_open;
|
||||
}
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id(("thinking-content", ix))
|
||||
.relative()
|
||||
.mt_1p5()
|
||||
.ml_1p5()
|
||||
.pl_2p5()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.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);
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_tool_use(
|
||||
@@ -2030,6 +2094,7 @@ impl ActiveThread {
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).app_state().fs.clone());
|
||||
let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
|
||||
let edit_tools = tool_use.needs_confirmation;
|
||||
|
||||
let status_icons = div().child(match &tool_use.status {
|
||||
ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
|
||||
@@ -2206,10 +2271,10 @@ impl ActiveThread {
|
||||
};
|
||||
|
||||
div().map(|element| {
|
||||
if !tool_use.needs_confirmation {
|
||||
if !edit_tools {
|
||||
element.child(
|
||||
v_flex()
|
||||
.my_1p5()
|
||||
.my_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.group("disclosure-header")
|
||||
@@ -2516,7 +2581,7 @@ impl ActiveThread {
|
||||
let label_text = match rules_files.as_slice() {
|
||||
&[] => return div().into_any(),
|
||||
&[rules_file] => {
|
||||
format!("Using {:?} file", rules_file.rel_path)
|
||||
format!("Using {:?} file", rules_file.path_in_worktree)
|
||||
}
|
||||
rules_files => {
|
||||
format!("Using {} rules files", rules_files.len())
|
||||
|
||||
@@ -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().clone();
|
||||
let project = workspace.project();
|
||||
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(
|
||||
workspace.clone(),
|
||||
project.downgrade(),
|
||||
Some(thread_store.downgrade()),
|
||||
)
|
||||
});
|
||||
@@ -344,7 +344,7 @@ impl AssistantPanel {
|
||||
|
||||
let message_editor_context_store = cx.new(|_cx| {
|
||||
crate::context_store::ContextStore::new(
|
||||
self.workspace.clone(),
|
||||
self.project.downgrade(),
|
||||
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.workspace.clone(),
|
||||
this.project.downgrade(),
|
||||
Some(this.thread_store.downgrade()),
|
||||
)
|
||||
});
|
||||
@@ -855,9 +855,11 @@ 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 {
|
||||
@@ -873,7 +875,7 @@ impl AssistantPanel {
|
||||
})
|
||||
.unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
|
||||
|
||||
Label::new(title).truncate().into_any_element()
|
||||
Label::new(title).ml_2().truncate().into_any_element()
|
||||
}
|
||||
ActiveView::History => Label::new("History").truncate().into_any_element(),
|
||||
ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
|
||||
@@ -910,23 +912,25 @@ impl AssistantPanel {
|
||||
|
||||
let go_back_button = match &self.active_view {
|
||||
ActiveView::History | ActiveView::Configuration => Some(
|
||||
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,
|
||||
)
|
||||
}
|
||||
}),
|
||||
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,
|
||||
)
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
@@ -944,8 +948,7 @@ impl AssistantPanel {
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pl_2()
|
||||
.gap_2()
|
||||
.gap_1()
|
||||
.children(go_back_button)
|
||||
.child(self.render_title_view(window, cx)),
|
||||
)
|
||||
@@ -1080,7 +1083,7 @@ impl AssistantPanel {
|
||||
cx,
|
||||
|menu, _window, _cx| {
|
||||
menu.action(
|
||||
"New Prompt Editor",
|
||||
"New Text Thread",
|
||||
NewPromptEditor.boxed_clone(),
|
||||
)
|
||||
.when(!is_empty, |menu| {
|
||||
|
||||
@@ -867,7 +867,7 @@ mod tests {
|
||||
.expect("Opened test file wasn't an editor")
|
||||
});
|
||||
|
||||
let context_store = cx.new(|_| ContextStore::new(workspace.downgrade(), None));
|
||||
let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
let editor_entity = editor.downgrade();
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
|
||||
@@ -8,11 +8,10 @@ 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::{ProjectItem, ProjectPath, Worktree};
|
||||
use project::{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::{
|
||||
@@ -23,7 +22,7 @@ use crate::context_strip::SuggestedContext;
|
||||
use crate::thread::{Thread, ThreadId};
|
||||
|
||||
pub struct ContextStore {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
context: Vec<AssistantContext>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
|
||||
@@ -40,11 +39,11 @@ pub struct ContextStore {
|
||||
|
||||
impl ContextStore {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
project,
|
||||
thread_store,
|
||||
context: Vec::new(),
|
||||
next_context_id: ContextId(0),
|
||||
@@ -81,12 +80,7 @@ impl ContextStore {
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
else {
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("failed to read project")));
|
||||
};
|
||||
|
||||
@@ -161,11 +155,7 @@ impl ContextStore {
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
else {
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("failed to read project")));
|
||||
};
|
||||
|
||||
|
||||
@@ -262,13 +262,7 @@ impl InlineAssistant {
|
||||
}
|
||||
InlineAssistTarget::Terminal(active_terminal) => {
|
||||
TerminalInlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(
|
||||
&active_terminal,
|
||||
cx.entity().downgrade(),
|
||||
thread_store,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
assistant.assist(&active_terminal, cx.entity(), thread_store, window, cx)
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -322,6 +316,13 @@ 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),
|
||||
@@ -425,7 +426,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(workspace.clone(), thread_store.clone()));
|
||||
cx.new(|_cx| ContextStore::new(project.clone(), thread_store.clone()));
|
||||
let codegen = cx.new(|cx| {
|
||||
BufferCodegen::new(
|
||||
editor.read(cx).buffer().clone(),
|
||||
@@ -519,7 +520,7 @@ impl InlineAssistant {
|
||||
initial_prompt: String,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
focus: bool,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
workspace: Entity<Workspace>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -537,8 +538,8 @@ impl InlineAssistant {
|
||||
range.end = range.end.bias_right(&snapshot);
|
||||
}
|
||||
|
||||
let context_store =
|
||||
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
|
||||
let project = workspace.read(cx).project().downgrade();
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
|
||||
|
||||
let codegen = cx.new(|cx| {
|
||||
BufferCodegen::new(
|
||||
@@ -562,7 +563,7 @@ impl InlineAssistant {
|
||||
codegen.clone(),
|
||||
self.fs.clone(),
|
||||
context_store,
|
||||
workspace.clone(),
|
||||
workspace.downgrade(),
|
||||
thread_store,
|
||||
window,
|
||||
cx,
|
||||
@@ -589,7 +590,7 @@ impl InlineAssistant {
|
||||
end_block_id,
|
||||
range,
|
||||
codegen.clone(),
|
||||
workspace.clone(),
|
||||
workspace.downgrade(),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
@@ -1779,6 +1780,7 @@ 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| {
|
||||
|
||||
@@ -66,7 +66,7 @@ impl TerminalInlineAssistant {
|
||||
pub fn assist(
|
||||
&mut self,
|
||||
terminal_view: &Entity<TerminalView>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
workspace: Entity<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 context_store =
|
||||
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
|
||||
let project = workspace.read(cx).project().downgrade();
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project, 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.clone(),
|
||||
workspace.downgrade(),
|
||||
thread_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
@@ -106,7 +106,7 @@ impl TerminalInlineAssistant {
|
||||
assist_id,
|
||||
terminal_view,
|
||||
prompt_editor,
|
||||
workspace.clone(),
|
||||
workspace.downgrade(),
|
||||
context_store,
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -3,6 +3,7 @@ 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};
|
||||
@@ -21,13 +22,11 @@ use language_model::{
|
||||
};
|
||||
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
|
||||
use project::{Project, Worktree};
|
||||
use prompt_store::{
|
||||
AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
|
||||
};
|
||||
use prompt_store::{AssistantSystemPromptContext, PromptBuilder, WorktreeInfoForSystemPrompt};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use util::{ResultExt as _, TryFutureExt as _, maybe, post_inc};
|
||||
use util::{ResultExt as _, TryFutureExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::{AssistantContext, ContextId, format_context_as_string};
|
||||
@@ -854,67 +853,36 @@ impl Thread {
|
||||
let root_name = worktree.root_name().into();
|
||||
let abs_path = worktree.abs_path();
|
||||
|
||||
// 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((
|
||||
let rules_task = load_worktree_rules_file(fs, worktree, cx);
|
||||
let Some(rules_task) = rules_task else {
|
||||
return 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(
|
||||
@@ -2266,7 +2234,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(workspace.downgrade(), None));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
(workspace, thread_store, thread, context_store)
|
||||
}
|
||||
|
||||
@@ -431,17 +431,6 @@ 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)
|
||||
@@ -452,12 +441,7 @@ impl RenderOnce for PastThread {
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Thread",
|
||||
&RemoveSelectedThread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
||||
})
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
@@ -538,17 +522,6 @@ 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)
|
||||
@@ -559,12 +532,7 @@ impl RenderOnce for PastContext {
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Prompt Editor",
|
||||
&RemoveSelectedThread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
||||
})
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
|
||||
@@ -334,6 +334,8 @@ impl ToolUseState {
|
||||
output: Result<String>,
|
||||
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);
|
||||
|
||||
25
crates/agent_rules/Cargo.toml
Normal file
25
crates/agent_rules/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[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
|
||||
1
crates/agent_rules/LICENSE-GPL
Symbolic link
1
crates/agent_rules/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
51
crates/agent_rules/src/agent_rules.rs
Normal file
51
crates/agent_rules/src/agent_rules.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
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(),
|
||||
})
|
||||
}))
|
||||
})
|
||||
}
|
||||
@@ -2793,7 +2793,7 @@ fn render_thought_process_fold_icon_button(
|
||||
let button = match status {
|
||||
ThoughtProcessStatus::Pending => button
|
||||
.child(
|
||||
Icon::new(IconName::Brain)
|
||||
Icon::new(IconName::LightBulb)
|
||||
.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::Brain).size(IconSize::Small))
|
||||
.child(Icon::new(IconName::LightBulb).size(IconSize::Small))
|
||||
.child(Label::new("Thought Process").single_line()),
|
||||
};
|
||||
|
||||
|
||||
@@ -156,7 +156,21 @@ impl Tool for BashTool {
|
||||
|
||||
// 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?;
|
||||
let mut bytes_read = 0;
|
||||
|
||||
// Read until we reach the limit
|
||||
loop {
|
||||
let read = reader.read(&mut buffer).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 {
|
||||
|
||||
@@ -7,3 +7,5 @@ You should use this tool when you want to edit a subset of a file's contents, bu
|
||||
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.
|
||||
|
||||
@@ -33,7 +33,7 @@ impl Tool for ThinkingTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Brain
|
||||
IconName::LightBulb
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
|
||||
@@ -1262,6 +1262,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -4271,6 +4272,7 @@ impl Editor {
|
||||
buffer
|
||||
.update(cx, |buffer, _| {
|
||||
buffer.push_transaction(transaction, Instant::now());
|
||||
buffer.finalize_last_transaction();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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 {
|
||||
@@ -50,6 +51,24 @@ 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,
|
||||
{
|
||||
@@ -57,7 +76,7 @@ pub trait AnimationExt {
|
||||
id: id.into(),
|
||||
element: Some(self),
|
||||
animator: Box::new(animator),
|
||||
animation,
|
||||
animations: animations.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,8 +87,8 @@ impl<E> AnimationExt for E {}
|
||||
pub struct AnimationElement<E> {
|
||||
id: ElementId,
|
||||
element: Option<E>,
|
||||
animation: Animation,
|
||||
animator: Box<dyn Fn(E, f32) -> E + 'static>,
|
||||
animations: SmallVec<[Animation; 1]>,
|
||||
animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
|
||||
}
|
||||
|
||||
impl<E> AnimationElement<E> {
|
||||
@@ -91,6 +110,7 @@ impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
|
||||
|
||||
struct AnimationState {
|
||||
start: Instant,
|
||||
animation_ix: usize,
|
||||
}
|
||||
|
||||
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
|
||||
@@ -108,22 +128,30 @@ 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 state = state.unwrap_or_else(|| AnimationState {
|
||||
let mut state = state.unwrap_or_else(|| AnimationState {
|
||||
start: Instant::now(),
|
||||
animation_ix: 0,
|
||||
});
|
||||
let mut delta =
|
||||
state.start.elapsed().as_secs_f32() / self.animation.duration.as_secs_f32();
|
||||
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 done = false;
|
||||
if delta > 1.0 {
|
||||
if self.animation.oneshot {
|
||||
done = true;
|
||||
if self.animations[animation_ix].oneshot {
|
||||
if animation_ix >= self.animations.len() - 1 {
|
||||
done = true;
|
||||
} else {
|
||||
state.start = Instant::now();
|
||||
state.animation_ix += 1;
|
||||
}
|
||||
delta = 1.0;
|
||||
} else {
|
||||
delta %= 1.0;
|
||||
}
|
||||
}
|
||||
let delta = (self.animation.easing)(delta);
|
||||
let delta = (self.animations[animation_ix].easing)(delta);
|
||||
|
||||
debug_assert!(
|
||||
(0.0..=1.0).contains(&delta),
|
||||
@@ -131,7 +159,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, delta).into_any_element();
|
||||
let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
|
||||
|
||||
if !done {
|
||||
window.request_animation_frame();
|
||||
|
||||
@@ -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 * dash_velocity;
|
||||
let corner_t = radians * corner_radius;
|
||||
|
||||
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;
|
||||
t = upto_r - corner_t * dash_velocity;
|
||||
} 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;
|
||||
t = upto_br + corner_t * dash_velocity;
|
||||
}
|
||||
} 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;
|
||||
t = upto_l - corner_t * dash_velocity;
|
||||
} 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;
|
||||
t = upto_tl + corner_t * dash_velocity;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -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 * dash_velocity;
|
||||
float corner_t = radians * corner_radius;
|
||||
|
||||
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;
|
||||
t = upto_r - corner_t * dash_velocity;
|
||||
} 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;
|
||||
t = upto_br + corner_t * dash_velocity;
|
||||
}
|
||||
} 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;
|
||||
t = upto_l - corner_t * dash_velocity;
|
||||
} 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;
|
||||
t = upto_tl + corner_t * dash_velocity;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -141,6 +141,7 @@ pub enum IconName {
|
||||
InlayHint,
|
||||
Keyboard,
|
||||
Library,
|
||||
LightBulb,
|
||||
LineHeight,
|
||||
Link,
|
||||
ListTree,
|
||||
|
||||
@@ -2015,11 +2015,16 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// Manually remove a transaction from the buffer's undo history
|
||||
pub fn forget_transaction(&mut self, transaction_id: TransactionId) {
|
||||
self.text.forget_transaction(transaction_id);
|
||||
pub fn forget_transaction(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
|
||||
self.text.forget_transaction(transaction_id)
|
||||
}
|
||||
|
||||
/// Manually merge two adjacent transactions in the buffer's undo history.
|
||||
/// 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.
|
||||
pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
|
||||
self.text.merge_transactions(transaction, destination);
|
||||
}
|
||||
|
||||
@@ -98,19 +98,6 @@ 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.
|
||||
@@ -432,12 +419,18 @@ 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, Debug)]
|
||||
#[derive(Clone)]
|
||||
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;
|
||||
|
||||
@@ -551,10 +544,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_smart_punctuation() {
|
||||
assert_eq!(
|
||||
parse_markdown("-- --- ... \"double quoted\" 'single quoted'"),
|
||||
parse_markdown("-- --- ... \"double quoted\" 'single quoted' ----------"),
|
||||
(
|
||||
vec![
|
||||
(0..42, Start(Paragraph)),
|
||||
(0..53, Start(Paragraph)),
|
||||
(0..2, SubstitutedText("–".into())),
|
||||
(2..3, Text),
|
||||
(3..6, SubstitutedText("—".into())),
|
||||
@@ -568,7 +561,9 @@ mod tests {
|
||||
(27..28, SubstitutedText("‘".into())),
|
||||
(28..41, Text),
|
||||
(41..42, SubstitutedText("’".into())),
|
||||
(0..42, End(MarkdownTagEnd::Paragraph))
|
||||
(42..43, Text),
|
||||
(43..53, SubstitutedText("–––––".into())),
|
||||
(0..53, End(MarkdownTagEnd::Paragraph))
|
||||
],
|
||||
HashSet::new(),
|
||||
HashSet::new()
|
||||
|
||||
@@ -275,6 +275,7 @@ impl RemoteBufferStore {
|
||||
if push_to_history {
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.push_transaction(transaction.clone(), Instant::now());
|
||||
buffer.finalize_last_transaction();
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,8 +339,7 @@ impl DapStore {
|
||||
local_store.language_registry.clone(),
|
||||
local_store.toolchain_store.clone(),
|
||||
local_store.environment.update(cx, |env, cx| {
|
||||
let worktree = worktree.read(cx);
|
||||
env.get_environment(worktree.abs_path().into(), cx)
|
||||
env.get_worktree_environment(worktree.clone(), cx)
|
||||
}),
|
||||
);
|
||||
let session_id = local_store.next_session_id();
|
||||
@@ -414,8 +413,7 @@ impl DapStore {
|
||||
local_store.language_registry.clone(),
|
||||
local_store.toolchain_store.clone(),
|
||||
local_store.environment.update(cx, |env, cx| {
|
||||
let worktree = worktree.read(cx);
|
||||
env.get_environment(Some(worktree.abs_path()), cx)
|
||||
env.get_worktree_environment(worktree.clone(), cx)
|
||||
}),
|
||||
);
|
||||
let session_id = local_store.next_session_id();
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
use futures::{
|
||||
FutureExt,
|
||||
future::{Shared, WeakShared},
|
||||
};
|
||||
use futures::{FutureExt, future::Shared};
|
||||
use language::Buffer;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::ResultExt;
|
||||
use worktree::Worktree;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
|
||||
use gpui::{AppContext as _, Context, Entity, EventEmitter, Task};
|
||||
use settings::Settings as _;
|
||||
|
||||
use crate::{
|
||||
project_settings::{DirenvSettings, ProjectSettings},
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
|
||||
pub struct ProjectEnvironment {
|
||||
cli_environment: Option<HashMap<String, String>>,
|
||||
environments: HashMap<Arc<Path>, WeakShared<Task<Option<HashMap<String, String>>>>>,
|
||||
environments: HashMap<Arc<Path>, Shared<Task<Option<HashMap<String, String>>>>>,
|
||||
environment_error_messages: HashMap<Arc<Path>, EnvironmentErrorMessage>,
|
||||
}
|
||||
|
||||
@@ -27,27 +26,12 @@ pub enum ProjectEnvironmentEvent {
|
||||
impl EventEmitter<ProjectEnvironmentEvent> for ProjectEnvironment {}
|
||||
|
||||
impl ProjectEnvironment {
|
||||
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(),
|
||||
}
|
||||
})
|
||||
pub fn new(cli_environment: Option<HashMap<String, String>>) -> Self {
|
||||
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.
|
||||
@@ -73,54 +57,85 @@ impl ProjectEnvironment {
|
||||
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated);
|
||||
}
|
||||
|
||||
/// 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_environment(
|
||||
pub(crate) fn get_buffer_environment(
|
||||
&mut self,
|
||||
abs_path: Option<Arc<Path>>,
|
||||
cx: &Context<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() {
|
||||
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();
|
||||
log::info!("using project environment variables from CLI");
|
||||
return Task::ready(Some(cli_environment)).shared();
|
||||
}
|
||||
|
||||
let Some(abs_path) = abs_path else {
|
||||
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();
|
||||
};
|
||||
|
||||
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
|
||||
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(
|
||||
&mut self,
|
||||
abs_path: Arc<Path>,
|
||||
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();
|
||||
}
|
||||
|
||||
self.environments
|
||||
.entry(abs_path.clone())
|
||||
.or_insert_with(|| get_directory_env_impl(abs_path.clone(), cx).shared())
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,7 +353,7 @@ async fn load_shell_environment(
|
||||
(Some(parsed_env), direnv_error)
|
||||
}
|
||||
|
||||
fn get_directory_env(
|
||||
fn get_directory_env_impl(
|
||||
abs_path: Arc<Path>,
|
||||
cx: &Context<ProjectEnvironment>,
|
||||
) -> Task<Option<HashMap<String, String>>> {
|
||||
@@ -368,10 +383,7 @@ fn get_directory_env(
|
||||
|
||||
if let Some(error) = error_message {
|
||||
this.update(cx, |this, cx| {
|
||||
log::error!(
|
||||
"error fetching environment for path {abs_path:?}: {}",
|
||||
error
|
||||
);
|
||||
log::error!("{error}",);
|
||||
this.environment_error_messages.insert(abs_path, error);
|
||||
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated)
|
||||
})
|
||||
|
||||
@@ -3263,7 +3263,7 @@ impl Repository {
|
||||
remote: SharedString,
|
||||
options: Option<PushOptions>,
|
||||
askpass: AskPassDelegate,
|
||||
_cx: &mut App,
|
||||
cx: &mut Context<Self>,
|
||||
) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
|
||||
let askpass_delegates = self.askpass_delegates.clone();
|
||||
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
||||
@@ -3276,25 +3276,52 @@ 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, cx| async move {
|
||||
Some(format!("git push {} {} {}", args, branch, remote).into()),
|
||||
move |git_repo, mut cx| async move {
|
||||
match git_repo {
|
||||
RepositoryState::Local {
|
||||
backend,
|
||||
environment,
|
||||
..
|
||||
} => {
|
||||
backend
|
||||
let result = backend
|
||||
.push(
|
||||
branch.to_string(),
|
||||
remote.to_string(),
|
||||
options,
|
||||
askpass,
|
||||
environment.clone(),
|
||||
cx,
|
||||
cx.clone(),
|
||||
)
|
||||
.await
|
||||
.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
|
||||
}
|
||||
RepositoryState::Remote { project_id, client } => {
|
||||
askpass_delegates.lock().insert(askpass_id, askpass);
|
||||
@@ -3749,7 +3776,7 @@ impl Repository {
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("missing project environment"))?
|
||||
.update(cx, |project_environment, cx| {
|
||||
project_environment.get_environment(Some(work_directory_abs_path.clone()), cx)
|
||||
project_environment.get_directory_environment(work_directory_abs_path.clone(), cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or_else(|| {
|
||||
|
||||
@@ -86,7 +86,8 @@ use text::{Anchor, BufferId, LineEnding, OffsetRangeExt};
|
||||
use url::Url;
|
||||
use util::{
|
||||
ResultExt, TryFutureExt as _, debug_panic, defer, maybe, merge_json_value_into,
|
||||
paths::SanitizedPath, post_inc,
|
||||
paths::{PathExt, SanitizedPath},
|
||||
post_inc,
|
||||
};
|
||||
|
||||
pub use fs::*;
|
||||
@@ -1220,8 +1221,61 @@ impl LocalLspStore {
|
||||
buffer.finalize_last_transaction();
|
||||
})?;
|
||||
|
||||
// handle whitespace formatting
|
||||
// helper function to avoid duplicate logic between formatter handlers below
|
||||
// We want to avoid continuing to format the buffer if it has been edited since the start
|
||||
// so we check that the last transaction id on the undo stack matches the one we expect
|
||||
// This check should be done after each "gather" step where we generate a diff or edits to apply,
|
||||
// and before applying them to the buffer to avoid messing up the user's buffer
|
||||
fn err_if_buffer_edited_since_start(
|
||||
buffer: &FormattableBuffer,
|
||||
transaction_id_format: Option<text::TransactionId>,
|
||||
cx: &AsyncApp,
|
||||
) -> Option<anyhow::Error> {
|
||||
let transaction_id_last = buffer
|
||||
.handle
|
||||
.read_with(cx, |buffer, _| {
|
||||
buffer.peek_undo_stack().map(|t| t.transaction_id())
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let should_continue_formatting = match (transaction_id_format, transaction_id_last)
|
||||
{
|
||||
(Some(format), Some(last)) => format == last,
|
||||
(Some(_), None) => false,
|
||||
(_, _) => true,
|
||||
};
|
||||
if !should_continue_formatting {
|
||||
return Some(anyhow::anyhow!("Buffer edited while formatting. Aborting"));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// variable used to track errors that occur during the formatting process below,
|
||||
// but that need to not be returned right away (with `?` for example) because we
|
||||
// still need to clean up the transaction history and update the project transaction
|
||||
// at the very end
|
||||
let mut result = anyhow::Ok(());
|
||||
|
||||
// see handling of code action formatting for why there might already be transactions
|
||||
// in project_transaction for this buffer
|
||||
if let Some(transaction_existing) = project_transaction.0.remove(&buffer.handle) {
|
||||
transaction_id_format = Some(transaction_existing.id);
|
||||
buffer.handle.update(cx, |buffer, _| {
|
||||
// ensure the transaction is in the history so we can group with it
|
||||
if buffer.get_transaction(transaction_existing.id).is_none() {
|
||||
buffer.push_transaction(transaction_existing, Instant::now());
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Some(err) = err_if_buffer_edited_since_start(buffer, transaction_id_format, &cx)
|
||||
{
|
||||
zlog::warn!(logger => "Buffer edited while formatting. Aborting");
|
||||
result = Err(err);
|
||||
}
|
||||
|
||||
// handle whitespace formatting
|
||||
if result.is_ok() {
|
||||
if settings.remove_trailing_whitespace_on_save {
|
||||
zlog::trace!(logger => "removing trailing whitespace");
|
||||
let diff = buffer
|
||||
@@ -1297,41 +1351,12 @@ impl LocalLspStore {
|
||||
|
||||
let formatters = code_actions_on_format_formatter.iter().chain(formatters);
|
||||
|
||||
// helper function to avoid duplicate logic between formatter handlers below
|
||||
// We want to avoid continuing to format the buffer if it has been edited since the start
|
||||
// so we check that the last transaction id on the undo stack matches the one we expect
|
||||
// This check should be done after each "gather" step where we generate a diff or edits to apply,
|
||||
// and before applying them to the buffer to avoid messing up the user's buffer
|
||||
fn err_if_buffer_edited_since_start(
|
||||
buffer: &FormattableBuffer,
|
||||
transaction_id_format: Option<text::TransactionId>,
|
||||
cx: &AsyncApp,
|
||||
) -> Option<anyhow::Error> {
|
||||
let transaction_id_last = buffer
|
||||
.handle
|
||||
.read_with(cx, |buffer, _| {
|
||||
buffer.peek_undo_stack().map(|t| t.transaction_id())
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let should_continue_formatting = match (transaction_id_format, transaction_id_last)
|
||||
{
|
||||
(Some(format), Some(last)) => format == last,
|
||||
(Some(_), None) => false,
|
||||
(_, _) => true,
|
||||
};
|
||||
if !should_continue_formatting {
|
||||
return Some(anyhow::anyhow!("Buffer edited while formatting. Aborting"));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// variable used to track errors that occur during the formatting process below,
|
||||
// but that need to not be returned right away (with `?` for example) because we
|
||||
// still need to clean up the transaction history and update the project transaction
|
||||
let mut result = anyhow::Ok(());
|
||||
|
||||
'formatters: for formatter in formatters {
|
||||
if result.is_err() {
|
||||
// may have been set above, instead of indenting this whole block with an if, just don't do anything
|
||||
// destructive if we're aborting
|
||||
continue;
|
||||
}
|
||||
match formatter {
|
||||
Formatter::Prettier => {
|
||||
let logger = zlog::scoped!(logger => "prettier");
|
||||
@@ -1583,32 +1608,19 @@ impl LocalLspStore {
|
||||
|
||||
let describe_code_action = |action: &CodeAction| {
|
||||
format!(
|
||||
"code action '{}' with title \"{}\"",
|
||||
"code action '{}' with title \"{}\" on server {}",
|
||||
action
|
||||
.lsp_action
|
||||
.action_kind()
|
||||
.unwrap_or("unknown".into())
|
||||
.as_str(),
|
||||
action.lsp_action.title()
|
||||
action.lsp_action.title(),
|
||||
server.name(),
|
||||
)
|
||||
};
|
||||
|
||||
zlog::trace!(logger => "Executing {}", describe_code_action(&action));
|
||||
|
||||
// NOTE: code below duplicated from `Self::deserialize_workspace_edit`
|
||||
// but filters out and logs warnings for code actions that cause unreasonably
|
||||
// difficult handling on our part, such as:
|
||||
// - applying edits that call commands
|
||||
// which can result in arbitrary workspace edits being sent from the server that
|
||||
// have no way of being tied back to the command that initiated them (i.e. we
|
||||
// can't know which edits are part of the format request, or if the server is done sending
|
||||
// actions in response to the command)
|
||||
// - actions that create/delete/modify/rename files other than the one we are formatting
|
||||
// as we then would need to handle such changes correctly in the local history as well
|
||||
// as the remote history through the ProjectTransaction
|
||||
// - actions with snippet edits, as these simply don't make sense in the context of a format request
|
||||
// Supporting these actions is not impossible, but not supported as of yet.
|
||||
|
||||
if let Err(err) =
|
||||
Self::try_resolve_code_action(server, &mut action).await
|
||||
{
|
||||
@@ -1620,163 +1632,339 @@ impl LocalLspStore {
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
if let Some(_) = action.lsp_action.command() {
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"Code actions with commands are not supported while formatting. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
let Some(edit) = action.lsp_action.edit().cloned() else {
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"No edit found for while formatting. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
};
|
||||
|
||||
if edit.changes.is_none() && edit.document_changes.is_none() {
|
||||
zlog::trace!(
|
||||
logger =>
|
||||
"No changes for code action. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
|
||||
let mut operations = Vec::new();
|
||||
if let Some(document_changes) = edit.document_changes {
|
||||
match document_changes {
|
||||
lsp::DocumentChanges::Edits(edits) => operations.extend(
|
||||
edits.into_iter().map(lsp::DocumentChangeOperation::Edit),
|
||||
),
|
||||
lsp::DocumentChanges::Operations(ops) => operations = ops,
|
||||
if let Some(edit) = action.lsp_action.edit().cloned() {
|
||||
// NOTE: code below duplicated from `Self::deserialize_workspace_edit`
|
||||
// but filters out and logs warnings for code actions that cause unreasonably
|
||||
// difficult handling on our part, such as:
|
||||
// - applying edits that call commands
|
||||
// which can result in arbitrary workspace edits being sent from the server that
|
||||
// have no way of being tied back to the command that initiated them (i.e. we
|
||||
// can't know which edits are part of the format request, or if the server is done sending
|
||||
// actions in response to the command)
|
||||
// - actions that create/delete/modify/rename files other than the one we are formatting
|
||||
// as we then would need to handle such changes correctly in the local history as well
|
||||
// as the remote history through the ProjectTransaction
|
||||
// - actions with snippet edits, as these simply don't make sense in the context of a format request
|
||||
// Supporting these actions is not impossible, but not supported as of yet.
|
||||
if edit.changes.is_none() && edit.document_changes.is_none() {
|
||||
zlog::trace!(
|
||||
logger =>
|
||||
"No changes for code action. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
} else if let Some(changes) = edit.changes {
|
||||
operations.extend(changes.into_iter().map(|(uri, edits)| {
|
||||
lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit {
|
||||
text_document:
|
||||
lsp::OptionalVersionedTextDocumentIdentifier {
|
||||
uri,
|
||||
version: None,
|
||||
},
|
||||
edits: edits.into_iter().map(Edit::Plain).collect(),
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
let mut edits = Vec::with_capacity(operations.len());
|
||||
|
||||
if operations.is_empty() {
|
||||
zlog::trace!(
|
||||
logger =>
|
||||
"No changes for code action. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
for operation in operations {
|
||||
let op = match operation {
|
||||
lsp::DocumentChangeOperation::Edit(op) => op,
|
||||
lsp::DocumentChangeOperation::Op(_) => {
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"Code actions which create, delete, or rename files are not supported on format. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
let mut operations = Vec::new();
|
||||
if let Some(document_changes) = edit.document_changes {
|
||||
match document_changes {
|
||||
lsp::DocumentChanges::Edits(edits) => operations.extend(
|
||||
edits
|
||||
.into_iter()
|
||||
.map(lsp::DocumentChangeOperation::Edit),
|
||||
),
|
||||
lsp::DocumentChanges::Operations(ops) => operations = ops,
|
||||
}
|
||||
};
|
||||
let Ok(file_path) = op.text_document.uri.to_file_path() else {
|
||||
zlog::warn!(
|
||||
} else if let Some(changes) = edit.changes {
|
||||
operations.extend(changes.into_iter().map(|(uri, edits)| {
|
||||
lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit {
|
||||
text_document:
|
||||
lsp::OptionalVersionedTextDocumentIdentifier {
|
||||
uri,
|
||||
version: None,
|
||||
},
|
||||
edits: edits.into_iter().map(Edit::Plain).collect(),
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
let mut edits = Vec::with_capacity(operations.len());
|
||||
|
||||
if operations.is_empty() {
|
||||
zlog::trace!(
|
||||
logger =>
|
||||
"Failed to convert URI '{:?}' to file path. Skipping {}",
|
||||
&op.text_document.uri,
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
};
|
||||
if &file_path != buffer_path_abs {
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"File path '{:?}' does not match buffer path '{:?}'. Skipping {}",
|
||||
file_path,
|
||||
buffer_path_abs,
|
||||
"No changes for code action. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
|
||||
let mut lsp_edits = Vec::new();
|
||||
for edit in op.edits {
|
||||
match edit {
|
||||
Edit::Plain(edit) => {
|
||||
if !lsp_edits.contains(&edit) {
|
||||
lsp_edits.push(edit);
|
||||
}
|
||||
}
|
||||
Edit::Annotated(edit) => {
|
||||
if !lsp_edits.contains(&edit.text_edit) {
|
||||
lsp_edits.push(edit.text_edit);
|
||||
}
|
||||
}
|
||||
Edit::Snippet(_) => {
|
||||
for operation in operations {
|
||||
let op = match operation {
|
||||
lsp::DocumentChangeOperation::Edit(op) => op,
|
||||
lsp::DocumentChangeOperation::Op(_) => {
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"Code actions which produce snippet edits are not supported during formatting. Skipping {}",
|
||||
"Code actions which create, delete, or rename files are not supported on format. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
};
|
||||
let Ok(file_path) = op.text_document.uri.to_file_path() else {
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"Failed to convert URI '{:?}' to file path. Skipping {}",
|
||||
&op.text_document.uri,
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
};
|
||||
if &file_path != buffer_path_abs {
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"File path '{:?}' does not match buffer path '{:?}'. Skipping {}",
|
||||
file_path,
|
||||
buffer_path_abs,
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
|
||||
let mut lsp_edits = Vec::new();
|
||||
for edit in op.edits {
|
||||
match edit {
|
||||
Edit::Plain(edit) => {
|
||||
if !lsp_edits.contains(&edit) {
|
||||
lsp_edits.push(edit);
|
||||
}
|
||||
}
|
||||
Edit::Annotated(edit) => {
|
||||
if !lsp_edits.contains(&edit.text_edit) {
|
||||
lsp_edits.push(edit.text_edit);
|
||||
}
|
||||
}
|
||||
Edit::Snippet(_) => {
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"Code actions which produce snippet edits are not supported during formatting. Skipping {}",
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
}
|
||||
}
|
||||
let edits_result = lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
lsp_store.as_local_mut().unwrap().edits_from_lsp(
|
||||
&buffer.handle,
|
||||
lsp_edits,
|
||||
server.server_id(),
|
||||
op.text_document.version,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
let Ok(resolved_edits) = edits_result else {
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"Failed to resolve edits from LSP for buffer {:?} while handling {}",
|
||||
buffer_path_abs.as_path(),
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
};
|
||||
edits.extend(resolved_edits);
|
||||
}
|
||||
|
||||
if edits.is_empty() {
|
||||
zlog::warn!(logger => "No edits resolved from LSP");
|
||||
continue 'actions;
|
||||
}
|
||||
|
||||
if let Some(err) = err_if_buffer_edited_since_start(
|
||||
buffer,
|
||||
transaction_id_format,
|
||||
&cx,
|
||||
) {
|
||||
zlog::warn!(logger => "Buffer edited while formatting. Aborting");
|
||||
result = Err(err);
|
||||
break 'formatters;
|
||||
}
|
||||
zlog::info!(logger => "Applying changes");
|
||||
buffer.handle.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction();
|
||||
buffer.edit(edits, None, cx);
|
||||
transaction_id_format =
|
||||
transaction_id_format.or(buffer.end_transaction(cx));
|
||||
if let Some(transaction_id) = transaction_id_format {
|
||||
buffer.group_until_transaction(transaction_id);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
if let Some(command) = action.lsp_action.command() {
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"Executing code action command '{}'. This may cause formatting to abort unnecessarily as well as splitting formatting into two entries in the undo history",
|
||||
&command.command,
|
||||
);
|
||||
// bail early and command is invalid
|
||||
{
|
||||
let server_capabilities = server.capabilities();
|
||||
let available_commands = server_capabilities
|
||||
.execute_command_provider
|
||||
.as_ref()
|
||||
.map(|options| options.commands.as_slice())
|
||||
.unwrap_or_default();
|
||||
if !available_commands.contains(&command.command) {
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"Cannot execute a command {} not listed in the language server capabilities of server {}",
|
||||
command.command,
|
||||
server.name(),
|
||||
);
|
||||
continue 'actions;
|
||||
}
|
||||
}
|
||||
let edits_result = lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
lsp_store.as_local_mut().unwrap().edits_from_lsp(
|
||||
&buffer.handle,
|
||||
lsp_edits,
|
||||
server.server_id(),
|
||||
op.text_document.version,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
|
||||
if let Some(err) = err_if_buffer_edited_since_start(
|
||||
buffer,
|
||||
transaction_id_format,
|
||||
&cx,
|
||||
) {
|
||||
zlog::warn!(logger => "Buffer edited while formatting. Aborting");
|
||||
result = Err(err);
|
||||
break 'formatters;
|
||||
}
|
||||
zlog::info!(logger => "Executing command {}", &command.command);
|
||||
|
||||
lsp_store.update(cx, |this, _| {
|
||||
this.as_local_mut()
|
||||
.unwrap()
|
||||
.last_workspace_edits_by_language_server
|
||||
.remove(&server.server_id());
|
||||
})?;
|
||||
|
||||
let execute_command_result = server
|
||||
.request::<lsp::request::ExecuteCommand>(
|
||||
lsp::ExecuteCommandParams {
|
||||
command: command.command.clone(),
|
||||
arguments: command
|
||||
.arguments
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let Ok(resolved_edits) = edits_result else {
|
||||
zlog::warn!(
|
||||
|
||||
if execute_command_result.is_err() {
|
||||
zlog::error!(
|
||||
logger =>
|
||||
"Failed to resolve edits from LSP for buffer {:?} while handling {}",
|
||||
buffer_path_abs.as_path(),
|
||||
"Failed to execute command '{}' as part of {}",
|
||||
&command.command,
|
||||
describe_code_action(&action),
|
||||
);
|
||||
continue 'actions;
|
||||
};
|
||||
edits.extend(resolved_edits);
|
||||
}
|
||||
|
||||
if edits.is_empty() {
|
||||
zlog::warn!(logger => "No edits resolved from LSP");
|
||||
continue 'actions;
|
||||
}
|
||||
|
||||
if let Some(err) =
|
||||
err_if_buffer_edited_since_start(buffer, transaction_id_format, &cx)
|
||||
{
|
||||
zlog::warn!(logger => "Buffer edited while formatting. Aborting");
|
||||
result = Err(err);
|
||||
break 'formatters;
|
||||
}
|
||||
zlog::info!(logger => "Applying changes");
|
||||
buffer.handle.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction();
|
||||
buffer.edit(edits, None, cx);
|
||||
transaction_id_format =
|
||||
transaction_id_format.or(buffer.end_transaction(cx));
|
||||
if let Some(transaction_id) = transaction_id_format {
|
||||
buffer.group_until_transaction(transaction_id);
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut project_transaction_command =
|
||||
lsp_store.update(cx, |this, _| {
|
||||
this.as_local_mut()
|
||||
.unwrap()
|
||||
.last_workspace_edits_by_language_server
|
||||
.remove(&server.server_id())
|
||||
.unwrap_or_default()
|
||||
})?;
|
||||
|
||||
if let Some(transaction) =
|
||||
project_transaction_command.0.remove(&buffer.handle)
|
||||
{
|
||||
zlog::trace!(
|
||||
logger =>
|
||||
"Successfully captured {} edits that resulted from command {}",
|
||||
transaction.edit_ids.len(),
|
||||
&command.command,
|
||||
);
|
||||
if let Some(transaction_id_format) = transaction_id_format {
|
||||
let transaction_id_project_transaction = transaction.id;
|
||||
buffer.handle.update(cx, |buffer, _| {
|
||||
// it may have been removed from history if push_to_history was
|
||||
// false in deserialize_workspace_edit. If so push it so we
|
||||
// can merge it with the format transaction
|
||||
// and pop the combined transaction off the history stack
|
||||
// later if push_to_history is false
|
||||
if buffer.get_transaction(transaction.id).is_none() {
|
||||
buffer
|
||||
.push_transaction(transaction, Instant::now());
|
||||
}
|
||||
buffer.merge_transactions(
|
||||
transaction_id_project_transaction,
|
||||
transaction_id_format,
|
||||
);
|
||||
})?;
|
||||
} else {
|
||||
transaction_id_format = Some(transaction.id);
|
||||
}
|
||||
}
|
||||
if !project_transaction_command.0.is_empty() {
|
||||
let extra_buffers = project_transaction_command
|
||||
.0
|
||||
.keys()
|
||||
.filter_map(|buffer_handle| {
|
||||
buffer_handle
|
||||
.read_with(cx, |b, cx| b.project_path(cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.map(|p| p.path.to_sanitized_string())
|
||||
.join(", ");
|
||||
zlog::warn!(
|
||||
logger =>
|
||||
"Unexpected edits to buffers other than the buffer actively being formatted due to command {}. Impacted buffers: [{}].",
|
||||
&command.command,
|
||||
extra_buffers,
|
||||
);
|
||||
for (buffer_handle, transaction) in
|
||||
project_transaction_command.0
|
||||
{
|
||||
let entry = project_transaction.0.entry(buffer_handle);
|
||||
use std::collections::hash_map::Entry;
|
||||
match entry {
|
||||
// if project_transaction already contains a transaction for this buffer, then
|
||||
// we already formatted it, and we need to merge the new transaction into the one
|
||||
// in project_transaction that we created while formatting
|
||||
Entry::Occupied(mut occupied_entry) => {
|
||||
let buffer_handle = occupied_entry.key();
|
||||
buffer_handle.update(cx, |buffer, _| {
|
||||
// if push_to_history is true, then we need to make sure it is merged in the
|
||||
// buffer history as well
|
||||
if push_to_history {
|
||||
let transaction_id_project_transaction =
|
||||
transaction.id;
|
||||
// ensure transaction from command project transaction
|
||||
// is in history so we can merge
|
||||
if buffer
|
||||
.get_transaction(transaction.id)
|
||||
.is_none()
|
||||
{
|
||||
buffer.push_transaction(
|
||||
transaction.clone(),
|
||||
Instant::now(),
|
||||
);
|
||||
}
|
||||
let transaction_id_existing =
|
||||
occupied_entry.get().id;
|
||||
buffer.merge_transactions(
|
||||
transaction_id_project_transaction,
|
||||
transaction_id_existing,
|
||||
);
|
||||
}
|
||||
})?;
|
||||
let transaction_existing = occupied_entry.get_mut();
|
||||
transaction_existing.merge_in(transaction);
|
||||
}
|
||||
// if there is no transaction in project_transaction, we either haven't formatted the buffer yet,
|
||||
// or we won't format this buffer
|
||||
// later iterations over the formattable_buffers list will use this as the starting transaction
|
||||
// for the formatting on that buffer
|
||||
Entry::Vacant(vacant_entry) => {
|
||||
vacant_entry.insert(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2898,20 +3086,35 @@ impl LocalLspStore {
|
||||
.await?;
|
||||
|
||||
let transaction = buffer_to_edit.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
let is_formatting = this.read_with(cx, |lsp_store, _| {
|
||||
lsp_store
|
||||
.as_local()
|
||||
.map_or(false, |local| !local.buffers_being_formatted.is_empty())
|
||||
});
|
||||
// finalizing last transaction breaks workspace edits received due to
|
||||
// code actions that are run as part of formatting because formatting
|
||||
// groups transactions from format steps together to allow undoing all formatting with
|
||||
// a single undo, and to bail when the user modifies the buffer during formatting
|
||||
// finalizing the transaction prevents the necessary grouping from happening
|
||||
if !is_formatting {
|
||||
buffer.finalize_last_transaction();
|
||||
}
|
||||
buffer.start_transaction();
|
||||
for (range, text) in edits {
|
||||
buffer.edit([(range, text)], None, cx);
|
||||
}
|
||||
let transaction = if buffer.end_transaction(cx).is_some() {
|
||||
let transaction = buffer.finalize_last_transaction().unwrap().clone();
|
||||
if !push_to_history {
|
||||
buffer.forget_transaction(transaction.id);
|
||||
|
||||
let transaction = buffer.end_transaction(cx).and_then(|transaction_id| {
|
||||
if push_to_history {
|
||||
if !is_formatting {
|
||||
// see comment above about finalizing during formatting
|
||||
buffer.finalize_last_transaction();
|
||||
}
|
||||
buffer.get_transaction(transaction_id).cloned()
|
||||
} else {
|
||||
buffer.forget_transaction(transaction_id)
|
||||
}
|
||||
Some(transaction)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
});
|
||||
|
||||
transaction
|
||||
})?;
|
||||
@@ -5451,6 +5654,7 @@ impl LspStore {
|
||||
if push_to_history {
|
||||
buffer_handle.update(cx, |buffer, _| {
|
||||
buffer.push_transaction(transaction.clone(), Instant::now());
|
||||
buffer.finalize_last_transaction();
|
||||
})?;
|
||||
}
|
||||
Ok(Some(transaction))
|
||||
@@ -8087,16 +8291,10 @@ impl LspStore {
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Shared<Task<Option<HashMap<String, String>>>> {
|
||||
let worktree_id = buffer.read(cx).file().map(|file| file.worktree_id(cx));
|
||||
let worktree_abs_path = worktree_id.and_then(|worktree_id| {
|
||||
self.worktree_store
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.map(|entry| entry.read(cx).abs_path().clone())
|
||||
});
|
||||
|
||||
if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) {
|
||||
environment.update(cx, |env, cx| env.get_environment(worktree_abs_path, cx))
|
||||
environment.update(cx, |env, cx| {
|
||||
env.get_buffer_environment(buffer.clone(), self.worktree_store.clone(), cx)
|
||||
})
|
||||
} else {
|
||||
Task::ready(None).shared()
|
||||
}
|
||||
@@ -9930,10 +10128,8 @@ impl LocalLspAdapterDelegate {
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) -> Arc<Self> {
|
||||
let worktree_abs_path = worktree.read(cx).abs_path();
|
||||
|
||||
let load_shell_env_task = environment.update(cx, |env, cx| {
|
||||
env.get_environment(Some(worktree_abs_path), cx)
|
||||
env.get_worktree_environment(worktree.clone(), cx)
|
||||
});
|
||||
|
||||
Arc::new(Self {
|
||||
|
||||
@@ -841,7 +841,7 @@ impl Project {
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
|
||||
let environment = ProjectEnvironment::new(&worktree_store, env, cx);
|
||||
let environment = cx.new(|_| ProjectEnvironment::new(env));
|
||||
let toolchain_store = cx.new(|cx| {
|
||||
ToolchainStore::local(
|
||||
languages.clone(),
|
||||
@@ -1043,7 +1043,7 @@ impl Project {
|
||||
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
|
||||
.detach();
|
||||
|
||||
let environment = ProjectEnvironment::new(&worktree_store, None, cx);
|
||||
let environment = cx.new(|_| ProjectEnvironment::new(None));
|
||||
|
||||
let lsp_store = cx.new(|cx| {
|
||||
LspStore::new_remote(
|
||||
@@ -1242,7 +1242,7 @@ impl Project {
|
||||
ImageStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
|
||||
})?;
|
||||
|
||||
let environment = cx.update(|cx| ProjectEnvironment::new(&worktree_store, None, cx))?;
|
||||
let environment = cx.new(|_| ProjectEnvironment::new(None))?;
|
||||
|
||||
let breakpoint_store =
|
||||
cx.new(|_| BreakpointStore::remote(remote_id, client.clone().into()))?;
|
||||
|
||||
@@ -295,10 +295,13 @@ fn local_task_context_for_location(
|
||||
.and_then(|worktree| worktree.read(cx).root_dir());
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let worktree_abs_path = worktree_abs_path.clone();
|
||||
let project_env = environment
|
||||
.update(cx, |environment, cx| {
|
||||
environment.get_environment(worktree_abs_path.clone(), cx)
|
||||
environment.get_buffer_environment(
|
||||
location.buffer.clone(),
|
||||
worktree_store.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()?
|
||||
.await;
|
||||
|
||||
@@ -317,21 +317,14 @@ impl LocalToolchainStore {
|
||||
cx: &App,
|
||||
) -> Task<Option<ToolchainList>> {
|
||||
let registry = self.languages.clone();
|
||||
let Some(root) = self
|
||||
.worktree_store
|
||||
.read(cx)
|
||||
.worktree_for_id(path.worktree_id, cx)
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
else {
|
||||
let Some(abs_path) = self.worktree_store.read(cx).absolutize(&path, cx) else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
|
||||
let abs_path = root.join(path.path);
|
||||
let environment = self.project_environment.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let project_env = environment
|
||||
.update(cx, |environment, cx| {
|
||||
environment.get_environment(Some(root.clone()), cx)
|
||||
environment.get_directory_environment(abs_path.as_path().into(), cx)
|
||||
})
|
||||
.ok()?
|
||||
.await;
|
||||
|
||||
@@ -38,12 +38,12 @@ impl AssistantSystemPromptContext {
|
||||
pub struct WorktreeInfoForSystemPrompt {
|
||||
pub root_name: String,
|
||||
pub abs_path: Arc<Path>,
|
||||
pub rules_file: Option<RulesFile>,
|
||||
pub rules_file: Option<SystemPromptRulesFile>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RulesFile {
|
||||
pub rel_path: Arc<Path>,
|
||||
pub struct SystemPromptRulesFile {
|
||||
pub path_in_worktree: Arc<Path>,
|
||||
pub abs_path: Arc<Path>,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
@@ -1336,7 +1336,7 @@ impl RemoteServerProjects {
|
||||
Modal::new("remote-projects", None)
|
||||
.header(
|
||||
ModalHeader::new()
|
||||
.child(Headline::new("Remote Projects (beta)").size(HeadlineSize::XSmall)),
|
||||
.child(Headline::new("Remote Projects").size(HeadlineSize::XSmall)),
|
||||
)
|
||||
.section(
|
||||
Section::new().padded(false).child(
|
||||
|
||||
@@ -9,7 +9,8 @@ use http_client::HttpClient;
|
||||
use language::{Buffer, BufferEvent, LanguageRegistry, proto::serialize_operation};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::{
|
||||
LspStore, LspStoreEvent, PrettierStore, ProjectPath, ToolchainStore, WorktreeId,
|
||||
LspStore, LspStoreEvent, PrettierStore, ProjectEnvironment, ProjectPath, ToolchainStore,
|
||||
WorktreeId,
|
||||
buffer_store::{BufferStore, BufferStoreEvent},
|
||||
debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore},
|
||||
git_store::GitStore,
|
||||
@@ -85,7 +86,7 @@ impl HeadlessProject {
|
||||
store
|
||||
});
|
||||
|
||||
let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
|
||||
let environment = cx.new(|_| ProjectEnvironment::new(None));
|
||||
|
||||
let toolchain_store = cx.new(|cx| {
|
||||
ToolchainStore::local(
|
||||
|
||||
@@ -12,6 +12,7 @@ use workspace::{Toast, Workspace};
|
||||
pub mod buffer_search;
|
||||
pub mod project_search;
|
||||
pub(crate) mod search_bar;
|
||||
pub mod search_status_button;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
menu::init();
|
||||
|
||||
47
crates/search/src/search_status_button.rs
Normal file
47
crates/search/src/search_status_button.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use ui::{
|
||||
ButtonCommon, ButtonLike, Clickable, Color, Context, Icon, IconName, IconSize, ParentElement,
|
||||
Render, Styled, Tooltip, Window, h_flex,
|
||||
};
|
||||
use workspace::{ItemHandle, StatusItemView};
|
||||
|
||||
pub struct SearchButton;
|
||||
|
||||
impl SearchButton {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SearchButton {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
h_flex().gap_2().child(
|
||||
ButtonLike::new("project-search-indicator")
|
||||
.child(
|
||||
Icon::new(IconName::MagnifyingGlass)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Default),
|
||||
)
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Project Search",
|
||||
&workspace::DeploySearch::default(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_this, _, window, cx| {
|
||||
window.dispatch_action(Box::new(workspace::DeploySearch::default()), cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for SearchButton {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_active_pane_item: Option<&dyn ItemHandle>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -130,6 +130,12 @@ pub struct Transaction {
|
||||
pub start: clock::Global,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
pub fn merge_in(&mut self, other: Transaction) {
|
||||
self.edit_ids.extend(other.edit_ids);
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryEntry {
|
||||
pub fn transaction_id(&self) -> TransactionId {
|
||||
self.transaction.id
|
||||
@@ -1423,8 +1429,12 @@ impl Buffer {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn forget_transaction(&mut self, transaction_id: TransactionId) {
|
||||
self.history.forget(transaction_id);
|
||||
pub fn forget_transaction(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
|
||||
self.history.forget(transaction_id)
|
||||
}
|
||||
|
||||
pub fn get_transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> {
|
||||
self.history.transaction(transaction_id)
|
||||
}
|
||||
|
||||
pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
|
||||
@@ -1482,7 +1492,6 @@ impl Buffer {
|
||||
|
||||
pub fn push_transaction(&mut self, transaction: Transaction, now: Instant) {
|
||||
self.history.push_transaction(transaction, now);
|
||||
self.history.finalize_last_transaction();
|
||||
}
|
||||
|
||||
pub fn edited_ranges_for_transaction_id<D>(
|
||||
|
||||
@@ -222,6 +222,7 @@ pub fn initialize_workspace(
|
||||
}
|
||||
});
|
||||
|
||||
let search_button = cx.new(|_| search::search_status_button::SearchButton::new());
|
||||
let diagnostic_summary =
|
||||
cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
|
||||
let activity_indicator = activity_indicator::ActivityIndicator::new(
|
||||
@@ -239,6 +240,7 @@ pub fn initialize_workspace(
|
||||
let cursor_position =
|
||||
cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
status_bar.add_left_item(search_button, window, cx);
|
||||
status_bar.add_left_item(diagnostic_summary, window, cx);
|
||||
status_bar.add_left_item(activity_indicator, window, cx);
|
||||
status_bar.add_right_item(inline_completion_button, window, cx);
|
||||
|
||||
@@ -38,7 +38,7 @@ Putting binary assets such as images in the Git repository will bloat the reposi
|
||||
|
||||
The table of contents files (`theme/page-toc.js` and `theme/page-doc.css`) were initially generated by [`mdbook-pagetoc`](https://crates.io/crates/mdbook-pagetoc).
|
||||
|
||||
Since all this preprocessor does does is generate the static assets, we don't need to keep it around once they have been generated.
|
||||
Since all this preprocessor does is generate the static assets, we don't need to keep it around once they have been generated.
|
||||
|
||||
## Referencing Keybindings and Actions
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
Remote Development allows you to code at the speed of thought, even when your codebase is not on your local machine. You use Zed locally so the UI is immediately responsive, but offload heavy computation to the development server so that you can work effectively.
|
||||
|
||||
> **Note:** Remoting is still "beta". We are still refining the reliability and performance.
|
||||
|
||||
## Overview
|
||||
|
||||
Remote development requires two computers, your local machine that runs the Zed UI and the remote server which runs a Zed headless server. The two communicate over SSH, so you will need to be able to SSH from your local machine into the remote server to use this feature.
|
||||
|
||||
Reference in New Issue
Block a user