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>
This commit is contained in:
Remco Smits 2025-03-18 17:55:25 +01:00 committed by GitHub
parent ed4e654fdf
commit 41a60ffecf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
156 changed files with 25840 additions and 451 deletions

View file

@ -0,0 +1,419 @@
use super::{
stack_frame_list::{StackFrameList, StackFrameListEvent},
variable_list::VariableList,
};
use anyhow::Result;
use collections::HashMap;
use dap::OutputEvent;
use editor::{CompletionProvider, Editor, EditorElement, EditorStyle};
use fuzzy::StringMatchCandidate;
use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity};
use language::{Buffer, CodeLabel};
use menu::Confirm;
use project::{
debugger::session::{CompletionsQuery, OutputToken, Session},
Completion,
};
use settings::Settings;
use std::{cell::RefCell, rc::Rc, usize};
use theme::ThemeSettings;
use ui::prelude::*;
pub struct Console {
console: Entity<Editor>,
query_bar: Entity<Editor>,
session: Entity<Session>,
_subscriptions: Vec<Subscription>,
variable_list: Entity<VariableList>,
stack_frame_list: Entity<StackFrameList>,
last_token: OutputToken,
update_output_task: Task<()>,
}
impl Console {
pub fn new(
session: Entity<Session>,
stack_frame_list: Entity<StackFrameList>,
variable_list: Entity<VariableList>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let console = cx.new(|cx| {
let mut editor = Editor::multi_line(window, cx);
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
editor.set_read_only(true);
editor.set_show_gutter(true, cx);
editor.set_show_runnables(false, cx);
editor.set_show_breakpoints(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_autoindent(false);
editor.set_input_enabled(false);
editor.set_use_autoclose(false);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_show_edit_predictions(Some(false), window, cx);
editor
});
let this = cx.weak_entity();
let query_bar = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_placeholder_text("Evaluate an expression", cx);
editor.set_use_autoclose(false);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_completion_provider(Some(Box::new(ConsoleQueryBarCompletionProvider(this))));
editor
});
let _subscriptions =
vec![cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events)];
Self {
session,
console,
query_bar,
variable_list,
_subscriptions,
stack_frame_list,
update_output_task: Task::ready(()),
last_token: OutputToken(0),
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn editor(&self) -> &Entity<Editor> {
&self.console
}
#[cfg(any(test, feature = "test-support"))]
pub fn query_bar(&self) -> &Entity<Editor> {
&self.query_bar
}
fn is_local(&self, cx: &Context<Self>) -> bool {
self.session.read(cx).is_local()
}
fn handle_stack_frame_list_events(
&mut self,
_: Entity<StackFrameList>,
event: &StackFrameListEvent,
cx: &mut Context<Self>,
) {
match event {
StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
}
}
pub fn add_messages<'a>(
&mut self,
events: impl Iterator<Item = &'a OutputEvent>,
window: &mut Window,
cx: &mut App,
) {
self.console.update(cx, |console, cx| {
let mut to_insert = String::default();
for event in events {
use std::fmt::Write;
_ = write!(to_insert, "{}\n", event.output.trim_end());
}
console.set_read_only(false);
console.move_to_end(&editor::actions::MoveToEnd, window, cx);
console.insert(&to_insert, window, cx);
console.set_read_only(true);
cx.notify();
});
}
pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
let expression = self.query_bar.update(cx, |editor, cx| {
let expression = editor.text(cx);
editor.clear(window, cx);
expression
});
self.session.update(cx, |state, cx| {
state.evaluate(
expression,
Some(dap::EvaluateArgumentsContext::Variables),
self.stack_frame_list.read(cx).current_stack_frame_id(),
None,
cx,
);
});
}
fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if self.console.read(cx).read_only(cx) {
cx.theme().colors().text_disabled
} else {
cx.theme().colors().text
},
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),
..Default::default()
};
EditorElement::new(
&self.console,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if self.console.read(cx).read_only(cx) {
cx.theme().colors().text_disabled
} else {
cx.theme().colors().text
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_fallbacks: settings.ui_font.fallbacks.clone(),
font_size: TextSize::Editor.rems(cx).into(),
font_weight: settings.ui_font.weight,
line_height: relative(1.3),
..Default::default()
};
EditorElement::new(
&self.query_bar,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
}
impl Render for Console {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let session = self.session.clone();
let token = self.last_token;
self.update_output_task = cx.spawn_in(window, move |this, mut cx| async move {
_ = session.update_in(&mut cx, move |session, window, cx| {
let (output, last_processed_token) = session.output(token);
_ = this.update(cx, |this, cx| {
if last_processed_token == this.last_token {
return;
}
this.add_messages(output, window, cx);
this.last_token = last_processed_token;
});
});
});
v_flex()
.key_context("DebugConsole")
.on_action(cx.listener(Self::evaluate))
.size_full()
.child(self.render_console(cx))
.when(self.is_local(cx), |this| {
this.child(self.render_query_bar(cx))
.pt(DynamicSpacing::Base04.rems(cx))
})
.border_2()
}
}
struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
impl CompletionProvider for ConsoleQueryBarCompletionProvider {
fn completions(
&self,
buffer: &Entity<Buffer>,
buffer_position: language::Anchor,
_trigger: editor::CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<Completion>>>> {
let Some(console) = self.0.upgrade() else {
return Task::ready(Ok(None));
};
let support_completions = console
.read(cx)
.session
.read(cx)
.capabilities()
.supports_completions_request
.unwrap_or_default();
if support_completions {
self.client_completions(&console, buffer, buffer_position, cx)
} else {
self.variable_list_completions(&console, buffer, buffer_position, cx)
}
}
fn resolve_completions(
&self,
_buffer: Entity<Buffer>,
_completion_indices: Vec<usize>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_cx: &mut Context<Editor>,
) -> gpui::Task<gpui::Result<bool>> {
Task::ready(Ok(false))
}
fn apply_additional_edits_for_completion(
&self,
_buffer: Entity<Buffer>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_completion_index: usize,
_push_to_history: bool,
_cx: &mut Context<Editor>,
) -> gpui::Task<gpui::Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
_buffer: &Entity<Buffer>,
_position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_cx: &mut Context<Editor>,
) -> bool {
true
}
}
impl ConsoleQueryBarCompletionProvider {
fn variable_list_completions(
&self,
console: &Entity<Console>,
buffer: &Entity<Buffer>,
buffer_position: language::Anchor,
cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<Completion>>>> {
let (variables, string_matches) = console.update(cx, |console, cx| {
let mut variables = HashMap::default();
let mut string_matches = Vec::default();
for variable in console.variable_list.update(cx, |variable_list, cx| {
variable_list.completion_variables(cx)
}) {
if let Some(evaluate_name) = &variable.evaluate_name {
variables.insert(evaluate_name.clone(), variable.value.clone());
string_matches.push(StringMatchCandidate {
id: 0,
string: evaluate_name.clone(),
char_bag: evaluate_name.chars().collect(),
});
}
variables.insert(variable.name.clone(), variable.value.clone());
string_matches.push(StringMatchCandidate {
id: 0,
string: variable.name.clone(),
char_bag: variable.name.chars().collect(),
});
}
(variables, string_matches)
});
let query = buffer.read(cx).text();
cx.spawn(|_, cx| async move {
let matches = fuzzy::match_strings(
&string_matches,
&query,
true,
10,
&Default::default(),
cx.background_executor().clone(),
)
.await;
Ok(Some(
matches
.iter()
.filter_map(|string_match| {
let variable_value = variables.get(&string_match.string)?;
Some(project::Completion {
old_range: buffer_position..buffer_position,
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),
text: format!("{} {}", string_match.string.clone(), variable_value),
runs: Vec::new(),
},
documentation: None,
confirm: None,
source: project::CompletionSource::Custom,
})
})
.collect(),
))
})
}
fn client_completions(
&self,
console: &Entity<Console>,
buffer: &Entity<Buffer>,
buffer_position: language::Anchor,
cx: &mut Context<Editor>,
) -> 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).current_stack_frame_id();
state.completions(
CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
cx,
)
})
});
cx.background_executor().spawn(async move {
Ok(Some(
completion_task
.await?
.iter()
.map(|completion| project::Completion {
old_range: buffer_position..buffer_position, // TODO(debugger): change this
new_text: completion.text.clone().unwrap_or(completion.label.clone()),
label: CodeLabel {
filter_range: 0..completion.label.len(),
text: completion.label.clone(),
runs: Vec::new(),
},
documentation: None,
confirm: None,
source: project::CompletionSource::Custom,
})
.collect(),
))
})
}
}

