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)),
)
}
}
}

View file

@ -588,7 +588,9 @@ impl<D: PickerDelegate> Picker<D> {
self.update_matches(query, window, cx);
}
editor::EditorEvent::Blurred => {
self.cancel(&menu::Cancel, window, cx);
if self.is_modal {
self.cancel(&menu::Cancel, window, cx);
}
}
_ => {}
}

View file

@ -179,6 +179,14 @@ impl TaskContexts {
})
.copied()
}
pub fn task_context_for_worktree_id(&self, worktree_id: WorktreeId) -> Option<&TaskContext> {
self.active_worktree_context
.iter()
.chain(self.other_worktree_contexts.iter())
.find(|(id, _)| *id == worktree_id)
.map(|(_, context)| context)
}
}
impl TaskSourceKind {
@ -206,13 +214,15 @@ impl Inventory {
cx.new(|_| Self::default())
}
pub fn list_debug_scenarios(&self, worktree: Option<WorktreeId>) -> Vec<DebugScenario> {
pub fn list_debug_scenarios(
&self,
worktrees: impl Iterator<Item = WorktreeId>,
) -> Vec<(TaskSourceKind, DebugScenario)> {
let global_scenarios = self.global_debug_scenarios_from_settings();
let worktree_scenarios = self.worktree_scenarios_from_settings(worktree);
worktree_scenarios
worktrees
.flat_map(|tree_id| self.worktree_scenarios_from_settings(Some(tree_id)))
.chain(global_scenarios)
.map(|(_, scenario)| scenario)
.collect()
}

View file

@ -19,7 +19,8 @@ pub use debug_format::{
AttachRequest, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, TcpArgumentsTemplate,
};
pub use task_template::{
DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates,
DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates,
substitute_all_template_variables_in_str,
};
pub use vscode_debug_format::VsCodeDebugTaskFile;
pub use vscode_format::VsCodeTaskFile;
@ -266,6 +267,10 @@ impl TaskVariables {
}
})
}
pub fn iter(&self) -> impl Iterator<Item = (&VariableName, &String)> {
self.0.iter()
}
}
impl FromIterator<(VariableName, String)> for TaskVariables {

View file

@ -83,15 +83,6 @@ pub enum DebugArgsRequest {
Attach(AttachRequest),
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// The type of task modal to spawn
pub enum TaskModal {
/// Show regular tasks
ScriptModal,
/// Show debug tasks
DebugModal,
}
/// What to do with the terminal pane and tab, after the command was started.
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
@ -302,7 +293,7 @@ fn to_hex_hash(object: impl Serialize) -> anyhow::Result<String> {
Ok(hex::encode(hasher.finalize()))
}
fn substitute_all_template_variables_in_str<A: AsRef<str>>(
pub fn substitute_all_template_variables_in_str<A: AsRef<str>>(
template_str: &str,
task_variables: &HashMap<String, A>,
variable_names: &HashMap<String, VariableName>,

View file

@ -10,7 +10,7 @@ use gpui::{
use itertools::Itertools;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{TaskSourceKind, task_store::TaskStore};
use task::{DebugScenario, ResolvedTask, RevealTarget, TaskContext, TaskModal, TaskTemplate};
use task::{DebugScenario, ResolvedTask, RevealTarget, TaskContext, TaskTemplate};
use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon,
IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, Label, LabelSize,
@ -34,8 +34,6 @@ pub(crate) struct TasksModalDelegate {
prompt: String,
task_contexts: TaskContexts,
placeholder_text: Arc<str>,
/// If this delegate is responsible for running a scripting task or a debugger
task_modal_type: TaskModal,
}
/// Task template amendments to do before resolving the context.
@ -50,7 +48,6 @@ impl TasksModalDelegate {
task_store: Entity<TaskStore>,
task_contexts: TaskContexts,
task_overrides: Option<TaskOverrides>,
task_modal_type: TaskModal,
workspace: WeakEntity<Workspace>,
) -> Self {
let placeholder_text = if let Some(TaskOverrides {
@ -71,7 +68,6 @@ impl TasksModalDelegate {
selected_index: 0,
prompt: String::default(),
task_contexts,
task_modal_type,
task_overrides,
placeholder_text,
}
@ -136,19 +132,12 @@ impl TasksModal {
task_contexts: TaskContexts,
task_overrides: Option<TaskOverrides>,
workspace: WeakEntity<Workspace>,
task_modal_type: TaskModal,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| {
Picker::uniform_list(
TasksModalDelegate::new(
task_store,
task_contexts,
task_overrides,
task_modal_type,
workspace,
),
TasksModalDelegate::new(task_store, task_contexts, task_overrides, workspace),
window,
cx,
)
@ -231,9 +220,8 @@ impl PickerDelegate for TasksModalDelegate {
window: &mut Window,
cx: &mut Context<picker::Picker<Self>>,
) -> Task<()> {
let task_type = self.task_modal_type.clone();
let candidates = match &self.candidates {
Some(candidates) => Task::ready(string_match_candidates(candidates, task_type)),
Some(candidates) => Task::ready(string_match_candidates(candidates)),
None => {
if let Some(task_inventory) = self.task_store.read(cx).task_inventory().cloned() {
let (used, current) = task_inventory
@ -276,8 +264,7 @@ impl PickerDelegate for TasksModalDelegate {
},
));
new_candidates.extend(current);
let match_candidates =
string_match_candidates(&new_candidates, task_type);
let match_candidates = string_match_candidates(&new_candidates);
let _ = picker.delegate.candidates.insert(new_candidates);
match_candidates
})
@ -669,12 +656,10 @@ impl PickerDelegate for TasksModalDelegate {
fn string_match_candidates<'a>(
candidates: impl IntoIterator<Item = &'a (TaskSourceKind, ResolvedTask)> + 'a,
task_modal_type: TaskModal,
) -> Vec<StringMatchCandidate> {
candidates
.into_iter()
.enumerate()
.filter(|(_, (_, _))| task_modal_type == TaskModal::ScriptModal)
.map(|(index, (_, candidate))| StringMatchCandidate::new(index, candidate.display_label()))
.collect()
}

View file

@ -5,9 +5,7 @@ use editor::Editor;
use gpui::{App, AppContext as _, Context, Entity, Task, Window};
use modal::TaskOverrides;
use project::{Location, TaskContexts, TaskSourceKind, Worktree};
use task::{
RevealTarget, TaskContext, TaskId, TaskModal, TaskTemplate, TaskVariables, VariableName,
};
use task::{RevealTarget, TaskContext, TaskId, TaskTemplate, TaskVariables, VariableName};
use workspace::Workspace;
mod modal;
@ -83,7 +81,7 @@ pub fn init(cx: &mut App) {
);
}
} else {
toggle_modal(workspace, None, TaskModal::ScriptModal, window, cx).detach();
toggle_modal(workspace, None, window, cx).detach();
};
});
},
@ -125,21 +123,15 @@ fn spawn_task_or_modal(
)
.detach_and_log_err(cx)
}
Spawn::ViaModal { reveal_target } => toggle_modal(
workspace,
*reveal_target,
TaskModal::ScriptModal,
window,
cx,
)
.detach(),
Spawn::ViaModal { reveal_target } => {
toggle_modal(workspace, *reveal_target, window, cx).detach()
}
}
}
pub fn toggle_modal(
workspace: &mut Workspace,
reveal_target: Option<RevealTarget>,
task_type: TaskModal,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Task<()> {
@ -162,7 +154,6 @@ pub fn toggle_modal(
reveal_target: Some(target),
}),
workspace_handle,
task_type,
window,
cx,
)