diff --git a/Cargo.lock b/Cargo.lock index d1072953b7074291f7b6ffdcd9ad0ab3c56846ef..44de9e533d611cc398c68cb78b800ce3f40e6b24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10088,6 +10088,7 @@ dependencies = [ "fuzzy", "gpui", "language", + "menu", "picker", "project", "schemars", diff --git a/assets/icons/history_rerun.svg b/assets/icons/history_rerun.svg new file mode 100644 index 0000000000000000000000000000000000000000..530465fe750ac43e496672df13250c2be97d9428 --- /dev/null +++ b/assets/icons/history_rerun.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index d60c6326bb53d8e96dfb77beb01c33ff6df15537..0d03767d8f5a21196b73803202824ac8bdc64116 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -19,9 +19,6 @@ "cmd-escape": "menu::Cancel", "ctrl-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", - "shift-enter": "picker::UseSelectedQuery", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }], - "cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }], "cmd-shift-w": "workspace::CloseWindow", "shift-escape": "workspace::ToggleZoom", "cmd-o": "workspace::Open", @@ -630,6 +627,14 @@ "ctrl-backspace": "tab_switcher::CloseSelectedItem" } }, + { + "context": "Picker", + "bindings": { + "alt-e": "picker::UseSelectedQuery", + "alt-enter": ["picker::ConfirmInput", { "secondary": false }], + "cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }] + } + }, { "context": "Terminal", "bindings": { diff --git a/crates/languages/src/bash.rs b/crates/languages/src/bash.rs index 5cdcf86a581276a13e91b8830941ebf6b767f88e..5bca10aee4639a2209c0c75d85fe6f449d1d3684 100644 --- a/crates/languages/src/bash.rs +++ b/crates/languages/src/bash.rs @@ -6,7 +6,6 @@ pub(super) fn bash_task_context() -> ContextProviderWithTasks { TaskTemplate { label: "execute selection".to_owned(), command: VariableName::SelectedText.template_value(), - ignore_previously_resolved: true, ..TaskTemplate::default() }, TaskTemplate { diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 858dafb71629efadbfc7bc6c63c4d9effd508ca6..0f5e0367f55bb104f3059a947513e6191f91713c 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -187,7 +187,6 @@ pub(super) fn python_task_context() -> ContextProviderWithTasks { label: "execute selection".to_owned(), command: "python3".to_owned(), args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()], - ignore_previously_resolved: true, ..TaskTemplate::default() }, TaskTemplate { diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index c4bf8dd7f545afbf0e7ca717150bc05720f4376d..ca48700a69ca9103db5bfb4c0d71ce4d8c5643b8 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -8,7 +8,7 @@ use std::{ use collections::{btree_map, BTreeMap, VecDeque}; use gpui::{AppContext, Context, Model, ModelContext}; -use itertools::{Either, Itertools}; +use itertools::Itertools; use language::Language; use task::{ static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, VariableName, @@ -188,7 +188,6 @@ impl Inventory { .last_scheduled_tasks .iter() .rev() - .filter(|(_, task)| !task.original_task().ignore_previously_resolved) .filter(|(task_kind, _)| { if matches!(task_kind, TaskSourceKind::Language { .. }) { Some(task_kind) == task_source_kind.as_ref() @@ -256,38 +255,46 @@ impl Inventory { tasks_by_label }, ); - tasks_by_label = currently_resolved_tasks.into_iter().fold( + tasks_by_label = currently_resolved_tasks.iter().fold( tasks_by_label, |mut tasks_by_label, (source, task, lru_score)| { - match tasks_by_label.entry((source, task.resolved_label.clone())) { + match tasks_by_label.entry((source.clone(), task.resolved_label.clone())) { btree_map::Entry::Occupied(mut o) => { let (previous_task, _) = o.get(); let new_template = task.original_task(); - if new_template.ignore_previously_resolved - || new_template != previous_task.original_task() - { - o.insert((task, lru_score)); + if new_template != previous_task.original_task() { + o.insert((task.clone(), *lru_score)); } } btree_map::Entry::Vacant(v) => { - v.insert((task, lru_score)); + v.insert((task.clone(), *lru_score)); } } tasks_by_label }, ); - tasks_by_label + let resolved = tasks_by_label .into_iter() .map(|((kind, _), (task, lru_score))| (kind, task, lru_score)) - .sorted_unstable_by(task_lru_comparator) - .partition_map(|(kind, task, lru_score)| { + .sorted_by(task_lru_comparator) + .filter_map(|(kind, task, lru_score)| { if lru_score < not_used_score { - Either::Left((kind, task)) + Some((kind, task)) } else { - Either::Right((kind, task)) + None } }) + .collect(); + + ( + resolved, + currently_resolved_tasks + .into_iter() + .sorted_unstable_by(task_lru_comparator) + .map(|(kind, task, _)| (kind, task)) + .collect(), + ) } /// Returns the last scheduled task, if any of the sources contains one with the matching id. @@ -334,6 +341,7 @@ fn task_lru_comparator( &task_b.resolved_label, )) .then(task_a.resolved_label.cmp(&task_b.resolved_label)) + .then(kind_a.cmp(kind_b)) }) } @@ -530,6 +538,7 @@ mod tests { assert_eq!( resolved_task_names(&inventory, None, cx), vec![ + "2_task".to_string(), "2_task".to_string(), "1_a_task".to_string(), "1_task".to_string(), @@ -548,6 +557,9 @@ mod tests { assert_eq!( resolved_task_names(&inventory, None, cx), vec![ + "3_task".to_string(), + "1_task".to_string(), + "2_task".to_string(), "3_task".to_string(), "1_task".to_string(), "2_task".to_string(), @@ -578,6 +590,9 @@ mod tests { assert_eq!( resolved_task_names(&inventory, None, cx), vec![ + "3_task".to_string(), + "1_task".to_string(), + "2_task".to_string(), "3_task".to_string(), "1_task".to_string(), "2_task".to_string(), @@ -595,6 +610,10 @@ mod tests { assert_eq!( resolved_task_names(&inventory, None, cx), vec![ + "11_hello".to_string(), + "3_task".to_string(), + "1_task".to_string(), + "2_task".to_string(), "11_hello".to_string(), "3_task".to_string(), "1_task".to_string(), diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index f2e974a3a2f1829c382687ffe49b083e241d7b86..efd9556366c274496a294c96d23c7a70821f9c13 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -39,20 +39,6 @@ pub struct TaskTemplate { /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish. #[serde(default)] pub allow_concurrent_runs: bool, - // Tasks like "execute the selection" better have the constant labels (to avoid polluting the history with temporary tasks), - // and always use the latest context with the latest selection. - // - // Current impl will always pick previously spawned tasks on full label conflict in the tasks modal and terminal tabs, never - // getting the latest selection for them. - // This flag inverts the behavior, effectively removing all previously spawned tasks from history, - // if their full labels are the same as the labels of the newly resolved tasks. - // Such tasks are still re-runnable, and will use the old context in that case (unless the rerun task forces this). - // - // Current approach is relatively hacky, a better way is understand when the new resolved tasks needs a rerun, - // and replace the historic task accordingly. - #[doc(hidden)] - #[serde(default)] - pub ignore_previously_resolved: bool, /// What to do with the terminal pane and tab, after the command was started: /// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default) /// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml index ca6af81ccd4f593200b14027f59829e029ca4847..d92437d017b329c2aa3e003552f17793a5e2515e 100644 --- a/crates/tasks_ui/Cargo.toml +++ b/crates/tasks_ui/Cargo.toml @@ -13,6 +13,7 @@ editor.workspace = true file_icons.workspace = true fuzzy.workspace = true gpui.workspace = true +menu.workspace = true picker.workspace = true project.workspace = true task.workspace = true diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 1a67278620d3142d5a5c12387ed59ca7b9fd22a4..6ed524972efe526d601a455e7c114357b56fc55e 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -3,17 +3,18 @@ use std::sync::Arc; use crate::active_item_selection_properties; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - impl_actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement, - Model, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext, - VisualContext, WeakView, + impl_actions, rems, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView, + InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, View, + ViewContext, VisualContext, WeakView, }; use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate}; use project::{Inventory, TaskSourceKind}; use task::{ResolvedTask, TaskContext, TaskTemplate}; use ui::{ - div, v_flex, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, IconButton, - IconButtonShape, IconName, IconSize, ListItem, ListItemSpacing, RenderOnce, Selectable, - Tooltip, WindowContext, + div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, + FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, + KeyBinding, LabelSize, ListItem, ListItemSpacing, RenderOnce, Selectable, Tooltip, + WindowContext, }; use util::ResultExt; use workspace::{tasks::schedule_resolved_task, ModalView, Workspace}; @@ -87,7 +88,7 @@ impl TasksModalDelegate { selected_index: 0, prompt: String::default(), task_context, - placeholder_text: Arc::from("Run a task..."), + placeholder_text: Arc::from("Find a task, or run a command"), } } @@ -352,12 +353,37 @@ impl PickerDelegate for TasksModalDelegate { TaskSourceKind::Language { name } => file_icons::FileIcons::get(cx) .get_type_icon(&name.to_lowercase()) .map(|icon_path| Icon::from_path(icon_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) + .inset(false) + .start_slot::(icon) + .end_slot::(history_run_icon) .spacing(ListItemSpacing::Sparse) + // .map(|this| { + // if Some(ix) <= self.divider_index { + // this.start_slot(Icon::new(IconName::HistoryRerun).size(IconSize::Small)) + // } else { + // this.start_slot(v_flex().flex_none().size(IconSize::Small.rems())) + // } + // }) .when_some(tooltip_label, |list_item, item_label| { list_item.tooltip(move |_| item_label.clone()) }) @@ -392,11 +418,7 @@ impl PickerDelegate for TasksModalDelegate { } else { item }; - if let Some(icon) = icon { - item.end_slot(icon) - } else { - item - } + item }) .selected(selected) .child(highlighted_location.render(cx)), @@ -429,6 +451,80 @@ impl PickerDelegate for TasksModalDelegate { Vec::new() } } + fn render_footer(&self, cx: &mut ViewContext>) -> Option { + let is_recent_selected = self.divider_index >= Some(self.selected_index); + let current_modifiers = cx.modifiers(); + Some( + h_flex() + .w_full() + .h_8() + .p_2() + .justify_between() + .rounded_b_md() + .bg(cx.theme().colors().ghost_element_selected) + .children( + KeyBinding::for_action(&picker::UseSelectedQuery, cx).map(|keybind| { + let edit_entry_label = if is_recent_selected { + "Edit task" + } else if !self.matches.is_empty() { + "Edit template" + } else { + "Rerun last task" + }; + + Button::new("edit-current-task", edit_entry_label) + .label_size(LabelSize::Small) + .key_binding(keybind) + }), + ) + .map(|this| { + if current_modifiers.alt || self.matches.is_empty() { + this.children( + KeyBinding::for_action( + &picker::ConfirmInput { + secondary: current_modifiers.secondary(), + }, + 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) + }), + ) + } 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) + }, + )) + } 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) + })) + } + }) + .into_any_element(), + ) + } } #[cfg(test)] @@ -787,7 +883,7 @@ mod tests { let tasks_picker = open_spawn_tasks(&workspace, cx); assert_eq!( task_names(&tasks_picker, cx), - vec!["TypeScript task from file /dir/a1.ts", "Another task from file /dir/a1.ts", "Task without variables"], + vec!["TypeScript task from file /dir/a1.ts", "TypeScript task from file /dir/a1.ts", "Another task from file /dir/a1.ts", "Task without variables"], "After spawning the task and getting it into the history, it should be up in the sort as recently used" ); tasks_picker.update(cx, |_, cx| { diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 2215d3dd75826bc07e6155808bc285b912397d45..77710a230977230c951fea7282b72a31e5238ff1 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -182,6 +182,7 @@ pub enum IconName { ZedXCopilot, ZedAssistant, PullRequest, + HistoryRerun, } impl IconName { @@ -295,6 +296,7 @@ impl IconName { IconName::ZedXCopilot => "icons/zed_x_copilot.svg", IconName::ZedAssistant => "icons/zed_assistant.svg", IconName::PullRequest => "icons/pull_request.svg", + IconName::HistoryRerun => "icons/history_rerun.svg", } } }