debugger: Add debug task picker to new session modal (#29702)

## Preview 

![image](https://github.com/user-attachments/assets/203a577f-3b38-4017-9571-de1234415162)


### TODO
- [x] Add scenario picker to new session modal
- [x] Make debugger start action open new session modal instead of task
modal
- [x] Fix `esc` not clearing the cancelling the new session modal while
it's in scenario or attach mode
- [x] Resolve debug scenario's correctly

Release Notes:

- N/A
This commit is contained in:
Anthony Eid 2025-05-02 04:38:29 -04:00 committed by GitHub
parent ba59305510
commit f619d5f02a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 607 additions and 180 deletions

View file

@ -7,6 +7,7 @@ use crate::{
};
use crate::{new_session_modal::NewSessionModal, session::DebugSession};
use anyhow::{Result, anyhow};
use collections::{HashMap, HashSet};
use command_palette_hooks::CommandPaletteFilter;
use dap::DebugRequest;
use dap::{
@ -26,6 +27,7 @@ use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
use settings::Settings;
use std::any::TypeId;
use std::path::PathBuf;
use task::{DebugScenario, TaskContext};
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
use workspace::SplitDirection;
@ -403,7 +405,6 @@ impl DebugPanel {
pub fn resolve_scenario(
&self,
scenario: DebugScenario,
task_context: TaskContext,
buffer: Option<Entity<Buffer>>,
window: &Window,
@ -424,8 +425,60 @@ impl DebugPanel {
stop_on_entry,
} = scenario;
let request = if let Some(mut request) = request {
// Resolve task variables within the request.
if let DebugRequest::Launch(_) = &mut request {}
if let DebugRequest::Launch(launch_config) = &mut request {
let mut variable_names = HashMap::default();
let mut substituted_variables = HashSet::default();
let task_variables = task_context
.task_variables
.iter()
.map(|(key, value)| {
let key_string = key.to_string();
if !variable_names.contains_key(&key_string) {
variable_names.insert(key_string.clone(), key.clone());
}
(key_string, value.as_str())
})
.collect::<HashMap<_, _>>();
let cwd = launch_config
.cwd
.as_ref()
.and_then(|cwd| cwd.to_str())
.and_then(|cwd| {
task::substitute_all_template_variables_in_str(
cwd,
&task_variables,
&variable_names,
&mut substituted_variables,
)
});
if let Some(cwd) = cwd {
launch_config.cwd = Some(PathBuf::from(cwd))
}
if let Some(program) = task::substitute_all_template_variables_in_str(
&launch_config.program,
&task_variables,
&variable_names,
&mut substituted_variables,
) {
launch_config.program = program;
}
for arg in launch_config.args.iter_mut() {
if let Some(substituted_arg) =
task::substitute_all_template_variables_in_str(
&arg,
&task_variables,
&variable_names,
&mut substituted_variables,
)
{
*arg = substituted_arg;
}
}
}
request
} else if let Some(build) = build {
@ -944,6 +997,7 @@ impl DebugPanel {
past_debug_definition,
weak_panel,
workspace,
None,
window,
cx,
)

View file

@ -158,6 +158,7 @@ pub fn init(cx: &mut App) {
debug_panel.read(cx).past_debug_definition.clone(),
weak_panel,
weak_workspace,
None,
window,
cx,
)
@ -166,14 +167,22 @@ pub fn init(cx: &mut App) {
},
)
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
tasks_ui::toggle_modal(
workspace,
None,
task::TaskModal::DebugModal,
window,
cx,
)
.detach();
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
let weak_panel = debug_panel.downgrade();
let weak_workspace = cx.weak_entity();
let task_store = workspace.project().read(cx).task_store().clone();
workspace.toggle_modal(window, cx, |window, cx| {
NewSessionModal::new(
debug_panel.read(cx).past_debug_definition.clone(),
weak_panel,
weak_workspace,
Some(task_store),
window,
cx,
)
});
}
});
})
})

View file

