Compare commits
20 Commits
v0.195.5
...
v0.187.4-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96c3fb7d67 | ||
|
|
af4d39efce | ||
|
|
b2f32c53c9 | ||
|
|
186660ed20 | ||
|
|
6b0d58da9d | ||
|
|
853b70687d | ||
|
|
75b8203566 | ||
|
|
1d0b4df24f | ||
|
|
2c861184fa | ||
|
|
7c0c5bd516 | ||
|
|
8f9b217f79 | ||
|
|
de30643e74 | ||
|
|
235fd06adc | ||
|
|
c408200f9b | ||
|
|
53faf0d78d | ||
|
|
f2050dfe2b | ||
|
|
4cfc49e2d4 | ||
|
|
72426a9608 | ||
|
|
1c638a1309 | ||
|
|
249597a4a8 |
@@ -2,16 +2,14 @@
|
||||
{
|
||||
"label": "Debug Zed (CodeLLDB)",
|
||||
"adapter": "CodeLLDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
"program": "target/debug/zed",
|
||||
"request": "launch"
|
||||
},
|
||||
{
|
||||
"label": "Debug Zed (GDB)",
|
||||
"adapter": "GDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"program": "target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"initialize_args": {
|
||||
"stopAtBeginningOfMainSubprogram": true
|
||||
}
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -18540,7 +18540,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.187.0"
|
||||
version = "0.187.4"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
|
||||
@@ -538,7 +538,6 @@
|
||||
"ctrl-alt-b": "workspace::ToggleRightDock",
|
||||
"ctrl-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-j": "workspace::ToggleBottomDock",
|
||||
"ctrl-w": "workspace::CloseActiveDock",
|
||||
"ctrl-alt-y": "workspace::CloseAllDocks",
|
||||
"shift-find": "pane::DeploySearch",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
|
||||
@@ -608,7 +608,6 @@
|
||||
"cmd-b": "workspace::ToggleLeftDock",
|
||||
"cmd-r": "workspace::ToggleRightDock",
|
||||
"cmd-j": "workspace::ToggleBottomDock",
|
||||
"cmd-w": "workspace::CloseActiveDock",
|
||||
"alt-cmd-y": "workspace::CloseAllDocks",
|
||||
"cmd-shift-f": "pane::DeploySearch",
|
||||
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
|
||||
@@ -383,18 +383,25 @@ fn render_markdown_code_block(
|
||||
)
|
||||
} else {
|
||||
let content = if let Some(parent) = path_range.path.parent() {
|
||||
let file_name = file_name.to_string_lossy().to_string();
|
||||
let path = parent.to_string_lossy().to_string();
|
||||
let path_and_file = format!("{}/{}", path, file_name);
|
||||
|
||||
h_flex()
|
||||
.id(("code-block-header-label", ix))
|
||||
.ml_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(file_name.to_string_lossy().to_string())
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new(parent.to_string_lossy().to_string())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(Label::new(file_name).size(LabelSize::Small))
|
||||
.child(Label::new(path).color(Color::Muted).size(LabelSize::Small))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Jump to File",
|
||||
None,
|
||||
path_and_file.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(path_range.path.to_string_lossy().to_string())
|
||||
@@ -404,7 +411,7 @@ fn render_markdown_code_block(
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(("code-block-header-label", ix))
|
||||
.id(("code-block-header-button", ix))
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.px_1()
|
||||
@@ -412,7 +419,6 @@ 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"))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
@@ -462,10 +468,87 @@ fn render_markdown_code_block(
|
||||
.element_background
|
||||
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
|
||||
|
||||
let control_buttons = h_flex()
|
||||
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.h_full()
|
||||
.bg(codeblock_header_bg)
|
||||
.rounded_tr_md()
|
||||
.px_1()
|
||||
.gap_1()
|
||||
.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();
|
||||
let code_block_range = metadata.content_range.clone();
|
||||
move |_event, _window, cx| {
|
||||
active_thread.update(cx, |this, cx| {
|
||||
this.copied_code_block_ids.insert((message_id, ix));
|
||||
|
||||
let code = parsed_markdown.source()[code_block_range.clone()].to_string();
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(code));
|
||||
|
||||
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();
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.when(can_expand, |header| {
|
||||
header.child(
|
||||
IconButton::new(
|
||||
("expand-collapse-code", ix),
|
||||
if is_expanded {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text(if is_expanded {
|
||||
"Collapse Code"
|
||||
} else {
|
||||
"Expand Code"
|
||||
}))
|
||||
.on_click({
|
||||
let active_thread = active_thread.clone();
|
||||
move |_event, _window, cx| {
|
||||
active_thread.update(cx, |this, cx| {
|
||||
this.toggle_codeblock_expanded(message_id, ix);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
let codeblock_header = h_flex()
|
||||
.py_1()
|
||||
.pl_1p5()
|
||||
.pr_1()
|
||||
.relative()
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
@@ -473,79 +556,7 @@ fn render_markdown_code_block(
|
||||
.bg(codeblock_header_bg)
|
||||
.rounded_t_md()
|
||||
.children(label)
|
||||
.child(
|
||||
h_flex()
|
||||
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
|
||||
.gap_1()
|
||||
.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();
|
||||
let code_block_range = metadata.content_range.clone();
|
||||
move |_event, _window, cx| {
|
||||
active_thread.update(cx, |this, cx| {
|
||||
this.copied_code_block_ids.insert((message_id, ix));
|
||||
|
||||
let code =
|
||||
parsed_markdown.source()[code_block_range.clone()].to_string();
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(code));
|
||||
|
||||
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();
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.when(can_expand, |header| {
|
||||
header.child(
|
||||
IconButton::new(
|
||||
("expand-collapse-code", ix),
|
||||
if is_expanded {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text(if is_expanded {
|
||||
"Collapse Code"
|
||||
} else {
|
||||
"Expand Code"
|
||||
}))
|
||||
.on_click({
|
||||
let active_thread = active_thread.clone();
|
||||
move |_event, _window, cx| {
|
||||
active_thread.update(cx, |this, cx| {
|
||||
this.toggle_codeblock_expanded(message_id, ix);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
);
|
||||
.child(control_buttons);
|
||||
|
||||
v_flex()
|
||||
.group(CODEBLOCK_CONTAINER_GROUP)
|
||||
|
||||
@@ -85,6 +85,7 @@ actions!(
|
||||
KeepAll,
|
||||
Follow,
|
||||
ResetTrialUpsell,
|
||||
ResetTrialEndUpsell,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use markdown::Markdown;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -66,8 +66,8 @@ use crate::ui::AgentOnboardingModal;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
|
||||
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
|
||||
OpenHistory, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker,
|
||||
ToggleNavigationMenu, ToggleOptionsMenu,
|
||||
OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent,
|
||||
ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
|
||||
};
|
||||
|
||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||
@@ -157,7 +157,10 @@ pub fn init(cx: &mut App) {
|
||||
window.refresh();
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
|
||||
set_trial_upsell_dismissed(false, cx);
|
||||
TrialUpsell::set_dismissed(false, cx);
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
|
||||
TrialEndUpsell::set_dismissed(false, cx);
|
||||
});
|
||||
},
|
||||
)
|
||||
@@ -1911,12 +1914,23 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
|
||||
if TrialEndUpsell::dismissed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let plan = self.user_store.read(cx).current_plan();
|
||||
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
|
||||
|
||||
matches!(plan, Some(Plan::Free)) && has_previous_trial
|
||||
}
|
||||
|
||||
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
|
||||
if !matches!(self.active_view, ActiveView::Thread { .. }) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.hide_trial_upsell || dismissed_trial_upsell() {
|
||||
if self.hide_trial_upsell || TrialUpsell::dismissed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1962,125 +1976,115 @@ impl AgentPanel {
|
||||
move |toggle_state, _window, cx| {
|
||||
let toggle_state_bool = toggle_state.selected();
|
||||
|
||||
set_trial_upsell_dismissed(toggle_state_bool, cx);
|
||||
TrialUpsell::set_dismissed(toggle_state_bool, cx);
|
||||
},
|
||||
);
|
||||
|
||||
Some(
|
||||
div().p_2().child(
|
||||
v_flex()
|
||||
let contents = div()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new("Try Zed Pro for free for 14 days - no credit card required.")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"Use your own API keys or enable usage-based billing once you hit the cap.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.elevation_2(cx)
|
||||
.rounded(px(8.))
|
||||
.bg(cx.theme().colors().background.alpha(0.5))
|
||||
.p(px(3.))
|
||||
|
||||
.px_neg_1()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(h_flex().items_center().gap_1().child(checkbox))
|
||||
.child(
|
||||
div()
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.border_1()
|
||||
.rounded(px(5.))
|
||||
.border_color(cx.theme().colors().text.alpha(0.1))
|
||||
.overflow_hidden()
|
||||
.relative()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.px_4()
|
||||
.py_3()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right(px(-1.0))
|
||||
.w(px(441.))
|
||||
.h(px(167.))
|
||||
.child(
|
||||
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1)))
|
||||
)
|
||||
Button::new("dismiss-button", "Not Now")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.on_click({
|
||||
let agent_panel = cx.entity();
|
||||
move |_, _, cx| {
|
||||
agent_panel.update(cx, |this, cx| {
|
||||
this.hide_trial_upsell = true;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(-8.0))
|
||||
.right_0()
|
||||
.w(px(400.))
|
||||
.h(px(92.))
|
||||
.child(
|
||||
Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32)))
|
||||
)
|
||||
)
|
||||
// .child(
|
||||
// div()
|
||||
// .absolute()
|
||||
// .top_0()
|
||||
// .right(px(360.))
|
||||
// .size(px(401.))
|
||||
// .overflow_hidden()
|
||||
// .bg(cx.theme().colors().panel_background)
|
||||
// )
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.w(px(660.))
|
||||
.h(px(401.))
|
||||
.overflow_hidden()
|
||||
.bg(linear_gradient(
|
||||
75.,
|
||||
linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
|
||||
linear_color_stop(cx.theme().colors().panel_background, 0.45),
|
||||
))
|
||||
)
|
||||
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
|
||||
.child(Label::new("Try Zed Pro for free for 14 days - no credit card required.").size(LabelSize::Small))
|
||||
.child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted))
|
||||
Button::new("cta-button", "Start Trial")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Some(self.render_upsell_container(cx, contents))
|
||||
}
|
||||
|
||||
fn render_trial_end_upsell(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
if !self.should_render_trial_end_upsell(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_upsell_container(
|
||||
cx,
|
||||
div()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new("You've been automatically reset to the free plan.")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.px_neg_1()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(div())
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.px_neg_1()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(h_flex().items_center().gap_1().child(checkbox))
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("dismiss-button", "Not Now")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.on_click({
|
||||
let agent_panel = cx.entity();
|
||||
move |_, _, cx| {
|
||||
agent_panel.update(
|
||||
cx,
|
||||
|this, cx| {
|
||||
let hidden =
|
||||
this.hide_trial_upsell;
|
||||
println!("hidden: {}", hidden);
|
||||
this.hide_trial_upsell = true;
|
||||
let new_hidden =
|
||||
this.hide_trial_upsell;
|
||||
println!(
|
||||
"new_hidden: {}",
|
||||
new_hidden
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("cta-button", "Start Trial")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url(&zed_urls::account_url(cx))
|
||||
}),
|
||||
),
|
||||
Button::new("dismiss-button", "Stay on Free")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.on_click({
|
||||
let agent_panel = cx.entity();
|
||||
move |_, _, cx| {
|
||||
agent_panel.update(cx, |_this, cx| {
|
||||
TrialEndUpsell::set_dismissed(true, cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("cta-button", "Upgrade to Zed Pro")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url(&zed_urls::account_url(cx))
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -2088,6 +2092,91 @@ impl AgentPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
|
||||
div().p_2().child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.elevation_2(cx)
|
||||
.rounded(px(8.))
|
||||
.bg(cx.theme().colors().background.alpha(0.5))
|
||||
.p(px(3.))
|
||||
.child(
|
||||
div()
|
||||
.gap_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.border_1()
|
||||
.rounded(px(5.))
|
||||
.border_color(cx.theme().colors().text.alpha(0.1))
|
||||
.overflow_hidden()
|
||||
.relative()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.px_4()
|
||||
.py_3()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right(px(-1.0))
|
||||
.w(px(441.))
|
||||
.h(px(167.))
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::Grid,
|
||||
rems_from_px(441.),
|
||||
rems_from_px(167.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(-8.0))
|
||||
.right_0()
|
||||
.w(px(400.))
|
||||
.h(px(92.))
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AiGrid,
|
||||
rems_from_px(400.),
|
||||
rems_from_px(92.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
|
||||
),
|
||||
)
|
||||
// .child(
|
||||
// div()
|
||||
// .absolute()
|
||||
// .top_0()
|
||||
// .right(px(360.))
|
||||
// .size(px(401.))
|
||||
// .overflow_hidden()
|
||||
// .bg(cx.theme().colors().panel_background)
|
||||
// )
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.w(px(660.))
|
||||
.h(px(401.))
|
||||
.overflow_hidden()
|
||||
.bg(linear_gradient(
|
||||
75.,
|
||||
linear_color_stop(
|
||||
cx.theme().colors().panel_background.alpha(0.01),
|
||||
1.0,
|
||||
),
|
||||
linear_color_stop(cx.theme().colors().panel_background, 0.45),
|
||||
)),
|
||||
)
|
||||
.child(content),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_active_thread_or_empty_state(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
@@ -2805,6 +2894,7 @@ impl Render for AgentPanel {
|
||||
.on_action(cx.listener(Self::toggle_zoom))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.children(self.render_trial_upsell(window, cx))
|
||||
.children(self.render_trial_end_upsell(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::Thread { .. } => parent
|
||||
.relative()
|
||||
@@ -2992,25 +3082,14 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
|
||||
struct TrialUpsell;
|
||||
|
||||
fn dismissed_trial_upsell() -> bool {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
|
||||
.log_err()
|
||||
.map_or(false, |s| s.is_some())
|
||||
impl Dismissable for TrialUpsell {
|
||||
const KEY: &'static str = "dismissed-trial-upsell";
|
||||
}
|
||||
|
||||
fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
|
||||
db::write_and_log(cx, move || async move {
|
||||
if is_dismissed {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
|
||||
.await
|
||||
} else {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
|
||||
.await
|
||||
}
|
||||
})
|
||||
struct TrialEndUpsell;
|
||||
|
||||
impl Dismissable for TrialEndUpsell {
|
||||
const KEY: &'static str = "dismissed-trial-end-upsell";
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
|
||||
use crate::{RemoveAllContext, ToggleContextPicker};
|
||||
use client::ErrorExt;
|
||||
use collections::VecDeque;
|
||||
use db::kvp::Dismissable;
|
||||
use editor::display_map::EditorMargins;
|
||||
use editor::{
|
||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||
@@ -33,7 +34,6 @@ use ui::utils::WithRemSize;
|
||||
use ui::{
|
||||
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct PromptEditor<T> {
|
||||
@@ -722,7 +722,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
.child(CheckboxWithLabel::new(
|
||||
"dont-show-again",
|
||||
Label::new("Don't show again"),
|
||||
if dismissed_rate_limit_notice() {
|
||||
if RateLimitNotice::dismissed() {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
@@ -734,7 +734,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
ui::ToggleState::Selected => true,
|
||||
};
|
||||
|
||||
set_rate_limit_notice_dismissed(is_dismissed, cx)
|
||||
RateLimitNotice::set_dismissed(is_dismissed, cx);
|
||||
},
|
||||
))
|
||||
.child(
|
||||
@@ -974,7 +974,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
CodegenStatus::Error(error) => {
|
||||
if cx.has_flag::<ZedProFeatureFlag>()
|
||||
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
|
||||
&& !dismissed_rate_limit_notice()
|
||||
&& !RateLimitNotice::dismissed()
|
||||
{
|
||||
self.show_rate_limit_notice = true;
|
||||
cx.notify();
|
||||
@@ -1180,27 +1180,10 @@ impl PromptEditor<TerminalCodegen> {
|
||||
}
|
||||
}
|
||||
|
||||
const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
|
||||
struct RateLimitNotice;
|
||||
|
||||
fn dismissed_rate_limit_notice() -> bool {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.read_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY)
|
||||
.log_err()
|
||||
.map_or(false, |s| s.is_some())
|
||||
}
|
||||
|
||||
fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
|
||||
db::write_and_log(cx, move || async move {
|
||||
if is_dismissed {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.write_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into(), "1".into())
|
||||
.await
|
||||
} else {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.delete_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into())
|
||||
.await
|
||||
}
|
||||
})
|
||||
impl Dismissable for RateLimitNotice {
|
||||
const KEY: &'static str = "dismissed-rate-limit-notice";
|
||||
}
|
||||
|
||||
pub enum CodegenStatus {
|
||||
|
||||
@@ -425,16 +425,17 @@ impl ToolUseState {
|
||||
|
||||
let content = match tool_result {
|
||||
ToolResultContent::Text(text) => {
|
||||
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
|
||||
|
||||
LanguageModelToolResultContent::Text(
|
||||
let text = if text.len() < tool_output_limit {
|
||||
text
|
||||
} else {
|
||||
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
|
||||
format!(
|
||||
"Tool result too long. The first {} bytes:\n\n{}",
|
||||
truncated.len(),
|
||||
truncated
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
};
|
||||
LanguageModelToolResultContent::Text(text.into())
|
||||
}
|
||||
ToolResultContent::Image(language_model_image) => {
|
||||
if language_model_image.estimate_tokens() < tool_output_limit {
|
||||
|
||||
@@ -49,6 +49,37 @@ impl ActionLog {
|
||||
is_created: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> &mut TrackedBuffer {
|
||||
let status = if is_created {
|
||||
if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
|
||||
match tracked.status {
|
||||
TrackedBufferStatus::Created {
|
||||
existing_file_content,
|
||||
} => TrackedBufferStatus::Created {
|
||||
existing_file_content,
|
||||
},
|
||||
TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => {
|
||||
TrackedBufferStatus::Created {
|
||||
existing_file_content: Some(tracked.diff_base),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists())
|
||||
{
|
||||
TrackedBufferStatus::Created {
|
||||
existing_file_content: Some(buffer.read(cx).as_rope().clone()),
|
||||
}
|
||||
} else {
|
||||
TrackedBufferStatus::Created {
|
||||
existing_file_content: None,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TrackedBufferStatus::Modified
|
||||
};
|
||||
|
||||
let tracked_buffer = self
|
||||
.tracked_buffers
|
||||
.entry(buffer.clone())
|
||||
@@ -60,36 +91,21 @@ impl ActionLog {
|
||||
let text_snapshot = buffer.read(cx).text_snapshot();
|
||||
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
||||
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
|
||||
let base_text;
|
||||
let status;
|
||||
let diff_base;
|
||||
let unreviewed_changes;
|
||||
if is_created {
|
||||
let existing_file_content = if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists())
|
||||
{
|
||||
Some(text_snapshot.as_rope().clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
base_text = Rope::default();
|
||||
status = TrackedBufferStatus::Created {
|
||||
existing_file_content,
|
||||
};
|
||||
diff_base = Rope::default();
|
||||
unreviewed_changes = Patch::new(vec![Edit {
|
||||
old: 0..1,
|
||||
new: 0..text_snapshot.max_point().row + 1,
|
||||
}])
|
||||
} else {
|
||||
base_text = buffer.read(cx).as_rope().clone();
|
||||
status = TrackedBufferStatus::Modified;
|
||||
diff_base = buffer.read(cx).as_rope().clone();
|
||||
unreviewed_changes = Patch::default();
|
||||
}
|
||||
TrackedBuffer {
|
||||
buffer: buffer.clone(),
|
||||
base_text,
|
||||
diff_base,
|
||||
unreviewed_changes,
|
||||
snapshot: text_snapshot.clone(),
|
||||
status,
|
||||
@@ -184,7 +200,7 @@ impl ActionLog {
|
||||
.context("buffer not tracked")?;
|
||||
|
||||
let rebase = cx.background_spawn({
|
||||
let mut base_text = tracked_buffer.base_text.clone();
|
||||
let mut base_text = tracked_buffer.diff_base.clone();
|
||||
let old_snapshot = tracked_buffer.snapshot.clone();
|
||||
let new_snapshot = buffer_snapshot.clone();
|
||||
let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
|
||||
@@ -210,7 +226,7 @@ impl ActionLog {
|
||||
))
|
||||
})??;
|
||||
|
||||
let (new_base_text, new_base_text_rope) = rebase.await;
|
||||
let (new_base_text, new_diff_base) = rebase.await;
|
||||
let diff_snapshot = BufferDiff::update_diff(
|
||||
diff.clone(),
|
||||
buffer_snapshot.clone(),
|
||||
@@ -229,24 +245,23 @@ impl ActionLog {
|
||||
.background_spawn({
|
||||
let diff_snapshot = diff_snapshot.clone();
|
||||
let buffer_snapshot = buffer_snapshot.clone();
|
||||
let new_base_text_rope = new_base_text_rope.clone();
|
||||
let new_diff_base = new_diff_base.clone();
|
||||
async move {
|
||||
let mut unreviewed_changes = Patch::default();
|
||||
for hunk in diff_snapshot.hunks_intersecting_range(
|
||||
Anchor::MIN..Anchor::MAX,
|
||||
&buffer_snapshot,
|
||||
) {
|
||||
let old_range = new_base_text_rope
|
||||
let old_range = new_diff_base
|
||||
.offset_to_point(hunk.diff_base_byte_range.start)
|
||||
..new_base_text_rope
|
||||
.offset_to_point(hunk.diff_base_byte_range.end);
|
||||
..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end);
|
||||
let new_range = hunk.range.start..hunk.range.end;
|
||||
unreviewed_changes.push(point_to_row_edit(
|
||||
Edit {
|
||||
old: old_range,
|
||||
new: new_range,
|
||||
},
|
||||
&new_base_text_rope,
|
||||
&new_diff_base,
|
||||
&buffer_snapshot.as_rope(),
|
||||
));
|
||||
}
|
||||
@@ -264,7 +279,7 @@ impl ActionLog {
|
||||
.tracked_buffers
|
||||
.get_mut(&buffer)
|
||||
.context("buffer not tracked")?;
|
||||
tracked_buffer.base_text = new_base_text_rope;
|
||||
tracked_buffer.diff_base = new_diff_base;
|
||||
tracked_buffer.snapshot = buffer_snapshot;
|
||||
tracked_buffer.unreviewed_changes = unreviewed_changes;
|
||||
cx.notify();
|
||||
@@ -283,7 +298,6 @@ impl ActionLog {
|
||||
/// Mark a buffer as edited, so we can refresh it in the context
|
||||
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.edited_since_project_diagnostics_check = true;
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
self.track_buffer_internal(buffer.clone(), true, cx);
|
||||
}
|
||||
|
||||
@@ -346,11 +360,11 @@ impl ActionLog {
|
||||
true
|
||||
} else {
|
||||
let old_range = tracked_buffer
|
||||
.base_text
|
||||
.diff_base
|
||||
.point_to_offset(Point::new(edit.old.start, 0))
|
||||
..tracked_buffer.base_text.point_to_offset(cmp::min(
|
||||
..tracked_buffer.diff_base.point_to_offset(cmp::min(
|
||||
Point::new(edit.old.end, 0),
|
||||
tracked_buffer.base_text.max_point(),
|
||||
tracked_buffer.diff_base.max_point(),
|
||||
));
|
||||
let new_range = tracked_buffer
|
||||
.snapshot
|
||||
@@ -359,7 +373,7 @@ impl ActionLog {
|
||||
Point::new(edit.new.end, 0),
|
||||
tracked_buffer.snapshot.max_point(),
|
||||
));
|
||||
tracked_buffer.base_text.replace(
|
||||
tracked_buffer.diff_base.replace(
|
||||
old_range,
|
||||
&tracked_buffer
|
||||
.snapshot
|
||||
@@ -417,7 +431,7 @@ impl ActionLog {
|
||||
}
|
||||
TrackedBufferStatus::Deleted => {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_text(tracked_buffer.base_text.to_string(), cx)
|
||||
buffer.set_text(tracked_buffer.diff_base.to_string(), cx)
|
||||
});
|
||||
let save = self
|
||||
.project
|
||||
@@ -464,14 +478,14 @@ impl ActionLog {
|
||||
|
||||
if revert {
|
||||
let old_range = tracked_buffer
|
||||
.base_text
|
||||
.diff_base
|
||||
.point_to_offset(Point::new(edit.old.start, 0))
|
||||
..tracked_buffer.base_text.point_to_offset(cmp::min(
|
||||
..tracked_buffer.diff_base.point_to_offset(cmp::min(
|
||||
Point::new(edit.old.end, 0),
|
||||
tracked_buffer.base_text.max_point(),
|
||||
tracked_buffer.diff_base.max_point(),
|
||||
));
|
||||
let old_text = tracked_buffer
|
||||
.base_text
|
||||
.diff_base
|
||||
.chunks_in_range(old_range)
|
||||
.collect::<String>();
|
||||
edits_to_revert.push((new_range, old_text));
|
||||
@@ -492,7 +506,7 @@ impl ActionLog {
|
||||
TrackedBufferStatus::Deleted => false,
|
||||
_ => {
|
||||
tracked_buffer.unreviewed_changes.clear();
|
||||
tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
|
||||
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
|
||||
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
|
||||
true
|
||||
}
|
||||
@@ -655,7 +669,7 @@ enum TrackedBufferStatus {
|
||||
|
||||
struct TrackedBuffer {
|
||||
buffer: Entity<Buffer>,
|
||||
base_text: Rope,
|
||||
diff_base: Rope,
|
||||
unreviewed_changes: Patch<u32>,
|
||||
status: TrackedBufferStatus,
|
||||
version: clock::Global,
|
||||
@@ -1094,6 +1108,86 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_overwriting_previously_edited_files(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"file1": "Lorem ipsum dolor"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
|
||||
.unwrap();
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.append(" sit amet consecteur", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![HunkStatus {
|
||||
range: Point::new(0, 0)..Point::new(0, 37),
|
||||
diff_status: DiffHunkStatusKind::Modified,
|
||||
old_text: "Lorem ipsum dolor".into(),
|
||||
}],
|
||||
)]
|
||||
);
|
||||
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("rewritten", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
unreviewed_hunks(&action_log, cx),
|
||||
vec![(
|
||||
buffer.clone(),
|
||||
vec![HunkStatus {
|
||||
range: Point::new(0, 0)..Point::new(0, 9),
|
||||
diff_status: DiffHunkStatusKind::Added,
|
||||
old_text: "".into(),
|
||||
}],
|
||||
)]
|
||||
);
|
||||
|
||||
action_log
|
||||
.update(cx, |log, cx| {
|
||||
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _cx| buffer.text()),
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_deleting_files(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -1601,7 +1695,7 @@ mod tests {
|
||||
cx.run_until_parked();
|
||||
action_log.update(cx, |log, cx| {
|
||||
let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
|
||||
let mut old_text = tracked_buffer.base_text.clone();
|
||||
let mut old_text = tracked_buffer.diff_base.clone();
|
||||
let new_text = buffer.read(cx).as_rope();
|
||||
for edit in tracked_buffer.unreviewed_changes.edits() {
|
||||
let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));
|
||||
|
||||
@@ -22,7 +22,7 @@ use language::{
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::Project;
|
||||
use project::{Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -86,7 +86,7 @@ pub struct EditFileToolInput {
|
||||
pub mode: EditFileMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum EditFileMode {
|
||||
Edit,
|
||||
@@ -171,12 +171,9 @@ impl Tool for EditFileTool {
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Path {} not found in project",
|
||||
input.path.display()
|
||||
)))
|
||||
.into();
|
||||
let project_path = match resolve_path(&input, project.clone(), cx) {
|
||||
Ok(path) => path,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let card = window.and_then(|window| {
|
||||
@@ -199,20 +196,6 @@ impl Tool for EditFileTool {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let exists = buffer.read_with(cx, |buffer, _| {
|
||||
buffer
|
||||
.file()
|
||||
.as_ref()
|
||||
.map_or(false, |file| file.disk_state().exists())
|
||||
})?;
|
||||
let create_or_overwrite = match input.mode {
|
||||
EditFileMode::Create | EditFileMode::Overwrite => true,
|
||||
_ => false,
|
||||
};
|
||||
if !create_or_overwrite && !exists {
|
||||
return Err(anyhow!("{} not found", input.path.display()));
|
||||
}
|
||||
|
||||
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let old_text = cx
|
||||
.background_spawn({
|
||||
@@ -221,15 +204,15 @@ impl Tool for EditFileTool {
|
||||
})
|
||||
.await;
|
||||
|
||||
let (output, mut events) = if create_or_overwrite {
|
||||
edit_agent.overwrite(
|
||||
let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
|
||||
edit_agent.edit(
|
||||
buffer.clone(),
|
||||
input.display_description.clone(),
|
||||
&request,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
edit_agent.edit(
|
||||
edit_agent.overwrite(
|
||||
buffer.clone(),
|
||||
input.display_description.clone(),
|
||||
&request,
|
||||
@@ -349,6 +332,72 @@ impl Tool for EditFileTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that the file path is valid, meaning:
|
||||
///
|
||||
/// - For `edit` and `overwrite`, the path must point to an existing file.
|
||||
/// - For `create`, the file must not already exist, but it's parent dir must exist.
|
||||
fn resolve_path(
|
||||
input: &EditFileToolInput,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Result<ProjectPath> {
|
||||
let project = project.read(cx);
|
||||
|
||||
match input.mode {
|
||||
EditFileMode::Edit | EditFileMode::Overwrite => {
|
||||
let path = project
|
||||
.find_project_path(&input.path, cx)
|
||||
.ok_or_else(|| anyhow!("Can't edit file: path not found"))?;
|
||||
|
||||
let entry = project
|
||||
.entry_for_path(&path, cx)
|
||||
.ok_or_else(|| anyhow!("Can't edit file: path not found"))?;
|
||||
|
||||
if !entry.is_file() {
|
||||
return Err(anyhow!("Can't edit file: path is a directory"));
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
EditFileMode::Create => {
|
||||
if let Some(path) = project.find_project_path(&input.path, cx) {
|
||||
if project.entry_for_path(&path, cx).is_some() {
|
||||
return Err(anyhow!("Can't create file: file already exists"));
|
||||
}
|
||||
}
|
||||
|
||||
let parent_path = input
|
||||
.path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("Can't create file: incorrect path"))?;
|
||||
|
||||
let parent_project_path = project.find_project_path(&parent_path, cx);
|
||||
|
||||
let parent_entry = parent_project_path
|
||||
.as_ref()
|
||||
.and_then(|path| project.entry_for_path(&path, cx))
|
||||
.ok_or_else(|| anyhow!("Can't create file: parent directory doesn't exist"))?;
|
||||
|
||||
if !parent_entry.is_dir() {
|
||||
return Err(anyhow!("Can't create file: parent is not a directory"));
|
||||
}
|
||||
|
||||
let file_name = input
|
||||
.path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("Can't create file: invalid filename"))?;
|
||||
|
||||
let new_file_path = parent_project_path.map(|parent| ProjectPath {
|
||||
path: Arc::from(parent.path.join(file_name)),
|
||||
..parent
|
||||
});
|
||||
|
||||
new_file_path.ok_or_else(|| anyhow!("Can't create file"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditFileToolCard {
|
||||
path: PathBuf,
|
||||
editor: Entity<Editor>,
|
||||
@@ -868,7 +917,10 @@ async fn build_buffer_diff(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::result::Result;
|
||||
|
||||
use super::*;
|
||||
use client::TelemetrySettings;
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
@@ -908,10 +960,102 @@ mod tests {
|
||||
.await;
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
"root/nonexistent_file.txt not found"
|
||||
"Can't edit file: path not found"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
|
||||
let mode = &EditFileMode::Create;
|
||||
|
||||
let result = test_resolve_path(mode, "root/new.txt", cx);
|
||||
assert_resolved_path_eq(result.await, "new.txt");
|
||||
|
||||
let result = test_resolve_path(mode, "new.txt", cx);
|
||||
assert_resolved_path_eq(result.await, "new.txt");
|
||||
|
||||
let result = test_resolve_path(mode, "dir/new.txt", cx);
|
||||
assert_resolved_path_eq(result.await, "dir/new.txt");
|
||||
|
||||
let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
|
||||
assert_eq!(
|
||||
result.await.unwrap_err().to_string(),
|
||||
"Can't create file: file already exists"
|
||||
);
|
||||
|
||||
let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
|
||||
assert_eq!(
|
||||
result.await.unwrap_err().to_string(),
|
||||
"Can't create file: parent directory doesn't exist"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
|
||||
let mode = &EditFileMode::Edit;
|
||||
|
||||
let path_with_root = "root/dir/subdir/existing.txt";
|
||||
let path_without_root = "dir/subdir/existing.txt";
|
||||
let result = test_resolve_path(mode, path_with_root, cx);
|
||||
assert_resolved_path_eq(result.await, path_without_root);
|
||||
|
||||
let result = test_resolve_path(mode, path_without_root, cx);
|
||||
assert_resolved_path_eq(result.await, path_without_root);
|
||||
|
||||
let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
|
||||
assert_eq!(
|
||||
result.await.unwrap_err().to_string(),
|
||||
"Can't edit file: path not found"
|
||||
);
|
||||
|
||||
let result = test_resolve_path(mode, "root/dir", cx);
|
||||
assert_eq!(
|
||||
result.await.unwrap_err().to_string(),
|
||||
"Can't edit file: path is a directory"
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_resolve_path(
|
||||
mode: &EditFileMode,
|
||||
path: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Result<ProjectPath, anyhow::Error> {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"dir": {
|
||||
"subdir": {
|
||||
"existing.txt": "hello"
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let input = EditFileToolInput {
|
||||
display_description: "Some edit".into(),
|
||||
path: path.into(),
|
||||
mode: mode.clone(),
|
||||
};
|
||||
|
||||
let result = cx.update(|cx| resolve_path(&input, project, cx));
|
||||
result
|
||||
}
|
||||
|
||||
fn assert_resolved_path_eq(path: Result<ProjectPath, anyhow::Error>, expected: &str) {
|
||||
let actual = path
|
||||
.expect("Should return valid path")
|
||||
.path
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace("\\", "/"); // Naive Windows paths normalization
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path() {
|
||||
let input = json!({
|
||||
@@ -984,6 +1128,7 @@ mod tests {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
TelemetrySettings::register(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use gpui::App;
|
||||
use sqlez_macros::sql;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{define_connection, query};
|
||||
use crate::{define_connection, query, write_and_log};
|
||||
|
||||
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
|
||||
&[sql!(
|
||||
@@ -11,6 +13,29 @@ define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
|
||||
)];
|
||||
);
|
||||
|
||||
pub trait Dismissable {
|
||||
const KEY: &'static str;
|
||||
|
||||
fn dismissed() -> bool {
|
||||
KEY_VALUE_STORE
|
||||
.read_kvp(Self::KEY)
|
||||
.log_err()
|
||||
.map_or(false, |s| s.is_some())
|
||||
}
|
||||
|
||||
fn set_dismissed(is_dismissed: bool, cx: &mut App) {
|
||||
write_and_log(cx, move || async move {
|
||||
if is_dismissed {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(Self::KEY.into(), "1".into())
|
||||
.await
|
||||
} else {
|
||||
KEY_VALUE_STORE.delete_kvp(Self::KEY.into()).await
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyValueStore {
|
||||
query! {
|
||||
pub fn read_kvp(key: &str) -> Result<Option<String>> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::persistence::DebuggerPaneItem;
|
||||
use crate::session::DebugSession;
|
||||
use crate::session::running::RunningState;
|
||||
use crate::{
|
||||
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
|
||||
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
|
||||
@@ -30,7 +31,7 @@ use settings::Settings;
|
||||
use std::any::TypeId;
|
||||
use std::sync::Arc;
|
||||
use task::{DebugScenario, TaskContext};
|
||||
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
|
||||
use ui::{ContextMenu, Divider, Tooltip, prelude::*};
|
||||
use workspace::SplitDirection;
|
||||
use workspace::{
|
||||
Pane, Workspace,
|
||||
@@ -87,7 +88,20 @@ impl DebugPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn filter_action_types(&self, cx: &mut App) {
|
||||
pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> {
|
||||
self.sessions.clone()
|
||||
}
|
||||
|
||||
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
|
||||
self.active_session.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn running_state(&self, cx: &mut App) -> Option<Entity<RunningState>> {
|
||||
self.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
}
|
||||
|
||||
pub(crate) fn filter_action_types(&self, cx: &mut App) {
|
||||
let (has_active_session, supports_restart, support_step_back, status) = self
|
||||
.active_session()
|
||||
.map(|item| {
|
||||
@@ -273,7 +287,7 @@ impl DebugPanel {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
async fn register_session(
|
||||
pub(crate) async fn register_session(
|
||||
this: WeakEntity<Self>,
|
||||
session: Entity<Session>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
@@ -342,7 +356,7 @@ impl DebugPanel {
|
||||
Ok(debug_session)
|
||||
}
|
||||
|
||||
fn handle_restart_request(
|
||||
pub(crate) fn handle_restart_request(
|
||||
&mut self,
|
||||
mut curr_session: Entity<Session>,
|
||||
window: &mut Window,
|
||||
@@ -416,11 +430,12 @@ impl DebugPanel {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
|
||||
self.active_session.clone()
|
||||
}
|
||||
|
||||
fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(crate) fn close_session(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(session) = self
|
||||
.sessions
|
||||
.iter()
|
||||
@@ -474,93 +489,8 @@ impl DebugPanel {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
fn sessions_drop_down_menu(
|
||||
&self,
|
||||
active_session: &Entity<DebugSession>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> DropdownMenu {
|
||||
let sessions = self.sessions.clone();
|
||||
let weak = cx.weak_entity();
|
||||
let label = active_session.read(cx).label_element(cx);
|
||||
|
||||
DropdownMenu::new_with_element(
|
||||
"debugger-session-list",
|
||||
label,
|
||||
ContextMenu::build(window, cx, move |mut this, _, cx| {
|
||||
let context_menu = cx.weak_entity();
|
||||
for session in sessions.into_iter() {
|
||||
let weak_session = session.downgrade();
|
||||
let weak_session_id = weak_session.entity_id();
|
||||
|
||||
this = this.custom_entry(
|
||||
{
|
||||
let weak = weak.clone();
|
||||
let context_menu = context_menu.clone();
|
||||
move |_, cx| {
|
||||
weak_session
|
||||
.read_with(cx, |session, cx| {
|
||||
let context_menu = context_menu.clone();
|
||||
let id: SharedString =
|
||||
format!("debug-session-{}", session.session_id(cx).0)
|
||||
.into();
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group(id.clone())
|
||||
.justify_between()
|
||||
.child(session.label_element(cx))
|
||||
.child(
|
||||
IconButton::new(
|
||||
"close-debug-session",
|
||||
IconName::Close,
|
||||
)
|
||||
.visible_on_hover(id.clone())
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
move |_, window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.close_session(
|
||||
weak_session_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
context_menu
|
||||
.update(cx, |this, cx| {
|
||||
this.cancel(
|
||||
&Default::default(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or_else(|_| div().into_any_element())
|
||||
}
|
||||
},
|
||||
{
|
||||
let weak = weak.clone();
|
||||
move |window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.activate_session(session.clone(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
this
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn deploy_context_menu(
|
||||
pub(crate) fn deploy_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
window: &mut Window,
|
||||
@@ -611,7 +541,11 @@ impl DebugPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
pub(crate) fn top_controls_strip(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Div> {
|
||||
let active_session = self.active_session.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal;
|
||||
@@ -651,12 +585,12 @@ impl DebugPanel {
|
||||
active_session
|
||||
.as_ref()
|
||||
.map(|session| session.read(cx).running_state()),
|
||||
|this, running_session| {
|
||||
|this, running_state| {
|
||||
let thread_status =
|
||||
running_session.read(cx).thread_status(cx).unwrap_or(
|
||||
running_state.read(cx).thread_status(cx).unwrap_or(
|
||||
project::debugger::session::ThreadStatus::Exited,
|
||||
);
|
||||
let capabilities = running_session.read(cx).capabilities(cx);
|
||||
let capabilities = running_state.read(cx).capabilities(cx);
|
||||
this.map(|this| {
|
||||
if thread_status == ThreadStatus::Running {
|
||||
this.child(
|
||||
@@ -667,7 +601,7 @@ impl DebugPanel {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.pause_thread(cx);
|
||||
},
|
||||
@@ -694,7 +628,7 @@ impl DebugPanel {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| this.continue_thread(cx),
|
||||
))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
@@ -718,7 +652,7 @@ impl DebugPanel {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.step_over(cx);
|
||||
},
|
||||
@@ -742,7 +676,7 @@ impl DebugPanel {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.step_out(cx);
|
||||
},
|
||||
@@ -769,7 +703,7 @@ impl DebugPanel {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.step_in(cx);
|
||||
},
|
||||
@@ -819,7 +753,7 @@ impl DebugPanel {
|
||||
|| thread_status == ThreadStatus::Ended,
|
||||
)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.toggle_ignore_breakpoints(cx);
|
||||
},
|
||||
@@ -842,7 +776,7 @@ impl DebugPanel {
|
||||
IconButton::new("debug-restart", IconName::DebugRestart)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.restart_session(cx);
|
||||
},
|
||||
@@ -864,7 +798,7 @@ impl DebugPanel {
|
||||
IconButton::new("debug-stop", IconName::Power)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.stop_thread(cx);
|
||||
},
|
||||
@@ -898,7 +832,7 @@ impl DebugPanel {
|
||||
IconButton::new("debug-disconnect", IconName::DebugDetach)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _, cx| {
|
||||
this.detach_client(cx);
|
||||
},
|
||||
@@ -932,30 +866,42 @@ impl DebugPanel {
|
||||
.as_ref()
|
||||
.map(|session| session.read(cx).running_state())
|
||||
.cloned(),
|
||||
|this, session| {
|
||||
this.child(
|
||||
session.update(cx, |this, cx| {
|
||||
this.thread_dropdown(window, cx)
|
||||
}),
|
||||
)
|
||||
|this, running_state| {
|
||||
this.children({
|
||||
let running_state = running_state.clone();
|
||||
let threads =
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let session = running_state.session();
|
||||
session
|
||||
.update(cx, |session, cx| session.threads(cx))
|
||||
});
|
||||
|
||||
self.render_thread_dropdown(
|
||||
&running_state,
|
||||
threads,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.when(!is_side, |this| this.gap_2().child(Divider::vertical()))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.when_some(active_session.as_ref(), |this, session| {
|
||||
let context_menu =
|
||||
self.sessions_drop_down_menu(session, window, cx);
|
||||
this.child(context_menu).gap_2().child(Divider::vertical())
|
||||
})
|
||||
.children(self.render_session_menu(
|
||||
self.active_session(),
|
||||
self.running_state(cx),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(!is_side, |this| this.child(new_session_button())),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn activate_pane_in_direction(
|
||||
pub(crate) fn activate_pane_in_direction(
|
||||
&mut self,
|
||||
direction: SplitDirection,
|
||||
window: &mut Window,
|
||||
@@ -970,7 +916,7 @@ impl DebugPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_item(
|
||||
pub(crate) fn activate_item(
|
||||
&mut self,
|
||||
item: DebuggerPaneItem,
|
||||
window: &mut Window,
|
||||
@@ -985,7 +931,7 @@ impl DebugPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_session(
|
||||
pub(crate) fn activate_session(
|
||||
&mut self,
|
||||
session_item: Entity<DebugSession>,
|
||||
window: &mut Window,
|
||||
|
||||
@@ -13,6 +13,7 @@ use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
|
||||
|
||||
pub mod attach_modal;
|
||||
pub mod debugger_panel;
|
||||
mod dropdown_menus;
|
||||
mod new_session_modal;
|
||||
mod persistence;
|
||||
pub(crate) mod session;
|
||||
|
||||
186
crates/debugger_ui/src/dropdown_menus.rs
Normal file
186
crates/debugger_ui/src/dropdown_menus.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use gpui::Entity;
|
||||
use project::debugger::session::{ThreadId, ThreadStatus};
|
||||
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
|
||||
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
session::{DebugSession, running::RunningState},
|
||||
};
|
||||
|
||||
impl DebugPanel {
|
||||
fn dropdown_label(label: impl Into<SharedString>) -> Label {
|
||||
Label::new(label).size(LabelSize::Small)
|
||||
}
|
||||
|
||||
pub fn render_session_menu(
|
||||
&mut self,
|
||||
active_session: Option<Entity<DebugSession>>,
|
||||
running_state: Option<Entity<RunningState>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
if let Some(running_state) = running_state {
|
||||
let sessions = self.sessions().clone();
|
||||
let weak = cx.weak_entity();
|
||||
let running_state = running_state.read(cx);
|
||||
let label = if let Some(active_session) = active_session {
|
||||
active_session.read(cx).session(cx).read(cx).label()
|
||||
} else {
|
||||
SharedString::new_static("Unknown Session")
|
||||
};
|
||||
|
||||
let is_terminated = running_state.session().read(cx).is_terminated();
|
||||
let session_state_indicator = {
|
||||
if is_terminated {
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
} else {
|
||||
match running_state.thread_status(cx).unwrap_or_default() {
|
||||
project::debugger::session::ThreadStatus::Stopped => {
|
||||
Some(Indicator::dot().color(Color::Conflict))
|
||||
}
|
||||
_ => Some(Indicator::dot().color(Color::Success)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let trigger = h_flex()
|
||||
.gap_2()
|
||||
.when_some(session_state_indicator, |this, indicator| {
|
||||
this.child(indicator)
|
||||
})
|
||||
.justify_between()
|
||||
.child(
|
||||
DebugPanel::dropdown_label(label)
|
||||
.when(is_terminated, |this| this.strikethrough()),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
Some(
|
||||
DropdownMenu::new_with_element(
|
||||
"debugger-session-list",
|
||||
trigger,
|
||||
ContextMenu::build(window, cx, move |mut this, _, cx| {
|
||||
let context_menu = cx.weak_entity();
|
||||
for session in sessions.into_iter() {
|
||||
let weak_session = session.downgrade();
|
||||
let weak_session_id = weak_session.entity_id();
|
||||
|
||||
this = this.custom_entry(
|
||||
{
|
||||
let weak = weak.clone();
|
||||
let context_menu = context_menu.clone();
|
||||
move |_, cx| {
|
||||
weak_session
|
||||
.read_with(cx, |session, cx| {
|
||||
let context_menu = context_menu.clone();
|
||||
let id: SharedString = format!(
|
||||
"debug-session-{}",
|
||||
session.session_id(cx).0
|
||||
)
|
||||
.into();
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group(id.clone())
|
||||
.justify_between()
|
||||
.child(session.label_element(cx))
|
||||
.child(
|
||||
IconButton::new(
|
||||
"close-debug-session",
|
||||
IconName::Close,
|
||||
)
|
||||
.visible_on_hover(id.clone())
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
move |_, window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.close_session(
|
||||
weak_session_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
context_menu
|
||||
.update(cx, |this, cx| {
|
||||
this.cancel(
|
||||
&Default::default(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or_else(|_| div().into_any_element())
|
||||
}
|
||||
},
|
||||
{
|
||||
let weak = weak.clone();
|
||||
move |window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.activate_session(session.clone(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
this
|
||||
}),
|
||||
)
|
||||
.style(DropdownStyle::Ghost),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn render_thread_dropdown(
|
||||
&self,
|
||||
running_state: &Entity<RunningState>,
|
||||
threads: Vec<(dap::Thread, ThreadStatus)>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<DropdownMenu> {
|
||||
let running_state = running_state.clone();
|
||||
let running_state_read = running_state.read(cx);
|
||||
let thread_id = running_state_read.thread_id();
|
||||
let session = running_state_read.session();
|
||||
let session_id = session.read(cx).session_id();
|
||||
let session_terminated = session.read(cx).is_terminated();
|
||||
let selected_thread_name = threads
|
||||
.iter()
|
||||
.find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
|
||||
.map(|(thread, _)| thread.name.clone());
|
||||
|
||||
if let Some(selected_thread_name) = selected_thread_name {
|
||||
let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
|
||||
Some(
|
||||
DropdownMenu::new_with_element(
|
||||
("thread-list", session_id.0),
|
||||
trigger,
|
||||
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
|
||||
for (thread, _) in threads {
|
||||
let running_state = running_state.clone();
|
||||
let thread_id = thread.id;
|
||||
this = this.entry(thread.name, None, move |window, cx| {
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
running_state.select_thread(ThreadId(thread_id), window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
this
|
||||
}),
|
||||
)
|
||||
.disabled(session_terminated)
|
||||
.style(DropdownStyle::Ghost),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod running;
|
||||
|
||||
use std::{cell::OnceCell, sync::OnceLock};
|
||||
|
||||
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
|
||||
use dap::client::SessionId;
|
||||
use gpui::{
|
||||
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
@@ -11,14 +10,13 @@ use project::debugger::session::Session;
|
||||
use project::worktree_store::WorktreeStore;
|
||||
use rpc::proto;
|
||||
use running::RunningState;
|
||||
use std::{cell::OnceCell, sync::OnceLock};
|
||||
use ui::{Indicator, prelude::*};
|
||||
use workspace::{
|
||||
CollaboratorId, FollowableItem, ViewId, Workspace,
|
||||
item::{self, Item},
|
||||
};
|
||||
|
||||
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
|
||||
|
||||
pub struct DebugSession {
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
running_state: Entity<RunningState>,
|
||||
@@ -159,7 +157,11 @@ impl DebugSession {
|
||||
.gap_2()
|
||||
.when_some(icon, |this, indicator| this.child(indicator))
|
||||
.justify_between()
|
||||
.child(Label::new(label).when(is_terminated, |this| this.strikethrough()))
|
||||
.child(
|
||||
Label::new(label)
|
||||
.size(LabelSize::Small)
|
||||
.when(is_terminated, |this| this.strikethrough()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,11 +43,10 @@ use task::{
|
||||
};
|
||||
use terminal_view::TerminalView;
|
||||
use ui::{
|
||||
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
|
||||
Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement,
|
||||
IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Tab, Tooltip, VisibleOnHover, VisualContext, Window, div,
|
||||
h_flex, v_flex,
|
||||
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, FluentBuilder,
|
||||
IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon as _,
|
||||
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Tab, Tooltip,
|
||||
VisibleOnHover, VisualContext, Window, div, h_flex, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use variable_list::VariableList;
|
||||
@@ -78,6 +77,12 @@ pub struct RunningState {
|
||||
_schedule_serialize: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl RunningState {
|
||||
pub(crate) fn thread_id(&self) -> Option<ThreadId> {
|
||||
self.thread_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RunningState {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let zoomed_pane = self
|
||||
@@ -515,7 +520,7 @@ impl Focusable for DebugTerminal {
|
||||
}
|
||||
|
||||
impl RunningState {
|
||||
pub fn new(
|
||||
pub(crate) fn new(
|
||||
session: Entity<Session>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -1214,10 +1219,16 @@ impl RunningState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
|
||||
pub(crate) fn go_to_selected_stack_frame(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.thread_id.is_some() {
|
||||
self.stack_frame_list
|
||||
.update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
|
||||
.update(cx, |list, cx| {
|
||||
let Some(stack_frame_id) = list.opened_stack_frame_id() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
list.go_to_stack_frame(stack_frame_id, window, cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1234,7 +1245,7 @@ impl RunningState {
|
||||
}
|
||||
|
||||
pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
|
||||
self.stack_frame_list.read(cx).selected_stack_frame_id()
|
||||
self.stack_frame_list.read(cx).opened_stack_frame_id()
|
||||
}
|
||||
|
||||
pub(crate) fn stack_frame_list(&self) -> &Entity<StackFrameList> {
|
||||
@@ -1311,7 +1322,12 @@ impl RunningState {
|
||||
.map(|id| self.session().read(cx).thread_status(id))
|
||||
}
|
||||
|
||||
fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(crate) fn select_thread(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.thread_id.is_some_and(|id| id == thread_id) {
|
||||
return;
|
||||
}
|
||||
@@ -1448,38 +1464,6 @@ impl RunningState {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn thread_dropdown(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, RunningState>,
|
||||
) -> DropdownMenu {
|
||||
let state = cx.entity();
|
||||
let session_terminated = self.session.read(cx).is_terminated();
|
||||
let threads = self.session.update(cx, |this, cx| this.threads(cx));
|
||||
let selected_thread_name = threads
|
||||
.iter()
|
||||
.find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
|
||||
.map(|(thread, _)| thread.name.clone())
|
||||
.unwrap_or("Threads".to_owned());
|
||||
DropdownMenu::new(
|
||||
("thread-list", self.session_id.0),
|
||||
selected_thread_name,
|
||||
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
|
||||
for (thread, _) in threads {
|
||||
let state = state.clone();
|
||||
let thread_id = thread.id;
|
||||
this = this.entry(thread.name, None, move |window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.select_thread(ThreadId(thread_id), window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
this
|
||||
}),
|
||||
)
|
||||
.disabled(session_terminated)
|
||||
}
|
||||
|
||||
fn default_pane_layout(
|
||||
project: Entity<Project>,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
|
||||
@@ -21,8 +21,8 @@ use project::{
|
||||
use ui::{
|
||||
App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement,
|
||||
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
|
||||
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Window, div,
|
||||
h_flex, px, v_flex,
|
||||
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window,
|
||||
div, h_flex, px, v_flex,
|
||||
};
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::Workspace;
|
||||
@@ -259,6 +259,11 @@ impl LineBreakpoint {
|
||||
dir, name, line
|
||||
)))
|
||||
.cursor_pointer()
|
||||
.tooltip(Tooltip::text(if breakpoint.state.is_enabled() {
|
||||
"Disable Breakpoint"
|
||||
} else {
|
||||
"Enable Breakpoint"
|
||||
}))
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
let path = path.clone();
|
||||
@@ -290,6 +295,9 @@ impl LineBreakpoint {
|
||||
)))
|
||||
.start_slot(indicator)
|
||||
.rounded()
|
||||
.on_secondary_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.end_hover_slot(
|
||||
IconButton::new(
|
||||
SharedString::from(format!(
|
||||
@@ -423,12 +431,20 @@ impl ExceptionBreakpoint {
|
||||
self.id
|
||||
)))
|
||||
.rounded()
|
||||
.on_secondary_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.start_slot(
|
||||
div()
|
||||
.id(SharedString::from(format!(
|
||||
"exception-breakpoint-ui-item-{}-click-handler",
|
||||
self.id
|
||||
)))
|
||||
.tooltip(Tooltip::text(if self.is_enabled {
|
||||
"Disable Exception Breakpoint"
|
||||
} else {
|
||||
"Enable Exception Breakpoint"
|
||||
}))
|
||||
.on_click(move |_, _, cx| {
|
||||
list.update(cx, |this, cx| {
|
||||
this.session.update(cx, |this, cx| {
|
||||
|
||||
@@ -162,7 +162,7 @@ impl Console {
|
||||
.evaluate(
|
||||
expression,
|
||||
Some(dap::EvaluateArgumentsContext::Repl),
|
||||
self.stack_frame_list.read(cx).selected_stack_frame_id(),
|
||||
self.stack_frame_list.read(cx).opened_stack_frame_id(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
@@ -389,7 +389,7 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
let completion_task = console.update(cx, |console, cx| {
|
||||
console.session.update(cx, |state, cx| {
|
||||
let frame_id = console.stack_frame_list.read(cx).selected_stack_frame_id();
|
||||
let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
|
||||
|
||||
state.completions(
|
||||
CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
use dap::Module;
|
||||
use gpui::{
|
||||
AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful,
|
||||
Subscription, WeakEntity, list,
|
||||
AnyElement, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
|
||||
Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
|
||||
};
|
||||
use project::{
|
||||
ProjectItem as _, ProjectPath,
|
||||
@@ -9,16 +10,17 @@ use project::{
|
||||
};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use ui::{Scrollbar, ScrollbarState, prelude::*};
|
||||
use util::maybe;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct ModuleList {
|
||||
list: ListState,
|
||||
invalidate: bool,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_ix: Option<usize>,
|
||||
session: Entity<Session>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
scrollbar_state: ScrollbarState,
|
||||
entries: Vec<Module>,
|
||||
_rebuild_task: Task<()>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
@@ -28,38 +30,43 @@ impl ModuleList {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let weak_entity = cx.weak_entity();
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let list = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, _window, cx| {
|
||||
weak_entity
|
||||
.upgrade()
|
||||
.map(|module_list| module_list.update(cx, |this, cx| this.render_entry(ix, cx)))
|
||||
.unwrap_or(div().into_any())
|
||||
},
|
||||
);
|
||||
|
||||
let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
|
||||
SessionEvent::Stopped(_) | SessionEvent::Modules => {
|
||||
this.invalidate = true;
|
||||
cx.notify();
|
||||
this.schedule_rebuild(cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Self {
|
||||
scrollbar_state: ScrollbarState::new(list.clone()),
|
||||
list,
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
let mut this = Self {
|
||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
|
||||
scroll_handle,
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
entries: Vec::new(),
|
||||
selected_ix: None,
|
||||
_subscription,
|
||||
invalidate: true,
|
||||
}
|
||||
_rebuild_task: Task::ready(()),
|
||||
};
|
||||
this.schedule_rebuild(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
|
||||
self._rebuild_task = cx.spawn(async move |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
let modules = this
|
||||
.session
|
||||
.update(cx, |session, cx| session.modules(cx).to_owned());
|
||||
this.entries = modules;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
|
||||
fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -111,36 +118,40 @@ impl ModuleList {
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(module) = maybe!({
|
||||
self.session
|
||||
.update(cx, |state, cx| state.modules(cx).get(ix).cloned())
|
||||
}) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
let module = self.entries[ix].clone();
|
||||
|
||||
v_flex()
|
||||
.rounded_md()
|
||||
.w_full()
|
||||
.group("")
|
||||
.id(("module-list", ix))
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.when(module.path.is_some(), |this| {
|
||||
this.on_click({
|
||||
let path = module.path.as_deref().map(|path| Arc::<Path>::from(Path::new(path)));
|
||||
let path = module
|
||||
.path
|
||||
.as_deref()
|
||||
.map(|path| Arc::<Path>::from(Path::new(path)));
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.selected_ix = Some(ix);
|
||||
if let Some(path) = path.as_ref() {
|
||||
this.open_module(path.clone(), window, cx);
|
||||
} else {
|
||||
log::error!("Wasn't able to find module path, but was still able to click on module list entry");
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
})
|
||||
.p_1()
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||
.when(Some(ix) == self.selected_ix, |s| {
|
||||
s.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -188,6 +199,96 @@ impl ModuleList {
|
||||
.cursor_default()
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(ix) = self.selected_ix else { return };
|
||||
let Some(entry) = self.entries.get(ix) else {
|
||||
return;
|
||||
};
|
||||
let Some(path) = entry.path.as_deref() else {
|
||||
return;
|
||||
};
|
||||
let path = Arc::from(Path::new(path));
|
||||
self.open_module(path, window, cx);
|
||||
}
|
||||
|
||||
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
|
||||
self.selected_ix = ix;
|
||||
if let Some(ix) = ix {
|
||||
self.scroll_handle
|
||||
.scroll_to_item(ix, ScrollStrategy::Center);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.entries.len() == 0 => None,
|
||||
None => Some(0),
|
||||
Some(ix) => {
|
||||
if ix == self.entries.len() - 1 {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(ix + 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.entries.len() == 0 => None,
|
||||
None => Some(self.entries.len() - 1),
|
||||
Some(ix) => {
|
||||
if ix == 0 {
|
||||
Some(self.entries.len() - 1)
|
||||
} else {
|
||||
Some(ix - 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let ix = if self.entries.len() > 0 {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let ix = if self.entries.len() > 0 {
|
||||
Some(self.entries.len() - 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
uniform_list(
|
||||
cx.entity(),
|
||||
"module-list",
|
||||
self.entries.len(),
|
||||
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.size_full()
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ModuleList {
|
||||
@@ -197,21 +298,17 @@ impl Focusable for ModuleList {
|
||||
}
|
||||
|
||||
impl Render for ModuleList {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.invalidate {
|
||||
let len = self
|
||||
.session
|
||||
.update(cx, |session, cx| session.modules(cx).len());
|
||||
self.list.reset(len);
|
||||
self.invalidate = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.size_full()
|
||||
.p_1()
|
||||
.child(list(self.list.clone()).size_full())
|
||||
.child(self.render_list(window, cx))
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,18 @@ use std::time::Duration;
|
||||
use anyhow::{Result, anyhow};
|
||||
use dap::StackFrameId;
|
||||
use gpui::{
|
||||
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
|
||||
Subscription, Task, WeakEntity, list,
|
||||
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,
|
||||
Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
|
||||
};
|
||||
|
||||
use crate::StackTraceView;
|
||||
use language::PointUtf16;
|
||||
use project::debugger::breakpoint_store::ActiveStackFrame;
|
||||
use project::debugger::session::{Session, SessionEvent, StackFrame};
|
||||
use project::{ProjectItem, ProjectPath};
|
||||
use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::{ItemHandle, Workspace};
|
||||
|
||||
use crate::StackTraceView;
|
||||
|
||||
use super::RunningState;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -28,15 +26,16 @@ pub enum StackFrameListEvent {
|
||||
}
|
||||
|
||||
pub struct StackFrameList {
|
||||
list: ListState,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: Subscription,
|
||||
session: Entity<Session>,
|
||||
state: WeakEntity<RunningState>,
|
||||
entries: Vec<StackFrameEntry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
selected_stack_frame_id: Option<StackFrameId>,
|
||||
selected_ix: Option<usize>,
|
||||
opened_stack_frame_id: Option<StackFrameId>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
|
||||
@@ -55,22 +54,8 @@ impl StackFrameList {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let weak_entity = cx.weak_entity();
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let list = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, _window, cx| {
|
||||
weak_entity
|
||||
.upgrade()
|
||||
.map(|stack_frame_list| {
|
||||
stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx))
|
||||
})
|
||||
.unwrap_or(div().into_any())
|
||||
},
|
||||
);
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
let _subscription =
|
||||
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
|
||||
@@ -84,15 +69,16 @@ impl StackFrameList {
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
scrollbar_state: ScrollbarState::new(list.clone()),
|
||||
list,
|
||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
state,
|
||||
_subscription,
|
||||
entries: Default::default(),
|
||||
selected_stack_frame_id: None,
|
||||
selected_ix: None,
|
||||
opened_stack_frame_id: None,
|
||||
scroll_handle,
|
||||
_refresh_task: Task::ready(()),
|
||||
};
|
||||
this.schedule_refresh(true, window, cx);
|
||||
@@ -123,7 +109,7 @@ impl StackFrameList {
|
||||
fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
|
||||
self.state
|
||||
.read_with(cx, |state, _| state.thread_id)
|
||||
.log_err()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|thread_id| {
|
||||
self.session
|
||||
@@ -140,27 +126,8 @@ impl StackFrameList {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn selected_stack_frame_id(&self) -> Option<StackFrameId> {
|
||||
self.selected_stack_frame_id
|
||||
}
|
||||
|
||||
pub(crate) fn select_stack_frame_id(
|
||||
&mut self,
|
||||
id: StackFrameId,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !self.entries.iter().any(|entry| match entry {
|
||||
StackFrameEntry::Normal(entry) => entry.id == id,
|
||||
StackFrameEntry::Collapsed(stack_frames) => {
|
||||
stack_frames.iter().any(|frame| frame.id == id)
|
||||
}
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.selected_stack_frame_id = Some(id);
|
||||
self.go_to_selected_stack_frame(window, cx);
|
||||
pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
|
||||
self.opened_stack_frame_id
|
||||
}
|
||||
|
||||
pub(super) fn schedule_refresh(
|
||||
@@ -193,13 +160,22 @@ impl StackFrameList {
|
||||
|
||||
pub fn build_entries(
|
||||
&mut self,
|
||||
select_first_stack_frame: bool,
|
||||
open_first_stack_frame: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let old_selected_frame_id = self
|
||||
.selected_ix
|
||||
.and_then(|ix| self.entries.get(ix))
|
||||
.and_then(|entry| match entry {
|
||||
StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
|
||||
StackFrameEntry::Collapsed(stack_frames) => {
|
||||
stack_frames.first().map(|stack_frame| stack_frame.id)
|
||||
}
|
||||
});
|
||||
let mut entries = Vec::new();
|
||||
let mut collapsed_entries = Vec::new();
|
||||
let mut current_stack_frame = None;
|
||||
let mut first_stack_frame = None;
|
||||
|
||||
let stack_frames = self.stack_frames(cx);
|
||||
for stack_frame in &stack_frames {
|
||||
@@ -213,7 +189,7 @@ impl StackFrameList {
|
||||
entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
|
||||
}
|
||||
|
||||
current_stack_frame.get_or_insert(&stack_frame.dap);
|
||||
first_stack_frame.get_or_insert(entries.len());
|
||||
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
|
||||
}
|
||||
}
|
||||
@@ -225,69 +201,60 @@ impl StackFrameList {
|
||||
}
|
||||
|
||||
std::mem::swap(&mut self.entries, &mut entries);
|
||||
self.list.reset(self.entries.len());
|
||||
|
||||
if let Some(current_stack_frame) = current_stack_frame.filter(|_| select_first_stack_frame)
|
||||
{
|
||||
self.select_stack_frame(current_stack_frame, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) {
|
||||
self.select_ix(Some(ix), cx);
|
||||
self.activate_selected_entry(window, cx);
|
||||
} else if let Some(old_selected_frame_id) = old_selected_frame_id {
|
||||
let ix = self.entries.iter().position(|entry| match entry {
|
||||
StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
|
||||
StackFrameEntry::Collapsed(frames) => {
|
||||
frames.iter().any(|frame| frame.id == old_selected_frame_id)
|
||||
}
|
||||
});
|
||||
self.selected_ix = ix;
|
||||
}
|
||||
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
|
||||
if let Some(selected_stack_frame_id) = self.selected_stack_frame_id {
|
||||
let frame = self
|
||||
.entries
|
||||
.iter()
|
||||
.find_map(|entry| match entry {
|
||||
StackFrameEntry::Normal(dap) => {
|
||||
if dap.id == selected_stack_frame_id {
|
||||
Some(dap)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
StackFrameEntry::Collapsed(daps) => {
|
||||
daps.iter().find(|dap| dap.id == selected_stack_frame_id)
|
||||
}
|
||||
})
|
||||
.cloned();
|
||||
|
||||
if let Some(frame) = frame.as_ref() {
|
||||
self.select_stack_frame(frame, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_stack_frame(
|
||||
pub fn go_to_stack_frame(
|
||||
&mut self,
|
||||
stack_frame: &dap::StackFrame,
|
||||
go_to_stack_frame: bool,
|
||||
window: &Window,
|
||||
stack_frame_id: StackFrameId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.selected_stack_frame_id = Some(stack_frame.id);
|
||||
|
||||
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
|
||||
stack_frame.id,
|
||||
));
|
||||
cx.notify();
|
||||
|
||||
if !go_to_stack_frame {
|
||||
return Task::ready(Ok(()));
|
||||
let Some(stack_frame) = self
|
||||
.entries
|
||||
.iter()
|
||||
.flat_map(|entry| match entry {
|
||||
StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
|
||||
StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
|
||||
})
|
||||
.find(|stack_frame| stack_frame.id == stack_frame_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("No stack frame for ID")));
|
||||
};
|
||||
self.go_to_stack_frame_inner(stack_frame, window, cx)
|
||||
}
|
||||
|
||||
let row = (stack_frame.line.saturating_sub(1)) as u32;
|
||||
|
||||
fn go_to_stack_frame_inner(
|
||||
&mut self,
|
||||
stack_frame: dap::StackFrame,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let stack_frame_id = stack_frame.id;
|
||||
self.opened_stack_frame_id = Some(stack_frame_id);
|
||||
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
|
||||
return Task::ready(Err(anyhow!("Project path not found")));
|
||||
};
|
||||
|
||||
let stack_frame_id = stack_frame.id;
|
||||
let row = stack_frame.line.saturating_sub(1) as u32;
|
||||
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
|
||||
stack_frame_id,
|
||||
));
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (worktree, relative_path) = this
|
||||
.update(cx, |this, cx| {
|
||||
@@ -386,11 +353,12 @@ impl StackFrameList {
|
||||
|
||||
fn render_normal_entry(
|
||||
&self,
|
||||
ix: usize,
|
||||
stack_frame: &dap::StackFrame,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let source = stack_frame.source.clone();
|
||||
let is_selected_frame = Some(stack_frame.id) == self.selected_stack_frame_id;
|
||||
let is_selected_frame = Some(ix) == self.selected_ix;
|
||||
|
||||
let path = source.clone().and_then(|s| s.path.or(s.name));
|
||||
let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
|
||||
@@ -426,12 +394,12 @@ impl StackFrameList {
|
||||
.when(is_selected_frame, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let stack_frame = stack_frame.clone();
|
||||
move |this, _, window, cx| {
|
||||
this.select_stack_frame(&stack_frame, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.selected_ix = Some(ix);
|
||||
this.activate_selected_entry(window, cx);
|
||||
}))
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(
|
||||
@@ -486,20 +454,15 @@ impl StackFrameList {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
pub fn expand_collapsed_entry(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
stack_frames: &Vec<dap::StackFrame>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.entries.splice(
|
||||
ix..ix + 1,
|
||||
stack_frames
|
||||
.iter()
|
||||
.map(|frame| StackFrameEntry::Normal(frame.clone())),
|
||||
);
|
||||
self.list.reset(self.entries.len());
|
||||
cx.notify();
|
||||
pub(crate) fn expand_collapsed_entry(&mut self, ix: usize) {
|
||||
let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
|
||||
return;
|
||||
};
|
||||
let entries = std::mem::take(stack_frames)
|
||||
.into_iter()
|
||||
.map(StackFrameEntry::Normal);
|
||||
self.entries.splice(ix..ix + 1, entries);
|
||||
self.selected_ix = Some(ix);
|
||||
}
|
||||
|
||||
fn render_collapsed_entry(
|
||||
@@ -509,6 +472,7 @@ impl StackFrameList {
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let first_stack_frame = &stack_frames[0];
|
||||
let is_selected = Some(ix) == self.selected_ix;
|
||||
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
@@ -517,11 +481,15 @@ impl StackFrameList {
|
||||
.group("")
|
||||
.id(("stack-frame", first_stack_frame.id))
|
||||
.p_1()
|
||||
.on_click(cx.listener({
|
||||
let stack_frames = stack_frames.clone();
|
||||
move |this, _, _window, cx| {
|
||||
this.expand_collapsed_entry(ix, &stack_frames, cx);
|
||||
}
|
||||
.when(is_selected, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.selected_ix = Some(ix);
|
||||
this.activate_selected_entry(window, cx);
|
||||
}))
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(
|
||||
@@ -544,7 +512,7 @@ impl StackFrameList {
|
||||
|
||||
fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
match &self.entries[ix] {
|
||||
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
|
||||
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
|
||||
StackFrameEntry::Collapsed(stack_frames) => {
|
||||
self.render_collapsed_entry(ix, stack_frames, cx)
|
||||
}
|
||||
@@ -583,15 +551,120 @@ impl StackFrameList {
|
||||
.cursor_default()
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
|
||||
}
|
||||
|
||||
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
|
||||
self.selected_ix = ix;
|
||||
if let Some(ix) = self.selected_ix {
|
||||
self.scroll_handle
|
||||
.scroll_to_item(ix, ScrollStrategy::Center);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.entries.len() == 0 => None,
|
||||
None => Some(0),
|
||||
Some(ix) => {
|
||||
if ix == self.entries.len() - 1 {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(ix + 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.entries.len() == 0 => None,
|
||||
None => Some(self.entries.len() - 1),
|
||||
Some(ix) => {
|
||||
if ix == 0 {
|
||||
Some(self.entries.len() - 1)
|
||||
} else {
|
||||
Some(ix - 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let ix = if self.entries.len() > 0 {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let ix = if self.entries.len() > 0 {
|
||||
Some(self.entries.len() - 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn activate_selected_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(ix) = self.selected_ix else {
|
||||
return;
|
||||
};
|
||||
let Some(entry) = self.entries.get_mut(ix) else {
|
||||
return;
|
||||
};
|
||||
match entry {
|
||||
StackFrameEntry::Normal(stack_frame) => {
|
||||
let stack_frame = stack_frame.clone();
|
||||
self.go_to_stack_frame_inner(stack_frame, window, cx)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix),
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.activate_selected_entry(window, cx);
|
||||
}
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
uniform_list(
|
||||
cx.entity(),
|
||||
"stack-frame-list",
|
||||
self.entries.len(),
|
||||
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.size_full()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for StackFrameList {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.p_1()
|
||||
.child(list(self.list.clone()).size_full())
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.child(self.render_list(window, cx))
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ impl StackTraceView {
|
||||
.filter(|id| Some(**id) != this.selected_stack_frame_id)
|
||||
{
|
||||
this.stack_frame_list.update(cx, |list, cx| {
|
||||
list.select_stack_frame_id(*stack_frame_id, window, cx);
|
||||
list.go_to_stack_frame(*stack_frame_id, window, cx).detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ impl StackTraceView {
|
||||
|this, stack_frame_list, event, window, cx| match event {
|
||||
StackFrameListEvent::BuiltEntries => {
|
||||
this.selected_stack_frame_id =
|
||||
stack_frame_list.read(cx).selected_stack_frame_id();
|
||||
stack_frame_list.read(cx).opened_stack_frame_id();
|
||||
this.update_excerpts(window, cx);
|
||||
}
|
||||
StackFrameListEvent::SelectedStackFrameChanged(selected_frame_id) => {
|
||||
|
||||
@@ -168,7 +168,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
|
||||
.update(cx, |state, _| state.stack_frame_list().clone());
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
|
||||
assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
|
||||
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
|
||||
});
|
||||
});
|
||||
@@ -373,14 +373,14 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
.unwrap();
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
|
||||
assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
|
||||
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
|
||||
});
|
||||
|
||||
// select second stack frame
|
||||
stack_frame_list
|
||||
.update_in(cx, |stack_frame_list, window, cx| {
|
||||
stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
|
||||
stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -388,7 +388,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
cx.run_until_parked();
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
assert_eq!(Some(2), stack_frame_list.selected_stack_frame_id());
|
||||
assert_eq!(Some(2), stack_frame_list.opened_stack_frame_id());
|
||||
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
|
||||
});
|
||||
|
||||
@@ -718,11 +718,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
stack_frame_list.entries()
|
||||
);
|
||||
|
||||
stack_frame_list.expand_collapsed_entry(
|
||||
1,
|
||||
&vec![stack_frames[1].clone(), stack_frames[2].clone()],
|
||||
cx,
|
||||
);
|
||||
stack_frame_list.expand_collapsed_entry(1);
|
||||
|
||||
assert_eq!(
|
||||
&vec![
|
||||
@@ -739,11 +735,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
stack_frame_list.entries()
|
||||
);
|
||||
|
||||
stack_frame_list.expand_collapsed_entry(
|
||||
4,
|
||||
&vec![stack_frames[4].clone(), stack_frames[5].clone()],
|
||||
cx,
|
||||
);
|
||||
stack_frame_list.expand_collapsed_entry(4);
|
||||
|
||||
assert_eq!(
|
||||
&vec![
|
||||
|
||||
@@ -190,7 +190,7 @@ async fn test_basic_fetch_initial_scope_and_variables(
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
running_state.stack_frame_list().update(cx, |list, _| {
|
||||
(list.flatten_entries(true), list.selected_stack_frame_id())
|
||||
(list.flatten_entries(true), list.opened_stack_frame_id())
|
||||
});
|
||||
|
||||
assert_eq!(stack_frames, stack_frame_list);
|
||||
@@ -431,7 +431,7 @@ async fn test_fetch_variables_for_multiple_scopes(
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
running_state.stack_frame_list().update(cx, |list, _| {
|
||||
(list.flatten_entries(true), list.selected_stack_frame_id())
|
||||
(list.flatten_entries(true), list.opened_stack_frame_id())
|
||||
});
|
||||
|
||||
assert_eq!(Some(1), stack_frame_id);
|
||||
@@ -1452,7 +1452,7 @@ async fn test_variable_list_only_sends_requests_when_rendering(
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
running_state.stack_frame_list().update(cx, |list, _| {
|
||||
(list.flatten_entries(true), list.selected_stack_frame_id())
|
||||
(list.flatten_entries(true), list.opened_stack_frame_id())
|
||||
});
|
||||
|
||||
assert_eq!(Some(1), stack_frame_id);
|
||||
@@ -1734,7 +1734,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
running_state.stack_frame_list().update(cx, |list, _| {
|
||||
(list.flatten_entries(true), list.selected_stack_frame_id())
|
||||
(list.flatten_entries(true), list.opened_stack_frame_id())
|
||||
});
|
||||
|
||||
let variable_list = running_state.variable_list().read(cx);
|
||||
@@ -1745,7 +1745,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
|
||||
running_state
|
||||
.stack_frame_list()
|
||||
.read(cx)
|
||||
.selected_stack_frame_id(),
|
||||
.opened_stack_frame_id(),
|
||||
Some(1)
|
||||
);
|
||||
|
||||
@@ -1778,7 +1778,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
|
||||
running_state
|
||||
.stack_frame_list()
|
||||
.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
|
||||
stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
@@ -1789,7 +1789,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
running_state.stack_frame_list().update(cx, |list, _| {
|
||||
(list.flatten_entries(true), list.selected_stack_frame_id())
|
||||
(list.flatten_entries(true), list.opened_stack_frame_id())
|
||||
});
|
||||
|
||||
let variable_list = running_state.variable_list().read(cx);
|
||||
|
||||
@@ -17918,10 +17918,6 @@ impl Editor {
|
||||
.and_then(|lines| lines.last().map(|line| line.range.start));
|
||||
|
||||
self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| {
|
||||
let snapshot = editor
|
||||
.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
|
||||
.ok()?;
|
||||
|
||||
let inline_values = editor
|
||||
.update(cx, |editor, cx| {
|
||||
let Some(current_execution_position) = current_execution_position else {
|
||||
@@ -17949,22 +17945,40 @@ impl Editor {
|
||||
.context("refreshing debugger inlays")
|
||||
.log_err()?;
|
||||
|
||||
let (excerpt_id, buffer_id) = snapshot
|
||||
.excerpts()
|
||||
.next()
|
||||
.map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?;
|
||||
let mut buffer_inline_values: HashMap<BufferId, Vec<InlayHint>> = HashMap::default();
|
||||
|
||||
for (buffer_id, inline_value) in inline_values
|
||||
.into_iter()
|
||||
.filter_map(|hint| Some((hint.position.buffer_id?, hint)))
|
||||
{
|
||||
buffer_inline_values
|
||||
.entry(buffer_id)
|
||||
.or_default()
|
||||
.push(inline_value);
|
||||
}
|
||||
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let new_inlays = inline_values
|
||||
.into_iter()
|
||||
.map(|debugger_value| {
|
||||
Inlay::debugger_hint(
|
||||
post_inc(&mut editor.next_inlay_id),
|
||||
Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position),
|
||||
debugger_value.text(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = editor.buffer.read(cx).snapshot(cx);
|
||||
let mut new_inlays = Vec::default();
|
||||
|
||||
for (excerpt_id, buffer_snapshot, _) in snapshot.excerpts() {
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
buffer_inline_values
|
||||
.get(&buffer_id)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|hint| {
|
||||
let inlay = Inlay::debugger_hint(
|
||||
post_inc(&mut editor.next_inlay_id),
|
||||
Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
|
||||
hint.text(),
|
||||
);
|
||||
|
||||
new_inlays.push(inlay);
|
||||
});
|
||||
}
|
||||
|
||||
let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect();
|
||||
std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids);
|
||||
|
||||
|
||||
@@ -27,6 +27,19 @@ pub trait FluentBuilder {
|
||||
self.map(|this| if condition { then(this) } else { this })
|
||||
}
|
||||
|
||||
/// Conditionally modify self with the given closure.
|
||||
fn when_else(
|
||||
self,
|
||||
condition: bool,
|
||||
then: impl FnOnce(Self) -> Self,
|
||||
else_fn: impl FnOnce(Self) -> Self,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.map(|this| if condition { then(this) } else { else_fn(this) })
|
||||
}
|
||||
|
||||
/// Conditionally unwrap and modify self with the given closure, if the given option is Some.
|
||||
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
|
||||
where
|
||||
|
||||
@@ -83,6 +83,13 @@ impl EditPredictionUsage {
|
||||
|
||||
Ok(Self { limit, amount })
|
||||
}
|
||||
|
||||
pub fn over_limit(&self) -> bool {
|
||||
match self.limit {
|
||||
UsageLimit::Limited(limit) => self.amount >= limit,
|
||||
UsageLimit::Unlimited => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EditPredictionProvider: 'static + Sized {
|
||||
|
||||
@@ -33,7 +33,7 @@ use workspace::{
|
||||
StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
|
||||
notifications::NotificationId,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_actions::{OpenBrowser, OpenZedUrl};
|
||||
use zed_llm_client::UsageLimit;
|
||||
use zeta::RateCompletions;
|
||||
|
||||
@@ -277,14 +277,31 @@ impl Render for InlineCompletionButton {
|
||||
);
|
||||
}
|
||||
|
||||
let mut over_limit = false;
|
||||
|
||||
if let Some(usage) = self
|
||||
.edit_prediction_provider
|
||||
.as_ref()
|
||||
.and_then(|provider| provider.usage(cx))
|
||||
{
|
||||
over_limit = usage.over_limit()
|
||||
}
|
||||
|
||||
let show_editor_predictions = self.editor_show_predictions;
|
||||
|
||||
let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
|
||||
.shape(IconButtonShape::Square)
|
||||
.when(enabled && !show_editor_predictions, |this| {
|
||||
this.indicator(Indicator::dot().color(Color::Muted))
|
||||
.when(
|
||||
enabled && (!show_editor_predictions || over_limit),
|
||||
|this| {
|
||||
this.indicator(Indicator::dot().when_else(
|
||||
over_limit,
|
||||
|dot| dot.color(Color::Error),
|
||||
|dot| dot.color(Color::Muted),
|
||||
))
|
||||
.indicator_border_color(Some(cx.theme().colors().status_bar_background))
|
||||
})
|
||||
},
|
||||
)
|
||||
.when(!self.popover_menu_handle.is_deployed(), |element| {
|
||||
element.tooltip(move |window, cx| {
|
||||
if enabled {
|
||||
@@ -440,6 +457,16 @@ impl InlineCompletionButton {
|
||||
},
|
||||
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
|
||||
)
|
||||
.when(usage.over_limit(), |menu| -> ContextMenu {
|
||||
menu.entry("Subscribe to increase your limit", None, |window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(OpenZedUrl {
|
||||
url: zed_urls::account_url(cx),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.separator();
|
||||
}
|
||||
|
||||
|
||||
@@ -326,8 +326,14 @@ struct GroupedModels {
|
||||
|
||||
impl GroupedModels {
|
||||
pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
|
||||
let recommended_ids: HashSet<_> = recommended.iter().map(|info| info.model.id()).collect();
|
||||
|
||||
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
|
||||
for model in other {
|
||||
if recommended_ids.contains(&model.model.id()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let provider = model.model.provider_id();
|
||||
if let Some(models) = other_by_provider.get_mut(&provider) {
|
||||
models.push(model);
|
||||
@@ -889,4 +895,26 @@ mod tests {
|
||||
let results = matcher.fuzzy_search("z4n");
|
||||
assert_models_eq(results, vec!["zed/gpt-4.1-nano"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_exclude_recommended_models(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models(vec![
|
||||
("zed", "claude"), // Should be filtered out from "other"
|
||||
("zed", "gemini"),
|
||||
("copilot", "o3"),
|
||||
]);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
|
||||
let actual_other_models = grouped_models
|
||||
.other
|
||||
.values()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Recommended models should not appear in "other"
|
||||
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/o3"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,6 +628,7 @@ impl GoogleEventMapper {
|
||||
// responds with `finish_reason: STOP`
|
||||
if wants_to_use_tool {
|
||||
self.stop_reason = StopReason::ToolUse;
|
||||
events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
@@ -477,14 +477,14 @@ fn add_message_content_part(
|
||||
_ => {
|
||||
messages.push(match role {
|
||||
Role::User => open_ai::RequestMessage::User {
|
||||
content: open_ai::MessageContent::empty(),
|
||||
content: open_ai::MessageContent::from(vec![new_part]),
|
||||
},
|
||||
Role::Assistant => open_ai::RequestMessage::Assistant {
|
||||
content: open_ai::MessageContent::empty(),
|
||||
content: open_ai::MessageContent::from(vec![new_part]),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => open_ai::RequestMessage::System {
|
||||
content: open_ai::MessageContent::empty(),
|
||||
content: open_ai::MessageContent::from(vec![new_part]),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -118,22 +118,19 @@ pub enum DirenvSettings {
|
||||
Direct,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct DiagnosticsSettings {
|
||||
/// Whether to show the project diagnostics button in the status bar.
|
||||
#[serde(default = "default_true")]
|
||||
pub button: bool,
|
||||
|
||||
/// Whether or not to include warning diagnostics.
|
||||
#[serde(default = "default_true")]
|
||||
pub include_warnings: bool,
|
||||
|
||||
/// Settings for showing inline diagnostics.
|
||||
#[serde(default)]
|
||||
pub inline: InlineDiagnosticsSettings,
|
||||
|
||||
/// Configuration, related to Rust language diagnostics.
|
||||
#[serde(default)]
|
||||
pub cargo: Option<CargoDiagnosticsSettings>,
|
||||
}
|
||||
|
||||
@@ -146,33 +143,29 @@ impl DiagnosticsSettings {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct InlineDiagnosticsSettings {
|
||||
/// Whether or not to show inline diagnostics
|
||||
///
|
||||
/// Default: false
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
/// Whether to only show the inline diagnostics after a delay after the
|
||||
/// last editor event.
|
||||
///
|
||||
/// Default: 150
|
||||
#[serde(default = "default_inline_diagnostics_debounce_ms")]
|
||||
pub update_debounce_ms: u64,
|
||||
/// The amount of padding between the end of the source line and the start
|
||||
/// of the inline diagnostic in units of columns.
|
||||
///
|
||||
/// Default: 4
|
||||
#[serde(default = "default_inline_diagnostics_padding")]
|
||||
pub padding: u32,
|
||||
/// The minimum column to display inline diagnostics. This setting can be
|
||||
/// used to horizontally align inline diagnostics at some position. Lines
|
||||
/// longer than this value will still push diagnostics further to the right.
|
||||
///
|
||||
/// Default: 0
|
||||
#[serde(default)]
|
||||
pub min_column: u32,
|
||||
|
||||
#[serde(default)]
|
||||
pub max_severity: Option<DiagnosticSeverity>,
|
||||
}
|
||||
|
||||
@@ -211,24 +204,27 @@ impl DiagnosticSeverity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InlineDiagnosticsSettings {
|
||||
impl Default for DiagnosticsSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
update_debounce_ms: default_inline_diagnostics_debounce_ms(),
|
||||
padding: default_inline_diagnostics_padding(),
|
||||
min_column: 0,
|
||||
max_severity: None,
|
||||
button: true,
|
||||
include_warnings: true,
|
||||
inline: Default::default(),
|
||||
cargo: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_inline_diagnostics_debounce_ms() -> u64 {
|
||||
150
|
||||
}
|
||||
|
||||
fn default_inline_diagnostics_padding() -> u32 {
|
||||
4
|
||||
impl Default for InlineDiagnosticsSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
update_debounce_ms: 150,
|
||||
padding: 4,
|
||||
min_column: 0,
|
||||
max_severity: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -37,7 +37,7 @@ use ui::{
|
||||
Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize,
|
||||
Toggleable, Tooltip, h_flex, prelude::*, utils::SearchInputWidth, v_flex,
|
||||
};
|
||||
use util::paths::PathMatcher;
|
||||
use util::{ResultExt as _, paths::PathMatcher};
|
||||
use workspace::{
|
||||
DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation,
|
||||
ToolbarItemView, Workspace, WorkspaceId,
|
||||
@@ -72,15 +72,18 @@ pub fn init(cx: &mut App) {
|
||||
);
|
||||
register_workspace_action(
|
||||
workspace,
|
||||
move |search_bar, _: &ToggleCaseSensitive, _, cx| {
|
||||
search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
|
||||
move |search_bar, _: &ToggleCaseSensitive, window, cx| {
|
||||
search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
|
||||
},
|
||||
);
|
||||
register_workspace_action(workspace, move |search_bar, _: &ToggleWholeWord, _, cx| {
|
||||
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
|
||||
});
|
||||
register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, _, cx| {
|
||||
search_bar.toggle_search_option(SearchOptions::REGEX, cx);
|
||||
register_workspace_action(
|
||||
workspace,
|
||||
move |search_bar, _: &ToggleWholeWord, window, cx| {
|
||||
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
|
||||
},
|
||||
);
|
||||
register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, window, cx| {
|
||||
search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
|
||||
});
|
||||
register_workspace_action(
|
||||
workspace,
|
||||
@@ -1032,6 +1035,61 @@ impl ProjectSearchView {
|
||||
});
|
||||
}
|
||||
|
||||
fn prompt_to_save_if_dirty_then_search(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
use workspace::AutosaveSetting;
|
||||
|
||||
let project = self.entity.read(cx).project.clone();
|
||||
|
||||
let can_autosave = self.results_editor.can_autosave(cx);
|
||||
let autosave_setting = self.results_editor.workspace_settings(cx).autosave;
|
||||
|
||||
let will_autosave = can_autosave
|
||||
&& matches!(
|
||||
autosave_setting,
|
||||
AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
|
||||
);
|
||||
|
||||
let is_dirty = self.is_dirty(cx);
|
||||
|
||||
let should_confirm_save = !will_autosave && is_dirty;
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let should_search = if should_confirm_save {
|
||||
let options = &["Save", "Don't Save", "Cancel"];
|
||||
let result_channel = this.update_in(cx, |_, window, cx| {
|
||||
window.prompt(
|
||||
gpui::PromptLevel::Warning,
|
||||
"Project search buffer contains unsaved edits. Do you want to save it?",
|
||||
None,
|
||||
options,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
let result = result_channel.await?;
|
||||
let should_save = result == 0;
|
||||
if should_save {
|
||||
this.update_in(cx, |this, window, cx| this.save(true, project, window, cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
let should_search = result != 2;
|
||||
should_search
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if should_search {
|
||||
this.update(cx, |this, cx| {
|
||||
this.search(cx);
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn search(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(query) = self.build_search_query(cx) {
|
||||
self.entity.update(cx, |model, cx| model.search(query, cx));
|
||||
@@ -1503,7 +1561,9 @@ impl ProjectSearchBar {
|
||||
.is_focused(window)
|
||||
{
|
||||
cx.stop_propagation();
|
||||
search_view.search(cx);
|
||||
search_view
|
||||
.prompt_to_save_if_dirty_then_search(window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1570,19 +1630,39 @@ impl ProjectSearchBar {
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut Context<Self>) -> bool {
|
||||
if let Some(search_view) = self.active_project_search.as_ref() {
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
search_view.toggle_search_option(option, cx);
|
||||
if search_view.entity.read(cx).active_query.is_some() {
|
||||
search_view.search(cx);
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
fn toggle_search_option(
|
||||
&mut self,
|
||||
option: SearchOptions,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
if self.active_project_search.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task = this.update_in(cx, |this, window, cx| {
|
||||
let search_view = this.active_project_search.as_ref()?;
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
search_view.toggle_search_option(option, cx);
|
||||
search_view
|
||||
.entity
|
||||
.read(cx)
|
||||
.active_query
|
||||
.is_some()
|
||||
.then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx))
|
||||
})
|
||||
})?;
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
}
|
||||
this.update(cx, |_, cx| {
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
true
|
||||
}
|
||||
|
||||
fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -1621,19 +1701,33 @@ impl ProjectSearchBar {
|
||||
}
|
||||
|
||||
fn toggle_opened_only(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||
if let Some(search_view) = self.active_project_search.as_ref() {
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
search_view.toggle_opened_only(window, cx);
|
||||
if search_view.entity.read(cx).active_query.is_some() {
|
||||
search_view.search(cx);
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
if self.active_project_search.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task = this.update_in(cx, |this, window, cx| {
|
||||
let search_view = this.active_project_search.as_ref()?;
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
search_view.toggle_opened_only(window, cx);
|
||||
search_view
|
||||
.entity
|
||||
.read(cx)
|
||||
.active_query
|
||||
.is_some()
|
||||
.then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx))
|
||||
})
|
||||
})?;
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
}
|
||||
this.update(cx, |_, cx| {
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
true
|
||||
}
|
||||
|
||||
fn is_opened_only_enabled(&self, cx: &App) -> bool {
|
||||
@@ -1860,22 +1954,22 @@ impl Render for ProjectSearchBar {
|
||||
.child(SearchOptions::CASE_SENSITIVE.as_button(
|
||||
self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx),
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, _, cx| {
|
||||
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
|
||||
}),
|
||||
))
|
||||
.child(SearchOptions::WHOLE_WORD.as_button(
|
||||
self.is_option_enabled(SearchOptions::WHOLE_WORD, cx),
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, _, cx| {
|
||||
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
|
||||
}),
|
||||
))
|
||||
.child(SearchOptions::REGEX.as_button(
|
||||
self.is_option_enabled(SearchOptions::REGEX, cx),
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, _, cx| {
|
||||
this.toggle_search_option(SearchOptions::REGEX, cx);
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_search_option(SearchOptions::REGEX, window, cx);
|
||||
}),
|
||||
)),
|
||||
);
|
||||
@@ -2147,8 +2241,12 @@ impl Render for ProjectSearchBar {
|
||||
.search_options
|
||||
.contains(SearchOptions::INCLUDE_IGNORED),
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, _, cx| {
|
||||
this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx);
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_search_option(
|
||||
SearchOptions::INCLUDE_IGNORED,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -2188,11 +2286,11 @@ impl Render for ProjectSearchBar {
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.toggle_replace(action, window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ToggleWholeWord, _, cx| {
|
||||
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
|
||||
.on_action(cx.listener(|this, _: &ToggleWholeWord, window, cx| {
|
||||
this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ToggleCaseSensitive, _, cx| {
|
||||
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
|
||||
.on_action(cx.listener(|this, _: &ToggleCaseSensitive, window, cx| {
|
||||
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
if let Some(search) = this.active_project_search.as_ref() {
|
||||
@@ -2209,8 +2307,8 @@ impl Render for ProjectSearchBar {
|
||||
}
|
||||
}))
|
||||
.when(search.filters_enabled, |this| {
|
||||
this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, _, cx| {
|
||||
this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx);
|
||||
this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, window, cx| {
|
||||
this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx);
|
||||
}))
|
||||
})
|
||||
.on_action(cx.listener(Self::select_next_match))
|
||||
|
||||
@@ -2,50 +2,48 @@ use db::anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use util::serde::default_true;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct TitleBarSettings {
|
||||
#[serde(default)]
|
||||
pub show_branch_icon: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_branch_name: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_project_items: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_onboarding_banner: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_user_picture: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct TitleBarSettingsContent {
|
||||
/// Whether to show the branch icon beside branch switcher in the title bar.
|
||||
///
|
||||
/// Default: false
|
||||
pub show_branch_icon: Option<bool>,
|
||||
pub show_branch_icon: bool,
|
||||
/// Whether to show onboarding banners in the title bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_onboarding_banner: Option<bool>,
|
||||
pub show_onboarding_banner: bool,
|
||||
/// Whether to show user avatar in the title bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_user_picture: Option<bool>,
|
||||
pub show_user_picture: bool,
|
||||
/// Whether to show the branch name button in the titlebar.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_branch_name: Option<bool>,
|
||||
pub show_branch_name: bool,
|
||||
/// Whether to show the project host and name in the titlebar.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_project_items: Option<bool>,
|
||||
pub show_project_items: bool,
|
||||
}
|
||||
|
||||
impl Default for TitleBarSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_branch_icon: false,
|
||||
show_onboarding_banner: true,
|
||||
show_user_picture: true,
|
||||
show_branch_name: true,
|
||||
show_project_items: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for TitleBarSettings {
|
||||
const KEY: Option<&'static str> = Some("title_bar");
|
||||
|
||||
type FileContent = TitleBarSettingsContent;
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut gpui::App) -> anyhow::Result<Self>
|
||||
where
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
use gpui::{ClickEvent, Corner, CursorStyle, Entity, MouseButton};
|
||||
use gpui::{ClickEvent, Corner, CursorStyle, Entity, Hsla, MouseButton};
|
||||
|
||||
use crate::{ContextMenu, PopoverMenu, prelude::*};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum DropdownStyle {
|
||||
#[default]
|
||||
Solid,
|
||||
Ghost,
|
||||
}
|
||||
|
||||
enum LabelKind {
|
||||
Text(SharedString),
|
||||
Element(AnyElement),
|
||||
@@ -11,6 +18,7 @@ enum LabelKind {
|
||||
pub struct DropdownMenu {
|
||||
id: ElementId,
|
||||
label: LabelKind,
|
||||
style: DropdownStyle,
|
||||
menu: Entity<ContextMenu>,
|
||||
full_width: bool,
|
||||
disabled: bool,
|
||||
@@ -25,6 +33,7 @@ impl DropdownMenu {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label: LabelKind::Text(label.into()),
|
||||
style: DropdownStyle::default(),
|
||||
menu,
|
||||
full_width: false,
|
||||
disabled: false,
|
||||
@@ -39,12 +48,18 @@ impl DropdownMenu {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label: LabelKind::Element(label),
|
||||
style: DropdownStyle::default(),
|
||||
menu,
|
||||
full_width: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: DropdownStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn full_width(mut self, full_width: bool) -> Self {
|
||||
self.full_width = full_width;
|
||||
self
|
||||
@@ -66,7 +81,8 @@ impl RenderOnce for DropdownMenu {
|
||||
.trigger(
|
||||
DropdownMenuTrigger::new(self.label)
|
||||
.full_width(self.full_width)
|
||||
.disabled(self.disabled),
|
||||
.disabled(self.disabled)
|
||||
.style(self.style),
|
||||
)
|
||||
.attach(Corner::BottomLeft)
|
||||
}
|
||||
@@ -135,12 +151,35 @@ impl Component for DropdownMenu {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DropdownTriggerStyle {
|
||||
pub bg: Hsla,
|
||||
}
|
||||
|
||||
impl DropdownTriggerStyle {
|
||||
pub fn for_style(style: DropdownStyle, cx: &App) -> Self {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
if style == DropdownStyle::Solid {
|
||||
Self {
|
||||
// why is this editor_background?
|
||||
bg: colors.editor_background,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
bg: colors.ghost_element_background,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct DropdownMenuTrigger {
|
||||
label: LabelKind,
|
||||
full_width: bool,
|
||||
selected: bool,
|
||||
disabled: bool,
|
||||
style: DropdownStyle,
|
||||
cursor_style: CursorStyle,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
@@ -152,6 +191,7 @@ impl DropdownMenuTrigger {
|
||||
full_width: false,
|
||||
selected: false,
|
||||
disabled: false,
|
||||
style: DropdownStyle::default(),
|
||||
cursor_style: CursorStyle::default(),
|
||||
on_click: None,
|
||||
}
|
||||
@@ -161,6 +201,11 @@ impl DropdownMenuTrigger {
|
||||
self.full_width = full_width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: DropdownStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for DropdownMenuTrigger {
|
||||
@@ -193,11 +238,13 @@ impl RenderOnce for DropdownMenuTrigger {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let disabled = self.disabled;
|
||||
|
||||
let style = DropdownTriggerStyle::for_style(self.style, cx);
|
||||
|
||||
h_flex()
|
||||
.id("dropdown-menu-trigger")
|
||||
.justify_between()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.bg(style.bg)
|
||||
.pl_2()
|
||||
.pr_1p5()
|
||||
.py_0p5()
|
||||
|
||||
@@ -13,6 +13,7 @@ pub struct ProgressBar {
|
||||
value: f32,
|
||||
max_value: f32,
|
||||
bg_color: Hsla,
|
||||
over_color: Hsla,
|
||||
fg_color: Hsla,
|
||||
}
|
||||
|
||||
@@ -23,6 +24,7 @@ impl ProgressBar {
|
||||
value,
|
||||
max_value,
|
||||
bg_color: cx.theme().colors().background,
|
||||
over_color: cx.theme().status().error,
|
||||
fg_color: cx.theme().status().info,
|
||||
}
|
||||
}
|
||||
@@ -50,6 +52,12 @@ impl ProgressBar {
|
||||
self.fg_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the over limit color of the progress bar.
|
||||
pub fn over_color(mut self, color: Hsla) -> Self {
|
||||
self.over_color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ProgressBar {
|
||||
@@ -74,7 +82,8 @@ impl RenderOnce for ProgressBar {
|
||||
div()
|
||||
.h_full()
|
||||
.rounded_full()
|
||||
.bg(self.fg_color)
|
||||
.when(self.value > self.max_value, |div| div.bg(self.over_color))
|
||||
.when(self.value <= self.max_value, |div| div.bg(self.fg_color))
|
||||
.w(relative(fill_width)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -564,6 +564,10 @@ pub trait ItemHandle: 'static + Send {
|
||||
fn preserve_preview(&self, cx: &App) -> bool;
|
||||
fn include_in_nav_history(&self) -> bool;
|
||||
fn relay_action(&self, action: Box<dyn Action>, window: &mut Window, cx: &mut App);
|
||||
fn can_autosave(&self, cx: &App) -> bool {
|
||||
let is_deleted = self.project_entry_ids(cx).is_empty();
|
||||
self.is_dirty(cx) && !self.has_conflict(cx) && self.can_save(cx) && !is_deleted
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WeakItemHandle: Send + Sync {
|
||||
|
||||
@@ -1857,7 +1857,7 @@ impl Pane {
|
||||
matches!(
|
||||
item.workspace_settings(cx).autosave,
|
||||
AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
|
||||
) && Self::can_autosave_item(item, cx)
|
||||
) && item.can_autosave(cx)
|
||||
})?;
|
||||
if !will_autosave {
|
||||
let item_id = item.item_id();
|
||||
@@ -1945,11 +1945,6 @@ impl Pane {
|
||||
})
|
||||
}
|
||||
|
||||
fn can_autosave_item(item: &dyn ItemHandle, cx: &App) -> bool {
|
||||
let is_deleted = item.project_entry_ids(cx).is_empty();
|
||||
item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
|
||||
}
|
||||
|
||||
pub fn autosave_item(
|
||||
item: &dyn ItemHandle,
|
||||
project: Entity<Project>,
|
||||
@@ -1960,7 +1955,7 @@ impl Pane {
|
||||
item.workspace_settings(cx).autosave,
|
||||
AutosaveSetting::AfterDelay { .. }
|
||||
);
|
||||
if Self::can_autosave_item(item, cx) {
|
||||
if item.can_autosave(cx) {
|
||||
item.save(format, project, window, cx)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.187.0"
|
||||
version = "0.187.4"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
@@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};
|
||||
|
||||
use crate::{ZED_PREDICT_DATA_COLLECTION_CHOICE, onboarding_event};
|
||||
use anyhow::Context as _;
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use client::{Client, UserStore};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
@@ -384,47 +384,29 @@ impl Render for ZedPredictModal {
|
||||
} else {
|
||||
(IconName::ChevronDown, IconName::ChevronUp)
|
||||
};
|
||||
let plan = plan.unwrap_or(proto::Plan::Free);
|
||||
|
||||
base.child(Label::new(copy).color(Color::Muted))
|
||||
.child(h_flex().map(|parent| {
|
||||
if let Some(plan) = plan {
|
||||
parent.child(
|
||||
Checkbox::new("plan", ToggleState::Selected)
|
||||
.fill()
|
||||
.disabled(true)
|
||||
.label(format!(
|
||||
"You get {} edit predictions through your {}.",
|
||||
if plan == proto::Plan::Free {
|
||||
"2,000"
|
||||
} else {
|
||||
"unlimited"
|
||||
},
|
||||
match plan {
|
||||
proto::Plan::Free => "Zed Free plan",
|
||||
proto::Plan::ZedPro => "Zed Pro plan",
|
||||
proto::Plan::ZedProTrial => "Zed Pro trial",
|
||||
}
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
parent
|
||||
.child(
|
||||
Checkbox::new("plan-required", ToggleState::Unselected)
|
||||
.fill()
|
||||
.disabled(true)
|
||||
.label("To get started with edit prediction"),
|
||||
)
|
||||
.child(
|
||||
Button::new("subscribe", "choose a plan")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(|_event, _window, cx| {
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
}),
|
||||
)
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
h_flex().child(
|
||||
Checkbox::new("plan", ToggleState::Selected)
|
||||
.fill()
|
||||
.disabled(true)
|
||||
.label(format!(
|
||||
"You get {} edit predictions through your {}.",
|
||||
if plan == proto::Plan::Free {
|
||||
"2,000"
|
||||
} else {
|
||||
"unlimited"
|
||||
},
|
||||
match plan {
|
||||
proto::Plan::Free => "Zed Free plan",
|
||||
proto::Plan::ZedPro => "Zed Pro plan",
|
||||
proto::Plan::ZedProTrial => "Zed Pro trial",
|
||||
}
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
@@ -495,7 +477,7 @@ impl Render for ZedPredictModal {
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("accept-tos", "Enable Edit Prediction")
|
||||
.disabled(plan.is_none() || !self.terms_of_service)
|
||||
.disabled(!self.terms_of_service)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::accept_and_enable)),
|
||||
|
||||
Reference in New Issue
Block a user