View file

@ -0,0 +1,103 @@
use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription};
use project::debugger::session::{Session, SessionEvent};
use ui::prelude::*;
use util::maybe;
pub struct LoadedSourceList {
list: ListState,
invalidate: bool,
focus_handle: FocusHandle,
_subscription: Subscription,
session: Entity<Session>,
}
impl LoadedSourceList {
pub fn new(session: Entity<Session>, 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(|loaded_sources| {
loaded_sources.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::LoadedSources => {
this.invalidate = true;
cx.notify();
}
_ => {}
});
Self {
list,
session,
focus_handle,
_subscription,
invalidate: true,
}
}
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
let Some(source) = maybe!({
self.session
.update(cx, |state, cx| state.loaded_sources(cx).get(ix).cloned())
}) else {
return Empty.into_any();
};
v_flex()
.rounded_md()
.w_full()
.group("")
.p_1()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.child(
h_flex()
.gap_0p5()
.text_ui_sm(cx)
.when_some(source.name.clone(), |this, name| this.child(name)),
)
.child(
h_flex()
.text_ui_xs(cx)
.text_color(cx.theme().colors().text_muted)
.when_some(source.path.clone(), |this, path| this.child(path)),
)
.into_any()
}
}
impl Focusable for LoadedSourceList {
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for LoadedSourceList {
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.loaded_sources(cx).len());
self.list.reset(len);
self.invalidate = false;
cx.notify();
}
div()
.track_focus(&self.focus_handle)
.size_full()
.p_1()
.child(list(self.list.clone()).size_full())
}
}

View file