@ -6,19 +6,25 @@ use std::{
use dap::{DapRegistry, DebugRequest, adapters::DebugTaskDefinition};
use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
WeakEntity,
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render,
Subscription, TextStyle, WeakEntity,
};
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{TaskSourceKind, task_store::TaskStore};
use session_modes::{AttachMode, DebugScenarioDelegate, LaunchMode};
use settings::Settings;
use task::{DebugScenario, LaunchRequest, TaskContext};
use task::{DebugScenario, LaunchRequest};
use theme::ThemeSettings;
use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label,
LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton,
ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, InteractiveElement,
IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing, ParentElement, RenderOnce,
SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Window, div, h_flex,
relative, rems, v_flex,
};
use util::ResultExt;
use workspace::{ModalView, Workspace};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
@ -57,6 +63,7 @@ impl NewSessionModal {
past_debug_definition: Option<DebugTaskDefinition>,
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
task_store: Option<Entity<TaskStore>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@ -73,6 +80,18 @@ impl NewSessionModal {
_ => None,
};
if let Some(task_store) = task_store {
cx.defer_in(window, |this, window, cx| {
this.mode = NewSessionMode::scenario(
this.debug_panel.clone(),
this.workspace.clone(),
task_store,
window,
cx,
);
});
};
Self {
workspace: workspace.clone(),
debugger,
@ -86,10 +105,10 @@ impl NewSessionModal {
}
}
fn debug_config(&self, cx: &App, debugger: &str) -> DebugScenario {
let request = self.mode.debug_task(cx);
fn debug_config(&self, cx: &App, debugger: &str) -> Option<DebugScenario> {
let request = self.mode.debug_task(cx)?;
let label = suggested_label(&request, debugger);
DebugScenario {
Some(DebugScenario {
adapter: debugger.to_owned().into(),
label,
request: Some(request),
@ -100,21 +119,42 @@ impl NewSessionModal {
_ => None,
},
build: None,
}
})
}
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
let Some(debugger) = self.debugger.as_ref() else {
// todo: show in UI.
// todo(debugger): show in UI.
log::error!("No debugger selected");
return;
};
let config = self.debug_config(cx, debugger);
if let NewSessionMode::Scenario(picker) = &self.mode {
picker.update(cx, |picker, cx| {
picker.delegate.confirm(false, window, cx);
});
return;
}
let Some(config) = self.debug_config(cx, debugger) else {
log::error!("debug config not found in mode: {}", self.mode);
return;
};
let debug_panel = self.debug_panel.clone();
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |this, cx| {
let task_contexts = workspace
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})?
.await;
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
debug_panel.update_in(cx, |debug_panel, window, cx| {
debug_panel.start_session(config, TaskContext::default(), None, window, cx)
debug_panel.start_session(config, task_context, None, window, cx)
})?;
this.update(cx, |_, cx| {
cx.emit(DismissEvent);
@ -256,9 +296,14 @@ impl NewSessionModal {
.iter()
.flat_map(|task_inventory| {
task_inventory.read(cx).list_debug_scenarios(
worktree.as_ref().map(|worktree| worktree.read(cx).id()),
worktree
.as_ref()
.map(|worktree| worktree.read(cx).id())
.iter()
.copied(),
)
})
.map(|(_source_kind, scenario)| scenario)
.collect()
})
.ok()
@ -277,102 +322,22 @@ impl NewSessionModal {
}
}
#[derive(Clone)]
struct LaunchMode {
program: Entity<Editor>,
cwd: Entity<Editor>,
}
impl LaunchMode {
fn new(
past_launch_config: Option<LaunchRequest>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let (past_program, past_cwd) = past_launch_config
.map(|config| (Some(config.program), config.cwd))
.unwrap_or_else(|| (None, None));
let program = cx.new(|cx| Editor::single_line(window, cx));
program.update(cx, |this, cx| {
this.set_placeholder_text("Program path", cx);
if let Some(past_program) = past_program {
this.set_text(past_program, window, cx);
};
});
let cwd = cx.new(|cx| Editor::single_line(window, cx));
cwd.update(cx, |this, cx| {
this.set_placeholder_text("Working Directory", cx);
if let Some(past_cwd) = past_cwd {
this.set_text(past_cwd.to_string_lossy(), window, cx);
};
});
cx.new(|_| Self { program, cwd })
}
fn debug_task(&self, cx: &App) -> task::LaunchRequest {
let path = self.cwd.read(cx).text(cx);
task::LaunchRequest {
program: self.program.read(cx).text(cx),
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
args: Default::default(),
env: Default::default(),
}
}
}
#[derive(Clone)]
struct AttachMode {
definition: DebugTaskDefinition,
attach_picker: Entity<AttachModal>,
}
impl AttachMode {
fn new(
debugger: Option<SharedString>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Entity<Self> {
let definition = DebugTaskDefinition {
adapter: debugger.clone().unwrap_or_default(),
label: "Attach New Session Setup".into(),
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
initialize_args: None,
tcp_connection: None,
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
window.focus(&modal.focus_handle(cx));
modal
});
cx.new(|_| Self {
definition,
attach_picker,
})
}
fn debug_task(&self) -> task::AttachRequest {
task::AttachRequest { process_id: None }
}
}
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
static SELECT_SCENARIO_LABEL: SharedString = SharedString::new_static("Select Profile");
#[derive(Clone)]
enum NewSessionMode {
Launch(Entity<LaunchMode>),
Scenario(Entity<Picker<DebugScenarioDelegate>>),
Attach(Entity<AttachMode>),
}
impl NewSessionMode {
fn debug_task(&self, cx: &App) -> DebugRequest {
fn debug_task(&self, cx: &App) -> Option<DebugRequest> {
match self {
NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(),
NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(),
NewSessionMode::Launch(entity) => Some(entity.read(cx).debug_task(cx).into()),
NewSessionMode::Attach(entity) => Some(entity.read(cx).debug_task().into()),
NewSessionMode::Scenario(_) => None,
}
}
fn as_attach(&self) -> Option<&Entity<AttachMode>> {
@ -382,6 +347,78 @@ impl NewSessionMode {
None
}
}
fn scenario(
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
task_store: Entity<TaskStore>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> NewSessionMode {
let picker = cx.new(|cx| {
Picker::uniform_list(
DebugScenarioDelegate::new(debug_panel, workspace, task_store),
window,
cx,
)
.modal(false)
});
cx.subscribe(&picker, |_, _, _, cx| {
cx.emit(DismissEvent);
})
.detach();
picker.focus_handle(cx).focus(window);
NewSessionMode::Scenario(picker)
}
fn attach(
debugger: Option<SharedString>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Self {
Self::Attach(AttachMode::new(debugger, workspace, window, cx))
}
fn launch(
past_launch_config: Option<LaunchRequest>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Self {
Self::Launch(LaunchMode::new(past_launch_config, window, cx))
}
fn has_match(&self, cx: &App) -> bool {
match self {
NewSessionMode::Scenario(picker) => picker.read(cx).delegate.match_count() > 0,
NewSessionMode::Attach(picker) => {
picker
.read(cx)
.attach_picker
.read(cx)
.picker
.read(cx)
.delegate
.match_count()
> 0
}
_ => false,
}
}
}
impl std::fmt::Display for NewSessionMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mode = match self {
NewSessionMode::Launch(_) => "launch".to_owned(),
NewSessionMode::Attach(_) => "attach".to_owned(),
NewSessionMode::Scenario(_) => "scenario picker".to_owned(),
};
write!(f, "{}", mode)
}
}
impl Focusable for NewSessionMode {
@ -389,6 +426,7 @@ impl Focusable for NewSessionMode {
match &self {
NewSessionMode::Launch(entity) => entity.read(cx).program.focus_handle(cx),
NewSessionMode::Attach(entity) => entity.read(cx).attach_picker.focus_handle(cx),
NewSessionMode::Scenario(entity) => entity.read(cx).focus_handle(cx),
}
}
}
@ -437,27 +475,14 @@ impl RenderOnce for NewSessionMode {
NewSessionMode::Attach(entity) => entity.update(cx, |this, cx| {
this.clone().render(window, cx).into_any_element()
}),
NewSessionMode::Scenario(entity) => v_flex()
.w(rems(34.))
.child(entity.clone())
.into_any_element(),
}
}
}
impl NewSessionMode {
fn attach(
debugger: Option<SharedString>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Self {
Self::Attach(AttachMode::new(debugger, workspace, window, cx))
}
fn launch(
past_launch_config: Option<LaunchRequest>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Self {
Self::Launch(LaunchMode::new(past_launch_config, window, cx))
}
}
fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let theme = cx.theme();
@ -519,6 +544,34 @@ impl Render for NewSessionModal {
h_flex()
.justify_start()
.w_full()
.child(
ToggleButton::new("debugger-session-ui-picker-button", "Scenarios")
.size(ButtonSize::Default)
.style(ui::ButtonStyle::Subtle)
.toggle_state(matches!(self.mode, NewSessionMode::Scenario(_)))
.on_click(cx.listener(|this, _, window, cx| {
let Some(task_store) = this
.workspace
.update(cx, |workspace, cx| {
workspace.project().read(cx).task_store().clone()
})
.ok()
else {
return;
};
this.mode = NewSessionMode::scenario(
this.debug_panel.clone(),
this.workspace.clone(),
task_store,
window,
cx,
);
cx.notify();
}))
.first(),
)
.child(
ToggleButton::new(
"debugger-session-ui-launch-button",
@ -532,7 +585,7 @@ impl Render for NewSessionModal {
this.mode.focus_handle(cx).focus(window);
cx.notify();
}))
.first(),
.middle(),
)
.child(
ToggleButton::new(
@ -601,10 +654,21 @@ impl Render for NewSessionModal {
})
.child(
Button::new("debugger-spawn", "Start")
.on_click(cx.listener(|this, _, window, cx| {
this.start_new_session(window, cx);
.on_click(cx.listener(|this, _, window, cx| match &this.mode {
NewSessionMode::Scenario(picker) => {
picker.update(cx, |picker, cx| {
picker.delegate.confirm(true, window, cx)
})
}
_ => this.start_new_session(window, cx),
}))
.disabled(self.debugger.is_none()),
.disabled(match self.mode {
NewSessionMode::Scenario(_) => !self.mode.has_match(cx),
NewSessionMode::Attach(_) => {
self.debugger.is_none() || !self.mode.has_match(cx)
}
NewSessionMode::Launch(_) => self.debugger.is_none(),
}),
),
),
)
@ -619,3 +683,319 @@ impl Focusable for NewSessionModal {
}
impl ModalView for NewSessionModal {}
// This module makes sure that the modes setup the correct subscriptions whenever they're created
mod session_modes {
use std::rc::Rc;
use super::*;
#[derive(Clone)]
#[non_exhaustive]
pub(super) struct LaunchMode {
pub(super) program: Entity<Editor>,
pub(super) cwd: Entity<Editor>,
}
impl LaunchMode {
pub(super) fn new(
past_launch_config: Option<LaunchRequest>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let (past_program, past_cwd) = past_launch_config
.map(|config| (Some(config.program), config.cwd))
.unwrap_or_else(|| (None, None));
let program = cx.new(|cx| Editor::single_line(window, cx));
program.update(cx, |this, cx| {
this.set_placeholder_text("Program path", cx);
if let Some(past_program) = past_program {
this.set_text(past_program, window, cx);
};
});
let cwd = cx.new(|cx| Editor::single_line(window, cx));
cwd.update(cx, |this, cx| {
this.set_placeholder_text("Working Directory", cx);
if let Some(past_cwd) = past_cwd {
this.set_text(past_cwd.to_string_lossy(), window, cx);
};
});
cx.new(|_| Self { program, cwd })
}
pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest {
let path = self.cwd.read(cx).text(cx);
task::LaunchRequest {
program: self.program.read(cx).text(cx),
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
args: Default::default(),
env: Default::default(),
}
}
}
#[derive(Clone)]
pub(super) struct AttachMode {
pub(super) definition: DebugTaskDefinition,
pub(super) attach_picker: Entity<AttachModal>,
_subscription: Rc<Subscription>,
}
impl AttachMode {
pub(super) fn new(
debugger: Option<SharedString>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Entity<Self> {
let definition = DebugTaskDefinition {
adapter: debugger.clone().unwrap_or_default(),
label: "Attach New Session Setup".into(),
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
initialize_args: None,
tcp_connection: None,
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
window.focus(&modal.focus_handle(cx));
modal
});
let subscription = cx.subscribe(&attach_picker, |_, _, _, cx| {
cx.emit(DismissEvent);
});
cx.new(|_| Self {
definition,
attach_picker,
_subscription: Rc::new(subscription),
})
}
pub(super) fn debug_task(&self) -> task::AttachRequest {
task::AttachRequest { process_id: None }
}
}
pub(super) struct DebugScenarioDelegate {
task_store: Entity<TaskStore>,
candidates: Option<Vec<(TaskSourceKind, DebugScenario)>>,
selected_index: usize,
matches: Vec<StringMatch>,
prompt: String,
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
}
impl DebugScenarioDelegate {
pub(super) fn new(
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
task_store: Entity<TaskStore>,
) -> Self {
Self {
task_store,
candidates: None,
selected_index: 0,
matches: Vec::new(),
prompt: String::new(),
debug_panel,
workspace,
}
}
}
impl PickerDelegate for DebugScenarioDelegate {
type ListItem = ui::ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<picker::Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
"".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<picker::Picker<Self>>,
) -> gpui::Task<()> {
let candidates: Vec<_> = match &self.candidates {
Some(candidates) => candidates
.into_iter()
.enumerate()
.map(|(index, (_, candidate))| {
StringMatchCandidate::new(index, candidate.label.as_ref())
})
.collect(),
None => {
let worktree_ids: Vec<_> = self
.workspace
.update(cx, |this, cx| {
this.visible_worktrees(cx)
.map(|tree| tree.read(cx).id())
.collect()
})
.ok()
.unwrap_or_default();
let scenarios: Vec<_> = self
.task_store
.read(cx)
.task_inventory()
.map(|item| item.read(cx).list_debug_scenarios(worktree_ids.into_iter()))
.unwrap_or_default();
self.candidates = Some(scenarios.clone());
scenarios
.into_iter()
.enumerate()
.map(|(index, (_, candidate))| {
StringMatchCandidate::new(index, candidate.label.as_ref())
})
.collect()
}
};
cx.spawn_in(window, async move |picker, cx| {
let matches = fuzzy::match_strings(
&candidates,
&query,
true,
1000,
&Default::default(),
cx.background_executor().clone(),
)
.await;
picker
.update(cx, |picker, _| {
let delegate = &mut picker.delegate;
delegate.matches = matches;
delegate.prompt = query;
if delegate.matches.is_empty() {
delegate.selected_index = 0;
} else {
delegate.selected_index =
delegate.selected_index.min(delegate.matches.len() - 1);
}
})
.log_err();
})
}
fn confirm(
&mut self,
_: bool,
window: &mut Window,
cx: &mut Context<picker::Picker<Self>>,
) {
let debug_scenario =
self.matches
.get(self.selected_index())
.and_then(|match_candidate| {
self.candidates
.as_ref()
.map(|candidates| candidates[match_candidate.candidate_id].clone())
});
let Some((task_source_kind, debug_scenario)) = debug_scenario else {
return;
};
let task_context = if let TaskSourceKind::Worktree {
id: worktree_id,
directory_in_worktree: _,
id_base: _,
} = task_source_kind
{
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, cx| {
workspace
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})
.ok()?
.await
.task_context_for_worktree_id(worktree_id)
.cloned()
})
} else {
gpui::Task::ready(None)
};
cx.spawn_in(window, async move |this, cx| {
let task_context = task_context.await.unwrap_or_default();
this.update_in(cx, |this, window, cx| {
this.delegate
.debug_panel
.update(cx, |panel, cx| {
panel.start_session(debug_scenario, task_context, None, window, cx);
})
.ok();
cx.emit(DismissEvent);
})
.ok();
})
.detach();
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
selected: bool,
window: &mut Window,
cx: &mut Context<picker::Picker<Self>>,
) -> Option<Self::ListItem> {
let hit = &self.matches[ix];
let highlighted_location = HighlightedMatch {
text: hit.string.clone(),
highlight_positions: hit.positions.clone(),
char_count: hit.string.chars().count(),
color: Color::Default,
};
let icon = Icon::new(IconName::FileTree)
.color(Color::Muted)
.size(ui::IconSize::Small);
Some(
ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
.inset(true)
.start_slot::<Icon>(icon)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(highlighted_location.render(window, cx)),
)
}
}
}