ZIm/crates/zeta/src/rate_completion_modal.rs
Remco Smits 41a60ffecf
Debugger implementation (#13433)
###  DISCLAIMER

> As of 6th March 2025, debugger is still in development. We plan to
merge it behind a staff-only feature flag for staff use only, followed
by non-public release and then finally a public one (akin to how Git
panel release was handled). This is done to ensure the best experience
when it gets released.

### END OF DISCLAIMER 

**The current state of the debugger implementation:**


https://github.com/user-attachments/assets/c4deff07-80dd-4dc6-ad2e-0c252a478fe9


https://github.com/user-attachments/assets/e1ed2345-b750-4bb6-9c97-50961b76904f

----

All the todo's are in the following channel, so it's easier to work on
this together:
https://zed.dev/channel/zed-debugger-11370

If you are on Linux, you can use the following command to join the
channel:
```cli
zed https://zed.dev/channel/zed-debugger-11370 
```

## Current Features

- Collab
  - Breakpoints
    - Sync when you (re)join a project
    - Sync when you add/remove a breakpoint
  - Sync active debug line
  - Stack frames
    - Click on stack frame
      - View variables that belong to the stack frame
      - Visit the source file
    - Restart stack frame (if adapter supports this)
  - Variables
  - Loaded sources
  - Modules
  - Controls
    - Continue
    - Step back
      - Stepping granularity (configurable)
    - Step into
      - Stepping granularity (configurable)
    - Step over
      - Stepping granularity (configurable)
    - Step out
      - Stepping granularity (configurable)
  - Debug console
- Breakpoints
  - Log breakpoints
  - line breakpoints
  - Persistent between zed sessions (configurable)
  - Multi buffer support
  - Toggle disable/enable all breakpoints
- Stack frames
  - Click on stack frame
    - View variables that belong to the stack frame
    - Visit the source file
    - Show collapsed stack frames
  - Restart stack frame (if adapter supports this)
- Loaded sources
  - View all used loaded sources if supported by adapter.
- Modules
  - View all used modules (if adapter supports this)
- Variables
  - Copy value
  - Copy name
  - Copy memory reference
  - Set value (if adapter supports this)
  - keyboard navigation
- Debug Console
  - See logs
  - View output that was sent from debug adapter
    - Output grouping
  - Evaluate code
    - Updates the variable list
    - Auto completion
- If not supported by adapter, we will show auto-completion for existing
variables
- Debug Terminal
- Run custom commands and change env values right inside your Zed
terminal
- Attach to process (if adapter supports this)
  - Process picker
- Controls
  - Continue
  - Step back
    - Stepping granularity (configurable)
  - Step into
    - Stepping granularity (configurable)
  - Step over
    - Stepping granularity (configurable)
  - Step out
    - Stepping granularity (configurable)
  - Disconnect
  - Restart
  - Stop
- Warning when a debug session exited without hitting any breakpoint
- Debug view to see Adapter/RPC log messages
- Testing
  - Fake debug adapter
    - Fake requests & events

---

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
Co-authored-by: Piotr <piotr@zed.dev>
2025-03-18 12:55:25 -04:00

681 lines
27 KiB
Rust

use crate::{CompletionDiffElement, InlineCompletion, InlineCompletionRating, Zeta};
use editor::Editor;
use gpui::{actions, prelude::*, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable};
use language::language_settings;
use std::time::Duration;
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, Tooltip};
use workspace::{ModalView, Workspace};
actions!(
zeta,
[
ThumbsUpActiveCompletion,
ThumbsDownActiveCompletion,
NextEdit,
PreviousEdit,
FocusCompletions,
PreviewCompletion,
]
);
pub struct RateCompletionModal {
zeta: Entity<Zeta>,
active_completion: Option<ActiveCompletion>,
selected_index: usize,
focus_handle: FocusHandle,
_subscription: gpui::Subscription,
current_view: RateCompletionView,
}
struct ActiveCompletion {
completion: InlineCompletion,
feedback_editor: Entity<Editor>,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
enum RateCompletionView {
SuggestedEdits,
RawInput,
}
impl RateCompletionView {
pub fn name(&self) -> &'static str {
match self {
Self::SuggestedEdits => "Suggested Edits",
Self::RawInput => "Recorded Events & Input",
}
}
}
impl RateCompletionModal {
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
if let Some(zeta) = Zeta::global(cx) {
workspace.toggle_modal(window, cx, |_window, cx| RateCompletionModal::new(zeta, cx));
telemetry::event!("Rate Completion Modal Open", source = "Edit Prediction");
}
}
pub fn new(zeta: Entity<Zeta>, cx: &mut Context<Self>) -> Self {
let subscription = cx.observe(&zeta, |_, _, cx| cx.notify());
Self {
zeta,
selected_index: 0,
focus_handle: cx.focus_handle(),
active_completion: None,
_subscription: subscription,
current_view: RateCompletionView::SuggestedEdits,
}
}
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
fn select_next(&mut self, _: &menu::SelectNext, _: &mut Window, cx: &mut Context<Self>) {
self.selected_index += 1;
self.selected_index = usize::min(
self.selected_index,
self.zeta.read(cx).shown_completions().count(),
);
cx.notify();
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.selected_index = self.selected_index.saturating_sub(1);
cx.notify();
}
fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {
let next_index = self
.zeta
.read(cx)
.shown_completions()
.skip(self.selected_index)
.enumerate()
.skip(1) // Skip straight to the next item
.find(|(_, completion)| !completion.edits.is_empty())
.map(|(ix, _)| ix + self.selected_index);
if let Some(next_index) = next_index {
self.selected_index = next_index;
cx.notify();
}
}
fn select_prev_edit(&mut self, _: &PreviousEdit, _: &mut Window, cx: &mut Context<Self>) {
let zeta = self.zeta.read(cx);
let completions_len = zeta.shown_completions_len();
let prev_index = self
.zeta
.read(cx)
.shown_completions()
.rev()
.skip((completions_len - 1) - self.selected_index)
.enumerate()
.skip(1) // Skip straight to the previous item
.find(|(_, completion)| !completion.edits.is_empty())
.map(|(ix, _)| self.selected_index - ix);
if let Some(prev_index) = prev_index {
self.selected_index = prev_index;
cx.notify();
}
cx.notify();
}
fn select_first(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
self.selected_index = 0;
cx.notify();
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
self.selected_index = self.zeta.read(cx).shown_completions_len() - 1;
cx.notify();
}
pub fn thumbs_up_active(
&mut self,
_: &ThumbsUpActiveCompletion,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.zeta.update(cx, |zeta, cx| {
if let Some(active) = &self.active_completion {
zeta.rate_completion(
&active.completion,
InlineCompletionRating::Positive,
active.feedback_editor.read(cx).text(cx),
cx,
);
}
});
let current_completion = self
.active_completion
.as_ref()
.map(|completion| completion.completion.clone());
self.select_completion(current_completion, false, window, cx);
self.select_next_edit(&Default::default(), window, cx);
self.confirm(&Default::default(), window, cx);
cx.notify();
}
pub fn thumbs_down_active(
&mut self,
_: &ThumbsDownActiveCompletion,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(active) = &self.active_completion {
if active.feedback_editor.read(cx).text(cx).is_empty() {
return;
}
self.zeta.update(cx, |zeta, cx| {
zeta.rate_completion(
&active.completion,
InlineCompletionRating::Negative,
active.feedback_editor.read(cx).text(cx),
cx,
);
});
}
let current_completion = self
.active_completion
.as_ref()
.map(|completion| completion.completion.clone());
self.select_completion(current_completion, false, window, cx);
self.select_next_edit(&Default::default(), window, cx);
self.confirm(&Default::default(), window, cx);
cx.notify();
}
fn focus_completions(
&mut self,
_: &FocusCompletions,
window: &mut Window,
cx: &mut Context<Self>,
) {
cx.focus_self(window);
cx.notify();
}
fn preview_completion(
&mut self,
_: &PreviewCompletion,
window: &mut Window,
cx: &mut Context<Self>,
) {
let completion = self
.zeta
.read(cx)
.shown_completions()
.skip(self.selected_index)
.take(1)
.next()
.cloned();
self.select_completion(completion, false, window, cx);
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let completion = self
.zeta
.read(cx)
.shown_completions()
.skip(self.selected_index)
.take(1)
.next()
.cloned();
self.select_completion(completion, true, window, cx);
}
pub fn select_completion(
&mut self,
completion: Option<InlineCompletion>,
focus: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
// Avoid resetting completion rating if it's already selected.
if let Some(completion) = completion.as_ref() {
self.selected_index = self
.zeta
.read(cx)
.shown_completions()
.enumerate()
.find(|(_, completion_b)| completion.id == completion_b.id)
.map(|(ix, _)| ix)
.unwrap_or(self.selected_index);
cx.notify();
if let Some(prev_completion) = self.active_completion.as_ref() {
if completion.id == prev_completion.completion.id {
if focus {
window.focus(&prev_completion.feedback_editor.focus_handle(cx));
}
return;
}
}
}
self.active_completion = completion.map(|completion| ActiveCompletion {
completion,
feedback_editor: cx.new(|cx| {
let mut editor = Editor::multi_line(window, cx);
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_scrollbars(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_runnables(false, cx);
editor.set_show_breakpoints(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_show_edit_predictions(Some(false), window, cx);
editor.set_placeholder_text("Add your feedback…", cx);
if focus {
cx.focus_self(window);
}
editor
}),
});
cx.notify();
}
fn render_view_nav(&self, cx: &Context<Self>) -> impl IntoElement {
h_flex()
.h_8()
.px_1()
.border_b_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().elevated_surface_background)
.gap_1()
.child(
Button::new(
ElementId::Name("suggested-edits".into()),
RateCompletionView::SuggestedEdits.name(),
)
.label_size(LabelSize::Small)
.on_click(cx.listener(move |this, _, _window, cx| {
this.current_view = RateCompletionView::SuggestedEdits;
cx.notify();
}))
.toggle_state(self.current_view == RateCompletionView::SuggestedEdits),
)
.child(
Button::new(
ElementId::Name("raw-input".into()),
RateCompletionView::RawInput.name(),
)
.label_size(LabelSize::Small)
.on_click(cx.listener(move |this, _, _window, cx| {
this.current_view = RateCompletionView::RawInput;
cx.notify();
}))
.toggle_state(self.current_view == RateCompletionView::RawInput),
)
}
fn render_suggested_edits(&self, cx: &mut Context<Self>) -> Option<gpui::Stateful<Div>> {
let active_completion = self.active_completion.as_ref()?;
let bg_color = cx.theme().colors().editor_background;
Some(
div()
.id("diff")
.p_4()
.size_full()
.bg(bg_color)
.overflow_scroll()
.whitespace_nowrap()
.child(CompletionDiffElement::new(
&active_completion.completion,
cx,
)),
)
}
fn render_raw_input(&self, cx: &mut Context<Self>) -> Option<gpui::Stateful<Div>> {
Some(
v_flex()
.size_full()
.overflow_hidden()
.relative()
.child(
div()
.id("raw-input")
.py_4()
.px_6()
.size_full()
.bg(cx.theme().colors().editor_background)
.overflow_scroll()
.child(if let Some(active_completion) = &self.active_completion {
format!(
"{}\n{}",
active_completion.completion.input_events,
active_completion.completion.input_excerpt
)
} else {
"No active completion".to_string()
}),
)
.id("raw-input-view"),
)
}
fn render_active_completion(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
let active_completion = self.active_completion.as_ref()?;
let completion_id = active_completion.completion.id;
let focus_handle = &self.focus_handle(cx);
let border_color = cx.theme().colors().border;
let bg_color = cx.theme().colors().editor_background;
let rated = self.zeta.read(cx).is_completion_rated(completion_id);
let feedback_empty = active_completion
.feedback_editor
.read(cx)
.text(cx)
.is_empty();
let label_container = h_flex().pl_1().gap_1p5();
Some(
v_flex()
.size_full()
.overflow_hidden()
.relative()
.child(
v_flex()
.size_full()
.overflow_hidden()
.relative()
.child(self.render_view_nav(cx))
.when_some(match self.current_view {
RateCompletionView::SuggestedEdits => self.render_suggested_edits(cx),
RateCompletionView::RawInput => self.render_raw_input(cx),
}, |this, element| this.child(element))
)
.when(!rated, |this| {
this.child(
h_flex()
.p_2()
.gap_2()
.border_y_1()
.border_color(border_color)
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted)
)
.child(
div()
.w_full()
.pr_2()
.flex_wrap()
.child(
Label::new("Explain why this completion is good or bad. If it's negative, describe what you expected instead.")
.size(LabelSize::Small)
.color(Color::Muted)
)
)
)
})
.when(!rated, |this| {
this.child(
div()
.h_40()
.pt_1()
.bg(bg_color)
.child(active_completion.feedback_editor.clone())
)
})
.child(
h_flex()
.p_1()
.h_8()
.max_h_8()
.border_t_1()
.border_color(border_color)
.max_w_full()
.justify_between()
.children(if rated {
Some(
label_container
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Rated completion.").color(Color::Muted)),
)
} else if active_completion.completion.edits.is_empty() {
Some(
label_container
.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new("No edits produced.").color(Color::Muted)),
)
} else {
Some(label_container)
})
.child(
h_flex()
.gap_1()
.child(
Button::new("bad", "Bad Completion")
.icon(IconName::ThumbsDown)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.disabled(rated || feedback_empty)
.when(feedback_empty, |this| {
this.tooltip(Tooltip::text("Explain what's bad about it before reporting it"))
})
.key_binding(KeyBinding::for_action_in(
&ThumbsDownActiveCompletion,
focus_handle,
window,
cx
))
.on_click(cx.listener(move |this, _, window, cx| {
this.thumbs_down_active(
&ThumbsDownActiveCompletion,
window, cx,
);
})),
)
.child(
Button::new("good", "Good Completion")
.icon(IconName::ThumbsUp)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.disabled(rated)
.key_binding(KeyBinding::for_action_in(
&ThumbsUpActiveCompletion,
focus_handle,
window,
cx
))
.on_click(cx.listener(move |this, _, window, cx| {
this.thumbs_up_active(&ThumbsUpActiveCompletion, window, cx);
})),
),
),
),
)
}
}
impl Render for RateCompletionModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let border_color = cx.theme().colors().border;
h_flex()
.key_context("RateCompletionModal")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_prev_edit))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_next_edit))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::thumbs_up_active))
.on_action(cx.listener(Self::thumbs_down_active))
.on_action(cx.listener(Self::focus_completions))
.on_action(cx.listener(Self::preview_completion))
.bg(cx.theme().colors().elevated_surface_background)
.border_1()
.border_color(border_color)
.w(window.viewport_size().width - px(320.))
.h(window.viewport_size().height - px(300.))
.rounded_lg()
.shadow_lg()
.child(
v_flex()
.w_72()
.h_full()
.border_r_1()
.border_color(border_color)
.flex_shrink_0()
.overflow_hidden()
.child(
h_flex()
.h_8()
.px_2()
.justify_between()
.border_b_1()
.border_color(border_color)
.child(
Icon::new(IconName::ZedPredict)
.size(IconSize::Small)
)
.child(
Label::new("From most recent to oldest")
.color(Color::Muted)
.size(LabelSize::Small),
)
)
.child(
div()
.id("completion_list")
.p_0p5()
.h_full()
.overflow_y_scroll()
.child(
List::new()
.empty_message(
div()
.p_2()
.child(
Label::new("No completions yet. Use the editor to generate some, and make sure to rate them!")
.color(Color::Muted),
)
.into_any_element(),
)
.children(self.zeta.read(cx).shown_completions().cloned().enumerate().map(
|(index, completion)| {
let selected =
self.active_completion.as_ref().map_or(false, |selected| {
selected.completion.id == completion.id
});
let rated =
self.zeta.read(cx).is_completion_rated(completion.id);
let (icon_name, icon_color, tooltip_text) = match (rated, completion.edits.is_empty()) {
(true, _) => (IconName::Check, Color::Success, "Rated Completion"),
(false, true) => (IconName::File, Color::Muted, "No Edits Produced"),
(false, false) => (IconName::FileDiff, Color::Accent, "Edits Available"),
};
let file_name = completion.path.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or("untitled".to_string());
let file_path = completion.path.parent().map(|p| p.to_string_lossy().to_string());
ListItem::new(completion.id)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.focused(index == self.selected_index)
.toggle_state(selected)
.child(
h_flex()
.id("completion-content")
.gap_3()
.child(
Icon::new(icon_name)
.color(icon_color)
.size(IconSize::Small)
)
.child(
v_flex()
.child(
h_flex().gap_1()
.child(Label::new(file_name).size(LabelSize::Small))
.when_some(file_path, |this, p| this.child(Label::new(p).size(LabelSize::Small).color(Color::Muted)))
)
.child(Label::new(format!("{} ago, {:.2?}", format_time_ago(completion.response_received_at.elapsed()), completion.latency()))
.color(Color::Muted)
.size(LabelSize::XSmall)
)
)
)
.tooltip(Tooltip::text(tooltip_text))
.on_click(cx.listener(move |this, _, window, cx| {
this.select_completion(Some(completion.clone()), true, window, cx);
}))
},
)),
)
),
)
.children(self.render_active_completion(window, cx))
.on_mouse_down_out(cx.listener(|_, _, _, cx| cx.emit(DismissEvent)))
}
}
impl EventEmitter<DismissEvent> for RateCompletionModal {}
impl Focusable for RateCompletionModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for RateCompletionModal {}
fn format_time_ago(elapsed: Duration) -> String {
let seconds = elapsed.as_secs();
if seconds < 120 {
"1 minute".to_string()
} else if seconds < 3600 {
format!("{} minutes", seconds / 60)
} else if seconds < 7200 {
"1 hour".to_string()
} else if seconds < 86400 {
format!("{} hours", seconds / 3600)
} else if seconds < 172800 {
"1 day".to_string()
} else {
format!("{} days", seconds / 86400)
}
}