@ -0,0 +1,183 @@
use anyhow::anyhow;
use gpui::{
list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription, WeakEntity,
};
use project::{
debugger::session::{Session, SessionEvent},
ProjectItem as _, ProjectPath,
};
use std::{path::Path, sync::Arc};
use ui::prelude::*;
use util::maybe;
use workspace::Workspace;
pub struct ModuleList {
list: ListState,
invalidate: bool,
session: Entity<Session>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
_subscription: Subscription,
}
impl ModuleList {
pub fn new(
session: Entity<Session>,
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();
}
_ => {}
});
Self {
list,
session,
workspace,
focus_handle,
_subscription,
invalidate: true,
}
}
fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
cx.spawn_in(window, move |this, mut cx| async move {
let (worktree, relative_path) = this
.update(&mut cx, |this, cx| {
this.workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |this, cx| {
this.find_or_create_worktree(&path, false, cx)
})
})
})??
.await?;
let buffer = this
.update(&mut cx, |this, cx| {
this.workspace.update(cx, |this, cx| {
this.project().update(cx, |this, cx| {
let worktree_id = worktree.read(cx).id();
this.open_buffer(
ProjectPath {
worktree_id,
path: relative_path.into(),
},
cx,
)
})
})
})??
.await?;
this.update_in(&mut cx, |this, window, cx| {
this.workspace.update(cx, |workspace, cx| {
let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
anyhow!("Could not select a stack frame for unnamed buffer")
})?;
anyhow::Ok(workspace.open_path_preview(
project_path,
None,
false,
true,
true,
window,
cx,
))
})
})???
.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
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();
};
v_flex()
.rounded_md()
.w_full()
.group("")
.id(("module-list", ix))
.when(module.path.is_some(), |this| {
this.on_click({
let path = module.path.as_deref().map(|path| Arc::<Path>::from(Path::new(path)));
cx.listener(move |this, _, window, cx| {
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");
}
})
})
})
.p_1()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
.child(
h_flex()
.text_ui_xs(cx)
.text_color(cx.theme().colors().text_muted)
.when_some(module.path.clone(), |this, path| this.child(path)),
)
.into_any()
}
}
#[cfg(any(test, feature = "test-support"))]
impl ModuleList {
pub fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
self.session
.update(cx, |session, cx| session.modules(cx).to_vec())
}
}
impl Focusable for ModuleList {
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
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();
}
div()
.track_focus(&self.focus_handle)
.size_full()
.p_1()
.child(list(self.list.clone()).size_full())
}
}

View file

