1325 lines · 48.2 KB · Rust
RawDownload
use std::sync::Arc; use crate::TaskContexts;use fuzzy::{StringMatch, StringMatchCandidate};use gpui::{    Action, AnyElement, App, AppContext as _, Context, DismissEvent, Entity, EventEmitter,    Focusable, InteractiveElement, ParentElement, Render, SharedString, Styled, Subscription, Task,    WeakEntity, Window, rems,};use itertools::Itertools;use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};use project::{TaskSourceKind, task_store::TaskStore};use task::{    DebugRequestType, DebugTaskDefinition, ResolvedTask, RevealTarget, TaskContext, TaskModal,    TaskTemplate, TaskType,};use ui::{    ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon,    IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, Label, LabelSize,    ListItem, ListItemSpacing, RenderOnce, Toggleable, Tooltip, div, h_flex, v_flex,}; use util::{ResultExt, truncate_and_trailoff};use workspace::{ModalView, Workspace, tasks::schedule_resolved_task};pub use zed_actions::{Rerun, Spawn}; /// A modal used to spawn new tasks.pub(crate) struct TasksModalDelegate {    task_store: Entity<TaskStore>,    candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,    task_overrides: Option<TaskOverrides>,    last_used_candidate_index: Option<usize>,    divider_index: Option<usize>,    matches: Vec<StringMatch>,    selected_index: usize,    workspace: WeakEntity<Workspace>,    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.#[derive(Clone, Debug, Default, PartialEq, Eq)]pub(crate) struct TaskOverrides {    /// See [`RevealTarget`].    pub(crate) reveal_target: Option<RevealTarget>,} impl TasksModalDelegate {    fn new(        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 {            reveal_target: Some(RevealTarget::Center),        }) = &task_overrides        {            Arc::from("Find a task, or run a command in the central pane")        } else {            Arc::from("Find a task, or run a command")        };        Self {            task_store,            workspace,            candidates: None,            matches: Vec::new(),            last_used_candidate_index: None,            divider_index: None,            selected_index: 0,            prompt: String::default(),            task_contexts,            task_modal_type,            task_overrides,            placeholder_text,        }    }     fn spawn_oneshot(&mut self) -> Option<(TaskSourceKind, ResolvedTask)> {        if self.prompt.trim().is_empty() {            return None;        }         let default_context = TaskContext::default();        let active_context = self            .task_contexts            .active_context()            .unwrap_or(&default_context);        let source_kind = TaskSourceKind::UserInput;        let id_base = source_kind.to_id_base();        let mut new_oneshot = TaskTemplate {            label: self.prompt.clone(),            command: self.prompt.clone(),            ..TaskTemplate::default()        };        if let Some(TaskOverrides {            reveal_target: Some(reveal_target),        }) = &self.task_overrides        {            new_oneshot.reveal_target = *reveal_target;        }        Some((            source_kind,            new_oneshot.resolve_task(&id_base, active_context)?,        ))    }     fn delete_previously_used(&mut self, ix: usize, cx: &mut App) {        let Some(candidates) = self.candidates.as_mut() else {            return;        };        let Some(task) = candidates.get(ix).map(|(_, task)| task.clone()) else {            return;        };        // We remove this candidate manually instead of .taking() the candidates, as we already know the index;        // it doesn't make sense to requery the inventory for new candidates, as that's potentially costly and more often than not it should just return back        // the original list without a removed entry.        candidates.remove(ix);        if let Some(inventory) = self.task_store.read(cx).task_inventory().cloned() {            inventory.update(cx, |inventory, _| {                inventory.delete_previously_used(&task.id);            })        };    }} pub struct TasksModal {    picker: Entity<Picker<TasksModalDelegate>>,    _subscription: [Subscription; 2],} impl TasksModal {    pub(crate) fn new(        task_store: Entity<TaskStore>,        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,                ),                window,                cx,            )        });        let _subscription = [            cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {                cx.emit(DismissEvent);            }),            cx.subscribe(&picker, |_, _, event: &ShowAttachModal, cx| {                cx.emit(ShowAttachModal {                    debug_config: event.debug_config.clone(),                });            }),        ];        Self {            picker,            _subscription,        }    }} impl Render for TasksModal {    fn render(        &mut self,        _window: &mut Window,        _: &mut Context<Self>,    ) -> impl gpui::prelude::IntoElement {        v_flex()            .key_context("TasksModal")            .w(rems(34.))            .child(self.picker.clone())    }} pub struct ShowAttachModal {    pub debug_config: DebugTaskDefinition,} impl EventEmitter<DismissEvent> for TasksModal {}impl EventEmitter<ShowAttachModal> for TasksModal {}impl EventEmitter<ShowAttachModal> for Picker<TasksModalDelegate> {} impl Focusable for TasksModal {    fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle {        self.picker.read(cx).focus_handle(cx)    }} impl ModalView for TasksModal {} const MAX_TAGS_LINE_LEN: usize = 30; impl PickerDelegate for TasksModalDelegate {    type ListItem = 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, _: &mut App) -> Arc<str> {        self.placeholder_text.clone()    }     fn update_matches(        &mut self,        query: String,        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)),            None => {                if let Some(task_inventory) = self.task_store.read(cx).task_inventory().cloned() {                    let (used, current) = task_inventory                        .read(cx)                        .used_and_current_resolved_tasks(&self.task_contexts, cx);                    let workspace = self.workspace.clone();                    let lsp_task_sources = self.task_contexts.lsp_task_sources.clone();                    let task_position = self.task_contexts.latest_selection;                     cx.spawn(async move |picker, cx| {                        let Ok(lsp_tasks) = workspace.update(cx, |workspace, cx| {                            editor::lsp_tasks(                                workspace.project().clone(),                                &lsp_task_sources,                                task_position,                                cx,                            )                        }) else {                            return Vec::new();                        };                         let lsp_tasks = lsp_tasks.await;                        picker                            .update(cx, |picker, _| {                                picker.delegate.last_used_candidate_index = if used.is_empty() {                                    None                                } else {                                    Some(used.len() - 1)                                };                                 let mut new_candidates = used;                                new_candidates.extend(lsp_tasks.into_iter().flat_map(                                    |(kind, tasks_with_locations)| {                                        tasks_with_locations                                            .into_iter()                                            .sorted_by_key(|(location, task)| {                                                (location.is_none(), task.resolved_label.clone())                                            })                                            .map(move |(_, task)| (kind.clone(), task))                                    },                                ));                                new_candidates.extend(current);                                let match_candidates =                                    string_match_candidates(&new_candidates, task_type);                                let _ = picker.delegate.candidates.insert(new_candidates);                                match_candidates                            })                            .ok()                            .unwrap_or_default()                    })                } else {                    Task::ready(Vec::new())                }            }        };         cx.spawn_in(window, async move |picker, cx| {            let candidates = candidates.await;            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;                    if let Some(index) = delegate.last_used_candidate_index {                        delegate.matches.sort_by_key(|m| m.candidate_id > index);                    }                     delegate.prompt = query;                    delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| {                        let index = delegate                            .matches                            .partition_point(|matching_task| matching_task.candidate_id <= index);                        Some(index).and_then(|index| (index != 0).then(|| index - 1))                    });                     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,        omit_history_entry: bool,        _: &mut Window,        cx: &mut Context<picker::Picker<Self>>,    ) {        let current_match_index = self.selected_index();        let task = self            .matches            .get(current_match_index)            .and_then(|current_match| {                let ix = current_match.candidate_id;                self.candidates                    .as_ref()                    .map(|candidates| candidates[ix].clone())            });        let Some((task_source_kind, mut task)) = task else {            return;        };        if let Some(TaskOverrides {            reveal_target: Some(reveal_target),        }) = &self.task_overrides        {            if let Some(resolved_task) = &mut task.resolved {                resolved_task.reveal_target = *reveal_target;            }        }         match task.task_type() {            TaskType::Debug(config) if config.locator.is_none() => {                let Some(config): Option<DebugTaskDefinition> =                    task.resolved_debug_adapter_config()                else {                    return;                };                 match &config.request {                    DebugRequestType::Attach(attach_config)                        if attach_config.process_id.is_none() =>                    {                        cx.emit(ShowAttachModal {                            debug_config: config.clone(),                        });                        return;                    }                    _ => {                        self.workspace                            .update(cx, |workspace, cx| {                                workspace.project().update(cx, |project, cx| {                                    project                                        .start_debug_session(config, cx)                                        .detach_and_log_err(cx);                                });                            })                            .ok();                    }                }            }            _ => {                self.workspace                    .update(cx, |workspace, cx| {                        schedule_resolved_task(                            workspace,                            task_source_kind,                            task,                            omit_history_entry,                            cx,                        );                    })                    .ok();            }        };         cx.emit(DismissEvent);    }     fn dismissed(&mut self, _window: &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 candidates = self.candidates.as_ref()?;        let hit = &self.matches[ix];        let (source_kind, resolved_task) = &candidates.get(hit.candidate_id)?;        let template = resolved_task.original_task();        let display_label = resolved_task.display_label();         let mut tooltip_label_text = if display_label != &template.label {            resolved_task.resolved_label.clone()        } else {            String::new()        };        if let Some(resolved) = resolved_task.resolved.as_ref() {            if resolved.command_label != display_label                && resolved.command_label != resolved_task.resolved_label            {                if !tooltip_label_text.trim().is_empty() {                    tooltip_label_text.push('\n');                }                tooltip_label_text.push_str(&resolved.command_label);            }        }        if template.tags.len() > 0 {            tooltip_label_text.push('\n');            tooltip_label_text.push_str(                template                    .tags                    .iter()                    .map(|tag| format!("\n#{}", tag))                    .collect::<Vec<_>>()                    .join("")                    .as_str(),            );        }        let tooltip_label = if tooltip_label_text.trim().is_empty() {            None        } else {            Some(Tooltip::simple(tooltip_label_text, cx))        };         let highlighted_location = HighlightedMatch {            text: hit.string.clone(),            highlight_positions: hit.positions.clone(),            char_count: hit.string.chars().count(),            color: Color::Default,        };        let icon = match source_kind {            TaskSourceKind::Lsp(..) => Some(Icon::new(IconName::Bolt)),            TaskSourceKind::UserInput => Some(Icon::new(IconName::Terminal)),            TaskSourceKind::AbsPath { .. } => Some(Icon::new(IconName::Settings)),            TaskSourceKind::Worktree { .. } => Some(Icon::new(IconName::FileTree)),            TaskSourceKind::Language { name } => file_icons::FileIcons::get(cx)                .get_icon_for_type(&name.to_lowercase(), cx)                .map(Icon::from_path),        }        .map(|icon| icon.color(Color::Muted).size(IconSize::Small));        let history_run_icon = if Some(ix) <= self.divider_index {            Some(                Icon::new(IconName::HistoryRerun)                    .color(Color::Muted)                    .size(IconSize::Small)                    .into_any_element(),            )        } else {            Some(                v_flex()                    .flex_none()                    .size(IconSize::Small.rems())                    .into_any_element(),            )        };         Some(            ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))                .inset(true)                .start_slot::<Icon>(icon)                .end_slot::<AnyElement>(                    h_flex()                        .gap_1()                        .child(Label::new(truncate_and_trailoff(                            &template

Lines 1–500 of 1325.