Allow .zed/tasks.json local configs (#8536)

![image](https://github.com/zed-industries/zed/assets/2690773/e1511777-b4ca-469e-8636-1e513b615368)

Follow-up of
https://github.com/zed-industries/zed/issues/7108#issuecomment-1960746397

Makes more clear where each task came from, auto (re)load
.zed/config.json changes, properly filtering out other worktree tasks.

Release Notes:

- Added local task configurations
This commit is contained in:
Kirill Bulatov 2024-02-29 01:18:13 +02:00 committed by GitHub
parent 7f954cbbb8
commit ac30ded80e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 715 additions and 281 deletions

View file

@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{path::PathBuf, sync::Arc};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
@ -6,11 +6,14 @@ use gpui::{
Model, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext,
VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
use project::Inventory;
use picker::{
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
Picker, PickerDelegate,
};
use project::{Inventory, ProjectPath, TaskSourceKind};
use task::{oneshot_source::OneshotSource, Task};
use ui::{v_flex, HighlightedLabel, ListItem, ListItemSpacing, Selectable, WindowContext};
use util::ResultExt;
use ui::{v_flex, ListItem, ListItemSpacing, RenderOnce, Selectable, WindowContext};
use util::{paths::PathExt, ResultExt};
use workspace::{ModalView, Workspace};
use crate::schedule_task;
@ -20,7 +23,7 @@ actions!(task, [Spawn, Rerun]);
/// A modal used to spawn new tasks.
pub(crate) struct TasksModalDelegate {
inventory: Model<Inventory>,
candidates: Vec<Arc<dyn Task>>,
candidates: Vec<(TaskSourceKind, Arc<dyn Task>)>,
matches: Vec<StringMatch>,
selected_index: usize,
workspace: WeakView<Workspace>,
@ -51,6 +54,21 @@ impl TasksModalDelegate {
)
})
}
fn active_item_path(
&mut self,
cx: &mut ViewContext<'_, Picker<Self>>,
) -> Option<(PathBuf, ProjectPath)> {
let workspace = self.workspace.upgrade()?.read(cx);
let project = workspace.project().read(cx);
let active_item = workspace.active_item(cx)?;
active_item.project_path(cx).and_then(|project_path| {
project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path().join(&project_path.path))
.zip(Some(project_path))
})
}
}
pub(crate) struct TasksModal {
@ -130,16 +148,22 @@ impl PickerDelegate for TasksModalDelegate {
cx.spawn(move |picker, mut cx| async move {
let Some(candidates) = picker
.update(&mut cx, |picker, cx| {
picker.delegate.candidates = picker
.delegate
.inventory
.update(cx, |inventory, cx| inventory.list_tasks(None, true, cx));
let (path, worktree) = match picker.delegate.active_item_path(cx) {
Some((abs_path, project_path)) => {
(Some(abs_path), Some(project_path.worktree_id))
}
None => (None, None),
};
picker.delegate.candidates =
picker.delegate.inventory.update(cx, |inventory, cx| {
inventory.list_tasks(path.as_deref(), worktree, true, cx)
});
picker
.delegate
.candidates
.iter()
.enumerate()
.map(|(index, candidate)| StringMatchCandidate {
.map(|(index, (_, candidate))| StringMatchCandidate {
id: index,
char_bag: candidate.name().chars().collect(),
string: candidate.name().into(),
@ -178,7 +202,6 @@ impl PickerDelegate for TasksModalDelegate {
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
let current_match_index = self.selected_index();
let task = if secondary {
if !self.prompt.trim().is_empty() {
self.spawn_oneshot(cx)
@ -188,7 +211,7 @@ impl PickerDelegate for TasksModalDelegate {
} else {
self.matches.get(current_match_index).map(|current_match| {
let ix = current_match.candidate_id;
self.candidates[ix].clone()
self.candidates[ix].1.clone()
})
};
@ -212,16 +235,35 @@ impl PickerDelegate for TasksModalDelegate {
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<picker::Picker<Self>>,
cx: &mut ViewContext<picker::Picker<Self>>,
) -> Option<Self::ListItem> {
let hit = &self.matches[ix];
let highlights: Vec<_> = hit.positions.iter().copied().collect();
let (source_kind, _) = &self.candidates[hit.candidate_id];
let details = match source_kind {
TaskSourceKind::UserInput => "user input".to_string(),
TaskSourceKind::Worktree { abs_path, .. } | TaskSourceKind::AbsPath(abs_path) => {
abs_path.compact().to_string_lossy().to_string()
}
};
let highlighted_location = HighlightedMatchWithPaths {
match_label: HighlightedText {
text: hit.string.clone(),
highlight_positions: hit.positions.clone(),
char_count: hit.string.chars().count(),
},
paths: vec![HighlightedText {
char_count: details.chars().count(),
highlight_positions: Vec::new(),
text: details,
}],
};
Some(
ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.start_slot(HighlightedLabel::new(hit.string.clone(), highlights)),
.child(highlighted_location.render(cx)),
)
}
}