@ -0,0 +1,519 @@
use std::path::Path;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use dap::StackFrameId;
use gpui::{
list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task,
WeakEntity,
};
use language::PointUtf16;
use project::debugger::session::{Session, SessionEvent, StackFrame};
use project::{ProjectItem, ProjectPath};
use ui::{prelude::*, Tooltip};
use util::ResultExt;
use workspace::Workspace;
use super::RunningState;
#[derive(Debug)]
pub enum StackFrameListEvent {
SelectedStackFrameChanged(StackFrameId),
}
pub struct StackFrameList {
list: ListState,
focus_handle: FocusHandle,
_subscription: Subscription,
session: Entity<Session>,
state: WeakEntity<RunningState>,
invalidate: bool,
entries: Vec<StackFrameEntry>,
workspace: WeakEntity<Workspace>,
current_stack_frame_id: Option<StackFrameId>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, PartialEq, Eq)]
pub enum StackFrameEntry {
Normal(dap::StackFrame),
Collapsed(Vec<dap::StackFrame>),
}
impl StackFrameList {
pub fn new(
workspace: WeakEntity<Workspace>,
session: Entity<Session>,
state: WeakEntity<RunningState>,
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 _subscription =
cx.subscribe_in(&session, window, |this, _, event, _, cx| match event {
SessionEvent::Stopped(_) | SessionEvent::StackTrace | SessionEvent::Threads => {
this.refresh(cx);
}
_ => {}
});
Self {
list,
session,
workspace,
focus_handle,
state,
_subscription,
invalidate: true,
entries: Default::default(),
current_stack_frame_id: None,
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn entries(&self) -> &Vec<StackFrameEntry> {
&self.entries
}
#[cfg(any(test, feature = "test-support"))]
pub fn flatten_entries(&self) -> Vec<dap::StackFrame> {
self.entries
.iter()
.flat_map(|frame| match frame {
StackFrameEntry::Normal(frame) => vec![frame.clone()],
StackFrameEntry::Collapsed(frames) => frames.clone(),
})
.collect::<Vec<_>>()
}
fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
self.state
.read_with(cx, |state, _| state.thread_id)
.log_err()
.flatten()
.map(|thread_id| {
self.session
.update(cx, |this, cx| this.stack_frames(thread_id, cx))
})
.unwrap_or_default()
}
#[cfg(any(test, feature = "test-support"))]
pub fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
self.stack_frames(cx)
.into_iter()
.map(|stack_frame| stack_frame.dap.clone())
.collect()
}
pub fn _get_main_stack_frame_id(&self, cx: &mut Context<Self>) -> u64 {
self.stack_frames(cx)
.first()
.map(|stack_frame| stack_frame.dap.id)
.unwrap_or(0)
}
pub fn current_stack_frame_id(&self) -> Option<StackFrameId> {
self.current_stack_frame_id
}
pub(super) fn refresh(&mut self, cx: &mut Context<Self>) {
self.invalidate = true;
self.entries.clear();
cx.notify();
}
pub fn build_entries(
&mut self,
select_first_stack_frame: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut entries = Vec::new();
let mut collapsed_entries = Vec::new();
let mut current_stack_frame = None;
let stack_frames = self.stack_frames(cx);
for stack_frame in &stack_frames {
match stack_frame.dap.presentation_hint {
Some(dap::StackFramePresentationHint::Deemphasize) => {
collapsed_entries.push(stack_frame.dap.clone());
}
_ => {
let collapsed_entries = std::mem::take(&mut collapsed_entries);
if !collapsed_entries.is_empty() {
entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
}
current_stack_frame.get_or_insert(&stack_frame.dap);
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
}
}
}
let collapsed_entries = std::mem::take(&mut collapsed_entries);
if !collapsed_entries.is_empty() {
entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
}
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);
}
cx.notify();
}
pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
if let Some(current_stack_frame_id) = self.current_stack_frame_id {
let frame = self
.entries
.iter()
.find_map(|entry| match entry {
StackFrameEntry::Normal(dap) => {
if dap.id == current_stack_frame_id {
Some(dap)
} else {
None
}
}
StackFrameEntry::Collapsed(daps) => {
daps.iter().find(|dap| dap.id == current_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(
&mut self,
stack_frame: &dap::StackFrame,
go_to_stack_frame: bool,
window: &Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.current_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 row = (stack_frame.line.saturating_sub(1)) as u32;
let Some(abs_path) = self.abs_path_from_stack_frame(&stack_frame) else {
return Task::ready(Err(anyhow!("Project path not found")));
};
cx.spawn_in(window, move |this, mut cx| async move {
let (worktree, relative_path) = this
.update(&mut cx, |this, cx| {
this.workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |this, cx| {
this.find_or_create_worktree(&abs_path, false, cx)
})
})
})??
.await?;
let buffer = this
.update(&mut cx, |this, cx| {
this.workspace.update(cx, |this, cx| {
this.project().update(cx, |this, cx| {
let worktree_id = worktree.read(cx).id();
this.open_buffer(
ProjectPath {
worktree_id,
path: relative_path.into(),
},
cx,
)
})
})
})??
.await?;
let position = buffer.update(&mut cx, |this, _| {
this.snapshot().anchor_after(PointUtf16::new(row, 0))
})?;
this.update_in(&mut cx, |this, window, cx| {
this.workspace.update(cx, |workspace, cx| {
let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
anyhow!("Could not select a stack frame for unnamed buffer")
})?;
anyhow::Ok(workspace.open_path_preview(
project_path,
None,
false,
true,
true,
window,
cx,
))
})
})???
.await?;
this.update(&mut cx, |this, cx| {
this.workspace.update(cx, |workspace, cx| {
let breakpoint_store = workspace.project().read(cx).breakpoint_store();
breakpoint_store.update(cx, |store, cx| {
store.set_active_position(
(this.session.read(cx).session_id(), abs_path, position),
cx,
);
})
})
})?
})
}
fn abs_path_from_stack_frame(&self, stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
stack_frame.source.as_ref().and_then(|s| {
s.path
.as_deref()
.map(|path| Arc::<Path>::from(Path::new(path)))
})
}
pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
self.session.update(cx, |state, cx| {
state.restart_stack_frame(stack_frame_id, cx)
});
}
fn render_normal_entry(
&self,
stack_frame: &dap::StackFrame,
cx: &mut Context<Self>,
) -> AnyElement {
let source = stack_frame.source.clone();
let is_selected_frame = Some(stack_frame.id) == self.current_stack_frame_id;
let formatted_path = format!(
"{}:{}",
source.clone().and_then(|s| s.name).unwrap_or_default(),
stack_frame.line,
);
let supports_frame_restart = self
.session
.read(cx)
.capabilities()
.supports_restart_frame
.unwrap_or_default();
let origin = stack_frame
.source
.to_owned()
.and_then(|source| source.origin);
h_flex()
.rounded_md()
.justify_between()
.w_full()
.group("")
.id(("stack-frame", stack_frame.id))
.tooltip({
let formatted_path = formatted_path.clone();
move |_window, app| {
app.new(|_| {
let mut tooltip = Tooltip::new(formatted_path.clone());
if let Some(origin) = &origin {
tooltip = tooltip.meta(origin);
}
tooltip
})
.into()
}
})
.p_1()
.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);
}
}))
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
.child(
v_flex()
.child(
h_flex()
.gap_0p5()
.text_ui_sm(cx)
.truncate()
.child(stack_frame.name.clone())
.child(formatted_path),
)
.child(
h_flex()
.text_ui_xs(cx)
.truncate()
.text_color(cx.theme().colors().text_muted)
.when_some(source.and_then(|s| s.path), |this, path| this.child(path)),
),
)
.when(
supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
|this| {
this.child(
h_flex()
.id(("restart-stack-frame", stack_frame.id))
.visible_on_hover("")
.absolute()
.right_2()
.overflow_hidden()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().element_selected)
.bg(cx.theme().colors().element_background)
.hover(|style| {
style
.bg(cx.theme().colors().ghost_element_hover)
.cursor_pointer()
})
.child(
IconButton::new(
("restart-stack-frame", stack_frame.id),
IconName::DebugRestart,
)
.icon_size(IconSize::Small)
.on_click(cx.listener({
let stack_frame_id = stack_frame.id;
move |this, _, _window, cx| {
this.restart_stack_frame(stack_frame_id, cx);
}
}))
.tooltip(move |window, cx| {
Tooltip::text("Restart Stack Frame")(window, cx)
}),
),
)
},
)
.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();
}
fn render_collapsed_entry(
&self,
ix: usize,
stack_frames: &Vec<dap::StackFrame>,
cx: &mut Context<Self>,
) -> AnyElement {
let first_stack_frame = &stack_frames[0];
h_flex()
.rounded_md()
.justify_between()
.w_full()
.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);
}
}))
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
.child(
v_flex()
.text_ui_sm(cx)
.truncate()
.text_color(cx.theme().colors().text_muted)
.child(format!(
"Show {} more{}",
stack_frames.len(),
first_stack_frame
.source
.as_ref()
.and_then(|source| source.origin.as_ref())
.map_or(String::new(), |origin| format!(": {}", origin))
)),
)
.into_any()
}
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::Collapsed(stack_frames) => {
self.render_collapsed_entry(ix, stack_frames, cx)
}
}
}
}
impl Render for StackFrameList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.invalidate {
self.build_entries(self.entries.is_empty(), window, cx);
self.invalidate = false;
cx.notify();
}
div()
.size_full()
.p_1()
.child(list(self.list.clone()).size_full())
}
}
impl Focusable for StackFrameList {
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<StackFrameListEvent> for StackFrameList {}

View file

@ -0,0 +1,946 @@
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
use dap::{ScopePresentationHint, StackFrameId, VariablePresentationHintKind, VariableReference};
use editor::Editor;
use gpui::{
actions, anchored, deferred, uniform_list, AnyElement, ClickEvent, ClipboardItem, Context,
DismissEvent, Entity, FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point,
Stateful, Subscription, TextStyleRefinement, UniformListScrollHandle,
};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
use project::debugger::session::{Session, SessionEvent};
use std::{collections::HashMap, ops::Range, sync::Arc};
use ui::{prelude::*, ContextMenu, ListItem, Scrollbar, ScrollbarState};
use util::{debug_panic, maybe};
actions!(variable_list, [ExpandSelectedEntry, CollapseSelectedEntry]);
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) struct EntryState {
depth: usize,
is_expanded: bool,
parent_reference: VariableReference,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub(crate) struct EntryPath {
pub leaf_name: Option<SharedString>,
pub indices: Arc<[SharedString]>,
}
impl EntryPath {
fn for_scope(scope_name: impl Into<SharedString>) -> Self {
Self {
leaf_name: Some(scope_name.into()),
indices: Arc::new([]),
}
}
fn with_name(&self, name: SharedString) -> Self {
Self {
leaf_name: Some(name),
indices: self.indices.clone(),
}
}
/// Create a new child of this variable path
fn with_child(&self, name: SharedString) -> Self {
Self {
leaf_name: None,
indices: self
.indices
.iter()
.cloned()
.chain(std::iter::once(name))
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
enum EntryKind {
Variable(dap::Variable),
Scope(dap::Scope),
}
impl EntryKind {
fn as_variable(&self) -> Option<&dap::Variable> {
match self {
EntryKind::Variable(dap) => Some(dap),
_ => None,
}
}
fn as_scope(&self) -> Option<&dap::Scope> {
match self {
EntryKind::Scope(dap) => Some(dap),
_ => None,
}
}
#[allow(dead_code)]
fn name(&self) -> &str {
match self {
EntryKind::Variable(dap) => &dap.name,
EntryKind::Scope(dap) => &dap.name,
}
}
}
#[derive(Debug, Clone, PartialEq)]
struct ListEntry {
dap_kind: EntryKind,
path: EntryPath,
}
impl ListEntry {
fn as_variable(&self) -> Option<&dap::Variable> {
self.dap_kind.as_variable()
}
fn as_scope(&self) -> Option<&dap::Scope> {
self.dap_kind.as_scope()
}
fn item_id(&self) -> ElementId {
use std::fmt::Write;
let mut id = match &self.dap_kind {
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
};
for name in self.path.indices.iter() {
_ = write!(id, "-{}", name);
}
SharedString::from(id).into()
}
fn item_value_id(&self) -> ElementId {
use std::fmt::Write;
let mut id = match &self.dap_kind {
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
};
for name in self.path.indices.iter() {
_ = write!(id, "-{}", name);
}
_ = write!(id, "-value");
SharedString::from(id).into()
}
}
pub struct VariableList {
entries: Vec<ListEntry>,
entry_states: HashMap<EntryPath, EntryState>,
selected_stack_frame_id: Option<StackFrameId>,
list_handle: UniformListScrollHandle,
scrollbar_state: ScrollbarState,
session: Entity<Session>,
selection: Option<EntryPath>,
open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
focus_handle: FocusHandle,
edited_path: Option<(EntryPath, Entity<Editor>)>,
disabled: bool,
_subscriptions: Vec<Subscription>,
}
impl VariableList {
pub fn new(
session: Entity<Session>,
stack_frame_list: Entity<StackFrameList>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let _subscriptions = vec![
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
cx.subscribe(&session, |this, _, event, _| match event {
SessionEvent::Stopped(_) => {
this.selection.take();
this.edited_path.take();
this.selected_stack_frame_id.take();
}
_ => {}
}),
cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
this.edited_path.take();
cx.notify();
}),
];
let list_state = UniformListScrollHandle::default();
Self {
scrollbar_state: ScrollbarState::new(list_state.clone()),
list_handle: list_state,
session,
focus_handle,
_subscriptions,
selected_stack_frame_id: None,
selection: None,
open_context_menu: None,
disabled: false,
edited_path: None,
entries: Default::default(),
entry_states: Default::default(),
}
}
pub(super) fn disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
let old_disabled = std::mem::take(&mut self.disabled);
self.disabled = disabled;
if old_disabled != disabled {
cx.notify();
}
}
fn build_entries(&mut self, cx: &mut Context<Self>) {
let Some(stack_frame_id) = self.selected_stack_frame_id else {
return;
};
let mut entries = vec![];
let scopes: Vec<_> = self.session.update(cx, |session, cx| {
session.scopes(stack_frame_id, cx).iter().cloned().collect()
});
let mut contains_local_scope = false;
let mut stack = scopes
.into_iter()
.rev()
.filter(|scope| {
if scope
.presentation_hint
.as_ref()
.map(|hint| *hint == ScopePresentationHint::Locals)
.unwrap_or(scope.name.to_lowercase().starts_with("local"))
{
contains_local_scope = true;
}
self.session.update(cx, |session, cx| {
session.variables(scope.variables_reference, cx).len() > 0
})
})
.map(|scope| {
(
scope.variables_reference,
scope.variables_reference,
EntryPath::for_scope(&scope.name),
EntryKind::Scope(scope),
)
})
.collect::<Vec<_>>();
let scopes_count = stack.len();
while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
{
match &dap_kind {
EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
}
let var_state = self
.entry_states
.entry(path.clone())
.and_modify(|state| {
state.parent_reference = container_reference;
})
.or_insert(EntryState {
depth: path.indices.len(),
is_expanded: dap_kind.as_scope().is_some_and(|scope| {
(scopes_count == 1 && !contains_local_scope)
|| scope
.presentation_hint
.as_ref()
.map(|hint| *hint == ScopePresentationHint::Locals)
.unwrap_or(scope.name.to_lowercase().starts_with("local"))
}),
parent_reference: container_reference,
});
entries.push(ListEntry {
dap_kind,
path: path.clone(),
});
if var_state.is_expanded {
let children = self
.session
.update(cx, |session, cx| session.variables(variables_reference, cx));
stack.extend(children.into_iter().rev().map(|child| {
(
variables_reference,
child.variables_reference,
path.with_child(child.name.clone().into()),
EntryKind::Variable(child),
)
}));
}
}
self.entries = entries;
cx.notify();
}
fn handle_stack_frame_list_events(
&mut self,
_: Entity<StackFrameList>,
event: &StackFrameListEvent,
cx: &mut Context<Self>,
) {
match event {
StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
self.selected_stack_frame_id = Some(*stack_frame_id);
cx.notify();
}
}
}
pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
EntryKind::Variable(dap) => Some(dap.clone()),
EntryKind::Scope(_) => None,
})
.collect()
}
fn render_entries(
&mut self,
ix: Range<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<AnyElement> {
ix.into_iter()
.filter_map(|ix| {
let (entry, state) = self
.entries
.get(ix)
.and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
match &entry.dap_kind {
EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
}
})
.collect()
}
pub(crate) fn toggle_entry(&mut self, var_path: &EntryPath, cx: &mut Context<Self>) {
let Some(entry) = self.entry_states.get_mut(var_path) else {
log::error!("Could not find variable list entry state to toggle");
return;
};
entry.is_expanded = !entry.is_expanded;
cx.notify();
}
fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(variable) = self.entries.first() {
self.selection = Some(variable.path.clone());
cx.notify();
}
}
fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(variable) = self.entries.last() {
self.selection = Some(variable.path.clone());
cx.notify();
}
}
fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(selection) = &self.selection {
if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| {
if &var.path == selection {
Some(ix.saturating_sub(1))
} else {
None
}
}) {
if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) {
self.selection = Some(new_selection);
cx.notify();
} else {
self.select_first(&SelectFirst, window, cx);
}
}
} else {
self.select_first(&SelectFirst, window, cx);
}
}
fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(selection) = &self.selection {
if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| {
if &var.path == selection {
Some(ix.saturating_add(1))
} else {
None
}
}) {
if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) {
self.selection = Some(new_selection);
cx.notify();
} else {
self.select_first(&SelectFirst, window, cx);
}
}
} else {
self.select_first(&SelectFirst, window, cx);
}
}
fn cancel_variable_edit(
&mut self,
_: &menu::Cancel,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.edited_path.take();
self.focus_handle.focus(window);
cx.notify();
}
fn confirm_variable_edit(
&mut self,
_: &menu::Confirm,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let res = maybe!({
let (var_path, editor) = self.edited_path.take()?;
let state = self.entry_states.get(&var_path)?;
let variables_reference = state.parent_reference;
let name = var_path.leaf_name?;
let value = editor.read(cx).text(cx);
self.session.update(cx, |session, cx| {
session.set_variable_value(variables_reference, name.into(), value, cx)
});
Some(())
});
if res.is_none() {
log::error!("Couldn't confirm variable edit because variable doesn't have a leaf name or a parent reference id");
}
}
fn collapse_selected_entry(
&mut self,
_: &CollapseSelectedEntry,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(ref selected_entry) = self.selection {
let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
debug_panic!("Trying to toggle variable in variable list that has an no state");
return;
};
entry_state.is_expanded = false;
cx.notify();
}
}
fn expand_selected_entry(
&mut self,
_: &ExpandSelectedEntry,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(ref selected_entry) = self.selection {
let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
debug_panic!("Trying to toggle variable in variable list that has an no state");
return;
};
entry_state.is_expanded = true;
cx.notify();
}
}
fn deploy_variable_context_menu(
&mut self,
variable: ListEntry,
position: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(dap_var) = variable.as_variable() else {
debug_panic!("Trying to open variable context menu on a scope");
return;
};
let variable_value = dap_var.value.clone();
let variable_name = dap_var.name.clone();
let this = cx.entity().clone();
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
menu.entry("Copy name", None, move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(variable_name.clone()))
})
.entry("Copy value", None, {
let variable_value = variable_value.clone();
move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(variable_value.clone()))
}
})
.entry("Set value", None, move |window, cx| {
this.update(cx, |variable_list, cx| {
let editor = Self::create_variable_editor(&variable_value, window, cx);
variable_list.edited_path = Some((variable.path.clone(), editor));
cx.notify();
});
})
});
cx.focus_view(&context_menu, window);
let subscription = cx.subscribe_in(
&context_menu,
window,
|this, _, _: &DismissEvent, window, cx| {
if this.open_context_menu.as_ref().is_some_and(|context_menu| {
context_menu.0.focus_handle(cx).contains_focused(window, cx)
}) {
cx.focus_self(window);
}
this.open_context_menu.take();
cx.notify();
},
);
self.open_context_menu = Some((context_menu, position, subscription));
}
#[track_caller]
#[cfg(any(test, feature = "test-support"))]
pub fn assert_visual_entries(&self, expected: Vec<&str>) {
const INDENT: &'static str = " ";
let entries = &self.entries;
let mut visual_entries = Vec::with_capacity(entries.len());
for entry in entries {
let state = self
.entry_states
.get(&entry.path)
.expect("If there's a variable entry there has to be a state that goes with it");
visual_entries.push(format!(
"{}{} {}{}",
INDENT.repeat(state.depth - 1),
if state.is_expanded { "v" } else { ">" },
entry.dap_kind.name(),
if self.selection.as_ref() == Some(&entry.path) {
" <=== selected"
} else {
""
}
));
}
pretty_assertions::assert_eq!(expected, visual_entries);
}
#[track_caller]
#[cfg(any(test, feature = "test-support"))]
pub fn scopes(&self) -> Vec<dap::Scope> {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
EntryKind::Scope(scope) => Some(scope),
_ => None,
})
.cloned()
.collect()
}
#[track_caller]
#[cfg(any(test, feature = "test-support"))]
pub fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> {
let mut scopes: Vec<(dap::Scope, Vec<_>)> = Vec::new();
let mut idx = 0;
for entry in self.entries.iter() {
match &entry.dap_kind {
EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
EntryKind::Scope(scope) => {
if scopes.len() > 0 {
idx += 1;
}
scopes.push((scope.clone(), Vec::new()));
}
}
}
scopes
}
#[track_caller]
#[cfg(any(test, feature = "test-support"))]
pub fn variables(&self) -> Vec<dap::Variable> {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
EntryKind::Variable(variable) => Some(variable),
_ => None,
})
.cloned()
.collect()
}
fn create_variable_editor(default: &str, window: &mut Window, cx: &mut App) -> Entity<Editor> {
let editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
let refinement = TextStyleRefinement {
font_size: Some(
TextSize::XSmall
.rems(cx)
.to_pixels(window.rem_size())
.into(),
),
..Default::default()
};
editor.set_text_style_refinement(refinement);
editor.set_text(default, window, cx);
editor.select_all(&editor::actions::SelectAll, window, cx);
editor
});
editor.focus_handle(cx).focus(window);
editor
}
fn render_scope(
&self,
entry: &ListEntry,
state: EntryState,
cx: &mut Context<Self>,
) -> AnyElement {
let Some(scope) = entry.as_scope() else {
debug_panic!("Called render scope on non scope variable list entry variant");
return div().into_any_element();
};
let var_ref = scope.variables_reference;
let is_selected = self
.selection
.as_ref()
.is_some_and(|selection| selection == &entry.path);
let colors = get_entry_color(cx);
let bg_hover_color = if !is_selected {
colors.hover
} else {
colors.default
};
let border_color = if is_selected {
colors.marked_active
} else {
colors.default
};
div()
.id(var_ref as usize)
.group("variable_list_entry")
.border_1()
.border_r_2()
.border_color(border_color)
.flex()
.w_full()
.h_full()
.hover(|style| style.bg(bg_hover_color))
.on_click(cx.listener({
move |_this, _, _window, cx| {
cx.notify();
}
}))
.child(
ListItem::new(SharedString::from(format!("scope-{}", var_ref)))
.selectable(false)
.indent_level(state.depth + 1)
.indent_step_size(px(20.))
.always_show_disclosure_icon(true)
.toggle(state.is_expanded)
.on_toggle({
let var_path = entry.path.clone();
cx.listener(move |this, _, _, cx| this.toggle_entry(&var_path, cx))
})
.child(div().text_ui(cx).w_full().child(scope.name.clone())),
)
.into_any()
}
fn render_variable(
&self,
variable: &ListEntry,
state: EntryState,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let dap = match &variable.dap_kind {
EntryKind::Variable(dap) => dap,
EntryKind::Scope(_) => {
debug_panic!("Called render variable on variable list entry kind scope");
return div().into_any_element();
}
};
let syntax_color_for = |name| cx.theme().syntax().get(name).color;
let variable_name_color = match &dap
.presentation_hint
.as_ref()
.and_then(|hint| hint.kind.as_ref())
.unwrap_or(&VariablePresentationHintKind::Unknown)
{
VariablePresentationHintKind::Class
| VariablePresentationHintKind::BaseClass
| VariablePresentationHintKind::InnerClass
| VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"),
VariablePresentationHintKind::Data => syntax_color_for("variable"),
VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"),
};
let variable_color = syntax_color_for("variable.special");
let var_ref = dap.variables_reference;
let colors = get_entry_color(cx);
let is_selected = self
.selection
.as_ref()
.is_some_and(|selected_path| *selected_path == variable.path);
let bg_hover_color = if !is_selected {
colors.hover
} else {
colors.default
};
let border_color = if is_selected && self.focus_handle.contains_focused(window, cx) {
colors.marked_active
} else {
colors.default
};
let path = variable.path.clone();
div()
.id(variable.item_id())
.group("variable_list_entry")
.border_1()
.border_r_2()
.border_color(border_color)
.h_4()
.size_full()
.hover(|style| style.bg(bg_hover_color))
.on_click(cx.listener({
move |this, _, _window, cx| {
this.selection = Some(path.clone());
cx.notify();
}
}))
.child(
ListItem::new(SharedString::from(format!(
"variable-item-{}-{}",
dap.name, state.depth
)))
.disabled(self.disabled)
.selectable(false)
.indent_level(state.depth + 1_usize)
.indent_step_size(px(20.))
.always_show_disclosure_icon(true)
.when(var_ref > 0, |list_item| {
list_item.toggle(state.is_expanded).on_toggle(cx.listener({
let var_path = variable.path.clone();
move |this, _, _, cx| {
this.session.update(cx, |session, cx| {
session.variables(var_ref, cx);
});
this.toggle_entry(&var_path, cx);
}
}))
})
.on_secondary_mouse_down(cx.listener({
let variable = variable.clone();
move |this, event: &MouseDownEvent, window, cx| {
this.deploy_variable_context_menu(
variable.clone(),
event.position,
window,
cx,
)
}
}))
.child(
h_flex()
.gap_1()
.text_ui_sm(cx)
.w_full()
.child(
Label::new(&dap.name).when_some(variable_name_color, |this, color| {
this.color(Color::from(color))
}),
)
.when(!dap.value.is_empty(), |this| {
this.child(div().w_full().id(variable.item_value_id()).map(|this| {
if let Some((_, editor)) = self
.edited_path
.as_ref()
.filter(|(path, _)| path == &variable.path)
{
this.child(div().size_full().px_2().child(editor.clone()))
} else {
this.text_color(cx.theme().colors().text_muted)
.when(
!self.disabled
&& self
.session
.read(cx)
.capabilities()
.supports_set_variable
.unwrap_or_default(),
|this| {
let path = variable.path.clone();
let variable_value = dap.value.clone();
this.on_click(cx.listener(
move |this, click: &ClickEvent, window, cx| {
if click.down.click_count < 2 {
return;
}
let editor = Self::create_variable_editor(
&variable_value,
window,
cx,
);
this.edited_path =
Some((path.clone(), editor));
cx.notify();
},
))
},
)
.child(
Label::new(format!("= {}", &dap.value))
.single_line()
.truncate()
.size(LabelSize::Small)
.when_some(variable_color, |this, color| {
this.color(Color::from(color))
}),
)
}
}))
}),
),
)
.into_any()
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.occlude()
.id("variable-list-vertical-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
}
impl Focusable for VariableList {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for VariableList {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.build_entries(cx);
v_flex()
.key_context("VariableList")
.id("variable-list")
.group("variable-list")
.overflow_y_scroll()
.size_full()
.track_focus(&self.focus_handle(cx))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::expand_selected_entry))
.on_action(cx.listener(Self::collapse_selected_entry))
.on_action(cx.listener(Self::cancel_variable_edit))
.on_action(cx.listener(Self::confirm_variable_edit))
//
.child(
uniform_list(
cx.entity().clone(),
"variable-list",
self.entries.len(),
move |this, range, window, cx| this.render_entries(range, window, cx),
)
.track_scroll(self.list_handle.clone())
.gap_1_5()
.size_full()
.flex_grow(),
)
.children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
deferred(
anchored()
.position(*position)
.anchor(gpui::Corner::TopLeft)
.child(menu.clone()),
)
.with_priority(1)
}))
.child(self.render_vertical_scrollbar(cx))
}
}
struct EntryColors {
default: Hsla,
hover: Hsla,
marked_active: Hsla,
}
fn get_entry_color(cx: &Context<VariableList>) -> EntryColors {
let colors = cx.theme().colors();
EntryColors {
default: colors.panel_background,
hover: colors.ghost_element_hover,
marked_active: colors.ghost_element_selected,
}
}