1025 lines · 37.4 KB · Rust
RawDownload
use std::sync::Arc; use crate::active_item_selection_properties;use fuzzy::{StringMatch, StringMatchCandidate};use gpui::{    rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView,    InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, Task,    View, ViewContext, VisualContext, WeakView,};use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate};use project::{task_store::TaskStore, TaskSourceKind};use task::{ResolvedTask, TaskContext, TaskTemplate};use ui::{    div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,    FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement,    KeyBinding, LabelSize, ListItem, ListItemSpacing, RenderOnce, Toggleable, Tooltip,    WindowContext,};use util::ResultExt;use workspace::{tasks::schedule_resolved_task, ModalView, Workspace};pub use zed_actions::{Rerun, Spawn}; /// A modal used to spawn new tasks.pub(crate) struct TasksModalDelegate {    task_store: Model<TaskStore>,    candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,    last_used_candidate_index: Option<usize>,    divider_index: Option<usize>,    matches: Vec<StringMatch>,    selected_index: usize,    workspace: WeakView<Workspace>,    prompt: String,    task_context: TaskContext,    placeholder_text: Arc<str>,} impl TasksModalDelegate {    fn new(        task_store: Model<TaskStore>,        task_context: TaskContext,        workspace: WeakView<Workspace>,    ) -> Self {        Self {            task_store,            workspace,            candidates: None,            matches: Vec::new(),            last_used_candidate_index: None,            divider_index: None,            selected_index: 0,            prompt: String::default(),            task_context,            placeholder_text: Arc::from("Find a task, or run a command"),        }    }     fn spawn_oneshot(&mut self) -> Option<(TaskSourceKind, ResolvedTask)> {        if self.prompt.trim().is_empty() {            return None;        }         let source_kind = TaskSourceKind::UserInput;        let id_base = source_kind.to_id_base();        let new_oneshot = TaskTemplate {            label: self.prompt.clone(),            command: self.prompt.clone(),            ..TaskTemplate::default()        };        Some((            source_kind,            new_oneshot.resolve_task(&id_base, &self.task_context)?,        ))    }     fn delete_previously_used(&mut self, ix: usize, cx: &mut AppContext) {        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(crate) struct TasksModal {    picker: View<Picker<TasksModalDelegate>>,    _subscription: Subscription,} impl TasksModal {    pub(crate) fn new(        task_store: Model<TaskStore>,        task_context: TaskContext,        workspace: WeakView<Workspace>,        cx: &mut ViewContext<Self>,    ) -> Self {        let picker = cx.new_view(|cx| {            Picker::uniform_list(                TasksModalDelegate::new(task_store, task_context, workspace),                cx,            )        });        let _subscription = cx.subscribe(&picker, |_, _, _, cx| {            cx.emit(DismissEvent);        });        Self {            picker,            _subscription,        }    }} impl Render for TasksModal {    fn render(&mut self, _: &mut ViewContext<Self>) -> impl gpui::prelude::IntoElement {        v_flex()            .key_context("TasksModal")            .w(rems(34.))            .child(self.picker.clone())    }} impl EventEmitter<DismissEvent> for TasksModal {} impl FocusableView for TasksModal {    fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {        self.picker.read(cx).focus_handle(cx)    }} impl ModalView for TasksModal {} 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, _cx: &mut ViewContext<picker::Picker<Self>>) {        self.selected_index = ix;    }     fn placeholder_text(&self, _: &mut WindowContext) -> Arc<str> {        self.placeholder_text.clone()    }     fn update_matches(        &mut self,        query: String,        cx: &mut ViewContext<picker::Picker<Self>>,    ) -> Task<()> {        cx.spawn(move |picker, mut cx| async move {            let Some(candidates) = picker                .update(&mut cx, |picker, cx| {                    match &mut picker.delegate.candidates {                        Some(candidates) => string_match_candidates(candidates.iter()),                        None => {                            let Ok((worktree, location)) =                                picker.delegate.workspace.update(cx, |workspace, cx| {                                    active_item_selection_properties(workspace, cx)                                })                            else {                                return Vec::new();                            };                            let Some(task_inventory) = picker                                .delegate                                .task_store                                .read(cx)                                .task_inventory()                                .cloned()                            else {                                return Vec::new();                            };                             let (used, current) =                                task_inventory.read(cx).used_and_current_resolved_tasks(                                    worktree,                                    location,                                    &picker.delegate.task_context,                                    cx,                                );                            picker.delegate.last_used_candidate_index = if used.is_empty() {                                None                            } else {                                Some(used.len() - 1)                            };                             let mut new_candidates = used;                            new_candidates.extend(current);                            let match_candidates = string_match_candidates(new_candidates.iter());                            let _ = picker.delegate.candidates.insert(new_candidates);                            match_candidates                        }                    }                })                .ok()            else {                return;            };            let matches = fuzzy::match_strings(                &candidates,                &query,                true,                1000,                &Default::default(),                cx.background_executor().clone(),            )            .await;            picker                .update(&mut 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, cx: &mut ViewContext<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, task)) = task else {            return;        };         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, cx: &mut ViewContext<picker::Picker<Self>>) {        cx.emit(DismissEvent);    }     fn render_match(        &self,        ix: usize,        selected: bool,        cx: &mut ViewContext<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);            }        }        let tooltip_label = if tooltip_label_text.trim().is_empty() {            None        } else {            Some(Tooltip::text(tooltip_label_text, cx))        };         let highlighted_location = HighlightedText {            text: hit.string.clone(),            highlight_positions: hit.positions.clone(),            char_count: hit.string.chars().count(),            color: Color::Default,        };        let icon = match source_kind {            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_type_icon(&name.to_lowercase())                .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>(history_run_icon)                .spacing(ListItemSpacing::Sparse)                .when_some(tooltip_label, |list_item, item_label| {                    list_item.tooltip(move |_| item_label.clone())                })                .map(|item| {                    let item = if matches!(source_kind, TaskSourceKind::UserInput)                        || Some(ix) <= self.divider_index                    {                        let task_index = hit.candidate_id;                        let delete_button = div().child(                            IconButton::new("delete", IconName::Close)                                .shape(IconButtonShape::Square)                                .icon_color(Color::Muted)                                .size(ButtonSize::None)                                .icon_size(IconSize::XSmall)                                .on_click(cx.listener(move |picker, _event, cx| {                                    cx.stop_propagation();                                    cx.prevent_default();                                     picker.delegate.delete_previously_used(task_index, cx);                                    picker.delegate.last_used_candidate_index = picker                                        .delegate                                        .last_used_candidate_index                                        .unwrap_or(0)                                        .checked_sub(1);                                    picker.refresh(cx);                                }))                                .tooltip(|cx| {                                    Tooltip::text("Delete Previously Scheduled Task", cx)                                }),                        );                        item.end_hover_slot(delete_button)                    } else {                        item                    };                    item                })                .toggle_state(selected)                .child(highlighted_location.render(cx)),        )    }     fn confirm_completion(        &mut self,        _: String,        _: &mut ViewContext<Picker<Self>>,    ) -> Option<String> {        let task_index = self.matches.get(self.selected_index())?.candidate_id;        let tasks = self.candidates.as_ref()?;        let (_, task) = tasks.get(task_index)?;        Some(task.resolved.as_ref()?.command_label.clone())    }     fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext<Picker<Self>>) {        let Some((task_source_kind, task)) = self.spawn_oneshot() else {            return;        };        self.workspace            .update(cx, |workspace, cx| {                schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx);            })            .ok();        cx.emit(DismissEvent);    }     fn separators_after_indices(&self) -> Vec<usize> {        if let Some(i) = self.divider_index {            vec![i]        } else {            Vec::new()        }    }    fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {        let is_recent_selected = self.divider_index >= Some(self.selected_index);        let current_modifiers = cx.modifiers();        let left_button = if self            .task_store            .read(cx)            .task_inventory()?            .read(cx)            .last_scheduled_task(None)            .is_some()        {            Some(("Rerun Last Task", Rerun::default().boxed_clone()))        } else {            None        };        Some(            h_flex()                .w_full()                .h_8()                .p_2()                .justify_between()                .rounded_b_md()                .bg(cx.theme().colors().ghost_element_selected)                .border_t_1()                .border_color(cx.theme().colors().border_variant)                .child(                    left_button                        .map(|(label, action)| {                            let keybind = KeyBinding::for_action(&*action, cx);                             Button::new("edit-current-task", label)                                .label_size(LabelSize::Small)                                .when_some(keybind, |this, keybind| this.key_binding(keybind))                                .on_click(move |_, cx| {                                    cx.dispatch_action(action.boxed_clone());                                })                                .into_any_element()                        })                        .unwrap_or_else(|| h_flex().into_any_element()),                )                .map(|this| {                    if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty()                    {                        let action = picker::ConfirmInput {                            secondary: current_modifiers.secondary(),                        }                        .boxed_clone();                        this.children(KeyBinding::for_action(&*action, cx).map(|keybind| {                            let spawn_oneshot_label = if current_modifiers.secondary() {                                "Spawn Oneshot Without History"                            } else {                                "Spawn Oneshot"                            };                             Button::new("spawn-onehshot", spawn_oneshot_label)                                .label_size(LabelSize::Small)                                .key_binding(keybind)                                .on_click(move |_, cx| cx.dispatch_action(action.boxed_clone()))                        }))                    } else if current_modifiers.secondary() {                        this.children(KeyBinding::for_action(&menu::SecondaryConfirm, cx).map(                            |keybind| {                                let label = if is_recent_selected {                                    "Rerun Without History"                                } else {                                    "Spawn Without History"                                };                                Button::new("spawn", label)                                    .label_size(LabelSize::Small)                                    .key_binding(keybind)                                    .on_click(move |_, cx| {                                        cx.dispatch_action(menu::SecondaryConfirm.boxed_clone())                                    })                            },                        ))                    } else {                        this.children(KeyBinding::for_action(&menu::Confirm, cx).map(|keybind| {                            let run_entry_label =                                if is_recent_selected { "Rerun" } else { "Spawn" };                             Button::new("spawn", run_entry_label)                                .label_size(LabelSize::Small)                                .key_binding(keybind)                                .on_click(|_, cx| {

Lines 1–500 of 1025.