Detailed changes
@@ -3394,6 +3394,7 @@ dependencies = [
"smol",
"snippet",
"sum_tree",
+ "task",
"text",
"theme",
"time",
@@ -9875,6 +9876,7 @@ dependencies = [
"futures 0.3.28",
"gpui",
"hex",
+ "parking_lot",
"schemars",
"serde",
"serde_json_lenient",
@@ -9887,7 +9889,6 @@ dependencies = [
name = "tasks_ui"
version = "0.1.0"
dependencies = [
- "anyhow",
"editor",
"file_icons",
"fuzzy",
@@ -667,7 +667,7 @@ async fn test_collaborating_with_code_actions(
editor_b.update(cx_b, |editor, cx| {
editor.toggle_code_actions(
&ToggleCodeActions {
- deployed_from_indicator: false,
+ deployed_from_indicator: None,
},
cx,
);
@@ -60,6 +60,7 @@ smallvec.workspace = true
smol.workspace = true
snippet.workspace = true
sum_tree.workspace = true
+task.workspace = true
text.workspace = true
time.workspace = true
time_format.workspace = true
@@ -53,7 +53,13 @@ pub struct SelectToEndOfLine {
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleCodeActions {
#[serde(default)]
- pub deployed_from_indicator: bool,
+ pub deployed_from_indicator: Option<u32>,
+}
+
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct ToggleTestRunner {
+ #[serde(default)]
+ pub deployed_from_row: Option<u32>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
@@ -34,6 +34,7 @@ mod persistence;
mod rust_analyzer_ext;
pub mod scroll;
mod selections_collection;
+pub mod tasks;
#[cfg(test)]
mod editor_tests;
@@ -78,6 +79,7 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use inline_completion_provider::*;
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
+use language::Runnable;
use language::{
char_kind,
language_settings::{self, all_language_settings, InlayHintSettings},
@@ -85,6 +87,7 @@ use language::{
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
Point, Selection, SelectionGoal, TransactionId,
};
+use task::{ResolvedTask, TaskTemplate};
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
use lsp::{DiagnosticSeverity, LanguageServerId};
@@ -99,7 +102,8 @@ use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::project_settings::{GitGutterSetting, ProjectSettings};
use project::{
- CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction,
+ CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath,
+ ProjectTransaction, TaskSourceKind, WorktreeId,
};
use rand::prelude::*;
use rpc::{proto::*, ErrorExt};
@@ -395,6 +399,19 @@ impl Default for ScrollbarMarkerState {
}
}
+#[derive(Clone)]
+struct RunnableTasks {
+ templates: SmallVec<[(TaskSourceKind, TaskTemplate); 1]>,
+ // We need the column at which the task context evaluation should take place.
+ column: u32,
+}
+
+#[derive(Clone)]
+struct ResolvedTasks {
+ templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
+ position: text::Point,
+}
+
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
///
/// See the [module level documentation](self) for more information.
@@ -487,6 +504,7 @@ pub struct Editor {
>,
last_bounds: Option<Bounds<Pixels>>,
expect_bounds_change: Option<Bounds<Pixels>>,
+ tasks: HashMap<u32, RunnableTasks>,
}
#[derive(Clone)]
@@ -1180,12 +1198,106 @@ impl CompletionsMenu {
}
#[derive(Clone)]
+struct CodeActionContents {
+ tasks: Option<Arc<ResolvedTasks>>,
+ actions: Option<Arc<[CodeAction]>>,
+}
+
+impl CodeActionContents {
+ fn len(&self) -> usize {
+ match (&self.tasks, &self.actions) {
+ (Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
+ (Some(tasks), None) => tasks.templates.len(),
+ (None, Some(actions)) => actions.len(),
+ (None, None) => 0,
+ }
+ }
+
+ fn is_empty(&self) -> bool {
+ match (&self.tasks, &self.actions) {
+ (Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
+ (Some(tasks), None) => tasks.templates.is_empty(),
+ (None, Some(actions)) => actions.is_empty(),
+ (None, None) => true,
+ }
+ }
+
+ fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
+ self.tasks
+ .iter()
+ .flat_map(|tasks| {
+ tasks
+ .templates
+ .iter()
+ .map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
+ })
+ .chain(self.actions.iter().flat_map(|actions| {
+ actions
+ .iter()
+ .map(|action| CodeActionsItem::CodeAction(action.clone()))
+ }))
+ }
+ fn get(&self, index: usize) -> Option<CodeActionsItem> {
+ match (&self.tasks, &self.actions) {
+ (Some(tasks), Some(actions)) => {
+ if index < tasks.templates.len() {
+ tasks
+ .templates
+ .get(index)
+ .cloned()
+ .map(|(kind, task)| CodeActionsItem::Task(kind, task))
+ } else {
+ actions
+ .get(index - tasks.templates.len())
+ .cloned()
+ .map(CodeActionsItem::CodeAction)
+ }
+ }
+ (Some(tasks), None) => tasks
+ .templates
+ .get(index)
+ .cloned()
+ .map(|(kind, task)| CodeActionsItem::Task(kind, task)),
+ (None, Some(actions)) => actions.get(index).cloned().map(CodeActionsItem::CodeAction),
+ (None, None) => None,
+ }
+ }
+}
+
+#[allow(clippy::large_enum_variant)]
+#[derive(Clone)]
+enum CodeActionsItem {
+ Task(TaskSourceKind, ResolvedTask),
+ CodeAction(CodeAction),
+}
+
+impl CodeActionsItem {
+ fn as_task(&self) -> Option<&ResolvedTask> {
+ let Self::Task(_, task) = self else {
+ return None;
+ };
+ Some(task)
+ }
+ fn as_code_action(&self) -> Option<&CodeAction> {
+ let Self::CodeAction(action) = self else {
+ return None;
+ };
+ Some(action)
+ }
+ fn label(&self) -> String {
+ match self {
+ Self::CodeAction(action) => action.lsp_action.title.clone(),
+ Self::Task(_, task) => task.resolved_label.clone(),
+ }
+ }
+}
+
struct CodeActionsMenu {
- actions: Arc<[CodeAction]>,
+ actions: CodeActionContents,
buffer: Model<Buffer>,
selected_item: usize,
scroll_handle: UniformListScrollHandle,
- deployed_from_indicator: bool,
+ deployed_from_indicator: Option<u32>,
}
impl CodeActionsMenu {
@@ -1240,8 +1352,10 @@ impl CodeActionsMenu {
"code_actions_menu",
self.actions.len(),
move |_this, range, cx| {
- actions[range.clone()]
+ actions
.iter()
+ .skip(range.start)
+ .take(range.end - range.start)
.enumerate()
.map(|(ix, action)| {
let item_ix = range.start + ix;
@@ -1260,23 +1374,42 @@ impl CodeActionsMenu {
.bg(colors.element_hover)
.text_color(colors.text_accent)
})
- .on_mouse_down(
- MouseButton::Left,
- cx.listener(move |editor, _, cx| {
- cx.stop_propagation();
- if let Some(task) = editor.confirm_code_action(
- &ConfirmCodeAction {
- item_ix: Some(item_ix),
- },
- cx,
- ) {
- task.detach_and_log_err(cx)
- }
- }),
- )
.whitespace_nowrap()
- // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
- .child(SharedString::from(action.lsp_action.title.clone()))
+ .when_some(action.as_code_action(), |this, action| {
+ this.on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |editor, _, cx| {
+ cx.stop_propagation();
+ if let Some(task) = editor.confirm_code_action(
+ &ConfirmCodeAction {
+ item_ix: Some(item_ix),
+ },
+ cx,
+ ) {
+ task.detach_and_log_err(cx)
+ }
+ }),
+ )
+ // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
+ .child(SharedString::from(action.lsp_action.title.clone()))
+ })
+ .when_some(action.as_task(), |this, task| {
+ this.on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |editor, _, cx| {
+ cx.stop_propagation();
+ if let Some(task) = editor.confirm_code_action(
+ &ConfirmCodeAction {
+ item_ix: Some(item_ix),
+ },
+ cx,
+ ) {
+ task.detach_and_log_err(cx)
+ }
+ }),
+ )
+ .child(SharedString::from(task.resolved_label.clone()))
+ })
})
.collect()
},
@@ -1291,16 +1424,20 @@ impl CodeActionsMenu {
self.actions
.iter()
.enumerate()
- .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
+ .max_by_key(|(_, action)| match action {
+ CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
+ CodeActionsItem::CodeAction(action) => action.lsp_action.title.chars().count(),
+ })
.map(|(ix, _)| ix),
)
.into_any_element();
- let cursor_position = if self.deployed_from_indicator {
- ContextMenuOrigin::GutterIndicator(cursor_position.row())
+ let cursor_position = if let Some(row) = self.deployed_from_indicator {
+ ContextMenuOrigin::GutterIndicator(row)
} else {
ContextMenuOrigin::EditorPoint(cursor_position)
};
+
(cursor_position, element)
}
}
@@ -1532,6 +1669,7 @@ impl Editor {
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
blame: None,
blame_subscription: None,
+ tasks: Default::default(),
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
@@ -3687,38 +3825,131 @@ impl Editor {
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
let mut context_menu = self.context_menu.write();
- if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) {
- *context_menu = None;
- cx.notify();
- return;
+ if let Some(ContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
+ if code_actions.deployed_from_indicator == action.deployed_from_indicator {
+ // Toggle if we're selecting the same one
+ *context_menu = None;
+ cx.notify();
+ return;
+ } else {
+ // Otherwise, clear it and start a new one
+ *context_menu = None;
+ cx.notify();
+ }
}
drop(context_menu);
let deployed_from_indicator = action.deployed_from_indicator;
let mut task = self.code_actions_task.take();
+ let action = action.clone();
cx.spawn(|this, mut cx| async move {
while let Some(prev_task) = task {
prev_task.await;
task = this.update(&mut cx, |this, _| this.code_actions_task.take())?;
}
- this.update(&mut cx, |this, cx| {
+ let spawned_test_task = this.update(&mut cx, |this, cx| {
if this.focus_handle.is_focused(cx) {
- if let Some((buffer, actions)) = this.available_code_actions.clone() {
- this.completion_tasks.clear();
- this.discard_inline_completion(cx);
- *this.context_menu.write() =
- Some(ContextMenu::CodeActions(CodeActionsMenu {
- buffer,
- actions,
- selected_item: Default::default(),
- scroll_handle: UniformListScrollHandle::default(),
- deployed_from_indicator,
- }));
- cx.notify();
+ let row = action
+ .deployed_from_indicator
+ .unwrap_or_else(|| this.selections.newest::<Point>(cx).head().row);
+ let tasks = this.tasks.get(&row).map(|t| Arc::new(t.to_owned()));
+ let (buffer, code_actions) = this
+ .available_code_actions
+ .clone()
+ .map(|(buffer, code_actions)| {
+ let snapshot = buffer.read(cx).snapshot();
+ let code_actions: Arc<[CodeAction]> = code_actions
+ .into_iter()
+ .filter(|action| {
+ text::ToPoint::to_point(&action.range.start, &snapshot).row
+ == row
+ })
+ .cloned()
+ .collect();
+ (buffer, code_actions)
+ })
+ .unzip();
+
+ if tasks.is_none() && code_actions.is_none() {
+ return None;
}
+ let buffer = buffer.or_else(|| {
+ let snapshot = this.snapshot(cx);
+ let (buffer_snapshot, _) =
+ snapshot.buffer_snapshot.buffer_line_for_row(row)?;
+ let buffer_id = buffer_snapshot.remote_id();
+ this.buffer().read(cx).buffer(buffer_id)
+ });
+ let Some(buffer) = buffer else {
+ return None;
+ };
+ this.completion_tasks.clear();
+ this.discard_inline_completion(cx);
+ let task_context = tasks.as_ref().zip(this.workspace.clone()).and_then(
+ |(tasks, (workspace, _))| {
+ let position = Point::new(row, tasks.column);
+ let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
+ let location = Location {
+ buffer: buffer.clone(),
+ range: range_start..range_start,
+ };
+ workspace
+ .update(cx, |workspace, cx| {
+ tasks::task_context_for_location(workspace, location, cx)
+ })
+ .ok()
+ .flatten()
+ },
+ );
+ let tasks = tasks
+ .zip(task_context.as_ref())
+ .map(|(tasks, task_context)| {
+ Arc::new(ResolvedTasks {
+ templates: tasks
+ .templates
+ .iter()
+ .filter_map(|(kind, template)| {
+ template
+ .resolve_task(&kind.to_id_base(), &task_context)
+ .map(|task| (kind.clone(), task))
+ })
+ .collect(),
+ position: Point::new(row, tasks.column),
+ })
+ });
+ let spawn_straight_away = tasks
+ .as_ref()
+ .map_or(false, |tasks| tasks.templates.len() == 1)
+ && code_actions
+ .as_ref()
+ .map_or(true, |actions| actions.is_empty());
+
+ *this.context_menu.write() = Some(ContextMenu::CodeActions(CodeActionsMenu {
+ buffer,
+ actions: CodeActionContents {
+ tasks,
+ actions: code_actions,
+ },
+ selected_item: Default::default(),
+ scroll_handle: UniformListScrollHandle::default(),
+ deployed_from_indicator,
+ }));
+ if spawn_straight_away {
+ if let Some(task) =
+ this.confirm_code_action(&ConfirmCodeAction { item_ix: Some(0) }, cx)
+ {
+ cx.notify();
+ return Some(task);
+ }
+ }
+ cx.notify();
}
+ None
})?;
+ if let Some(task) = spawned_test_task {
+ task.await?;
+ }
Ok::<_, anyhow::Error>(())
})
@@ -3736,23 +3967,47 @@ impl Editor {
return None;
};
let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
- let action = actions_menu.actions.get(action_ix)?.clone();
- let title = action.lsp_action.title.clone();
+ let action = actions_menu.actions.get(action_ix)?;
+ let title = action.label();
let buffer = actions_menu.buffer;
let workspace = self.workspace()?;
- let apply_code_actions = workspace
- .read(cx)
- .project()
- .clone()
- .update(cx, |project, cx| {
- project.apply_code_action(buffer, action, true, cx)
- });
- let workspace = workspace.downgrade();
- Some(cx.spawn(|editor, cx| async move {
- let project_transaction = apply_code_actions.await?;
- Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await
- }))
+ match action {
+ CodeActionsItem::Task(task_source_kind, resolved_task) => {
+ workspace.update(cx, |workspace, cx| {
+ workspace::tasks::schedule_resolved_task(
+ workspace,
+ task_source_kind,
+ resolved_task,
+ false,
+ cx,
+ );
+
+ None
+ })
+ }
+ CodeActionsItem::CodeAction(action) => {
+ let apply_code_actions = workspace
+ .read(cx)
+ .project()
+ .clone()
+ .update(cx, |project, cx| {
+ project.apply_code_action(buffer, action, true, cx)
+ });
+ let workspace = workspace.downgrade();
+ Some(cx.spawn(|editor, cx| async move {
+ let project_transaction = apply_code_actions.await?;
+ Self::open_project_transaction(
+ &editor,
+ workspace,
+ project_transaction,
+ title,
+ cx,
+ )
+ .await
+ }))
+ }
+ }
}
async fn open_project_transaction(
@@ -4213,9 +4468,10 @@ impl Editor {
Some(self.inline_completion_provider.as_ref()?.provider.clone())
}
- pub fn render_code_actions_indicator(
+ fn render_code_actions_indicator(
&self,
_style: &EditorStyle,
+ row: u32,
is_active: bool,
cx: &mut ViewContext<Self>,
) -> Option<IconButton> {
@@ -4226,10 +4482,10 @@ impl Editor {
.size(ui::ButtonSize::None)
.icon_color(Color::Muted)
.selected(is_active)
- .on_click(cx.listener(|editor, _e, cx| {
+ .on_click(cx.listener(move |editor, _e, cx| {
editor.toggle_code_actions(
&ToggleCodeActions {
- deployed_from_indicator: true,
+ deployed_from_indicator: Some(row),
},
cx,
);
@@ -4240,6 +4496,39 @@ impl Editor {
}
}
+ fn clear_tasks(&mut self) {
+ self.tasks.clear()
+ }
+
+ fn insert_tasks(&mut self, row: u32, tasks: RunnableTasks) {
+ if let Some(_) = self.tasks.insert(row, tasks) {
+ // This case should hopefully be rare, but just in case...
+ log::error!("multiple different run targets found on a single line, only the last target will be rendered")
+ }
+ }
+
+ fn render_run_indicator(
+ &self,
+ _style: &EditorStyle,
+ is_active: bool,
+ row: u32,
+ cx: &mut ViewContext<Self>,
+ ) -> IconButton {
+ IconButton::new("code_actions_indicator", ui::IconName::Play)
+ .icon_size(IconSize::XSmall)
+ .size(ui::ButtonSize::None)
+ .icon_color(Color::Muted)
+ .selected(is_active)
+ .on_click(cx.listener(move |editor, _e, cx| {
+ editor.toggle_code_actions(
+ &ToggleCodeActions {
+ deployed_from_indicator: Some(row),
+ },
+ cx,
+ );
+ }))
+ }
+
pub fn render_fold_indicators(
&mut self,
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
@@ -7400,6 +7689,80 @@ impl Editor {
self.select_larger_syntax_node_stack = stack;
}
+ fn runnable_display_rows(
+ &self,
+ range: Range<Anchor>,
+ snapshot: &DisplaySnapshot,
+ cx: &WindowContext,
+ ) -> Vec<(u32, RunnableTasks)> {
+ if self
+ .project
+ .as_ref()
+ .map_or(false, |project| project.read(cx).is_remote())
+ {
+ // Do not display any test indicators in remote projects.
+ return vec![];
+ }
+ snapshot
+ .buffer_snapshot
+ .runnable_ranges(range)
+ .filter_map(|(multi_buffer_range, mut runnable)| {
+ let (tasks, _) = self.resolve_runnable(&mut runnable, cx);
+ if tasks.is_empty() {
+ return None;
+ }
+ let point = multi_buffer_range.start.to_display_point(&snapshot);
+ Some((
+ point.row(),
+ RunnableTasks {
+ templates: tasks,
+ column: point.column(),
+ },
+ ))
+ })
+ .collect()
+ }
+
+ fn resolve_runnable(
+ &self,
+ runnable: &mut Runnable,
+ cx: &WindowContext<'_>,
+ ) -> (
+ SmallVec<[(TaskSourceKind, TaskTemplate); 1]>,
+ Option<WorktreeId>,
+ ) {
+ let Some(project) = self.project.as_ref() else {
+ return Default::default();
+ };
+ let (inventory, worktree_id) = project.read_with(cx, |project, cx| {
+ let worktree_id = project
+ .buffer_for_id(runnable.buffer)
+ .and_then(|buffer| buffer.read(cx).file())
+ .map(|file| WorktreeId::from_usize(file.worktree_id()));
+
+ (project.task_inventory().clone(), worktree_id)
+ });
+
+ let inventory = inventory.read(cx);
+ let tags = mem::take(&mut runnable.tags);
+ (
+ SmallVec::from_iter(
+ tags.into_iter()
+ .flat_map(|tag| {
+ let tag = tag.0.clone();
+ inventory
+ .list_tasks(Some(runnable.language.clone()), worktree_id)
+ .into_iter()
+ .filter(move |(_, template)| {
+ template.tags.iter().any(|source_tag| source_tag == &tag)
+ })
+ })
+ .sorted_by_key(|(kind, _)| kind.to_owned()),
+ ),
+ worktree_id,
+ )
+ }
+
pub fn move_to_enclosing_bracket(
&mut self,
_: &MoveToEnclosingBracket,
@@ -12,10 +12,11 @@ use crate::{
items::BufferSearchHighlights,
mouse_context_menu::{self, MouseContextMenu},
scroll::scroll_amount::ScrollAmount,
- CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
- EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, GutterDimensions, HalfPageDown,
- HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp, OpenExcerpts, PageDown, PageUp,
- Point, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
+ CodeActionsMenu, CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite,
+ Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts,
+ GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp,
+ OpenExcerpts, PageDown, PageUp, Point, RunnableTasks, SelectPhase, Selection, SoftWrap,
+ ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
};
use anyhow::Result;
use client::ParticipantIndex;
@@ -1374,6 +1375,60 @@ impl EditorElement {
Some(shaped_lines)
}
+ fn layout_run_indicators(
+ &self,
+ task_lines: Vec<(u32, RunnableTasks)>,
+ line_height: Pixels,
+ scroll_pixel_position: gpui::Point<Pixels>,
+ gutter_dimensions: &GutterDimensions,
+ gutter_hitbox: &Hitbox,
+ cx: &mut WindowContext,
+ ) -> Vec<AnyElement> {
+ self.editor.update(cx, |editor, cx| {
+ editor.clear_tasks();
+
+ let active_task_indicator_row =
+ if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
+ deployed_from_indicator,
+ actions,
+ ..
+ })) = editor.context_menu.read().as_ref()
+ {
+ actions
+ .tasks
+ .as_ref()
+ .map(|tasks| tasks.position.row)
+ .or_else(|| *deployed_from_indicator)
+ } else {
+ None
+ };
+ task_lines
+ .into_iter()
+ .map(|(row, tasks)| {
+ editor.insert_tasks(row, tasks);
+
+ let button = editor.render_run_indicator(
+ &self.style,
+ Some(row) == active_task_indicator_row,
+ row,
+ cx,
+ );
+
+ let button = prepaint_gutter_button(
+ button,
+ row,
+ line_height,
+ gutter_dimensions,
+ scroll_pixel_position,
+ gutter_hitbox,
+ cx,
+ );
+ button
+ })
+ .collect_vec()
+ })
+ }
+
fn layout_code_actions_indicator(
&self,
line_height: Pixels,
@@ -1385,35 +1440,28 @@ impl EditorElement {
) -> Option<AnyElement> {
let mut active = false;
let mut button = None;
+ let row = newest_selection_head.row();
self.editor.update(cx, |editor, cx| {
- active = matches!(
- editor.context_menu.read().as_ref(),
- Some(crate::ContextMenu::CodeActions(_))
- );
- button = editor.render_code_actions_indicator(&self.style, active, cx);
+ if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
+ deployed_from_indicator,
+ ..
+ })) = editor.context_menu.read().as_ref()
+ {
+ active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
+ };
+ button = editor.render_code_actions_indicator(&self.style, row, active, cx);
});
- let mut button = button?.into_any_element();
- let available_space = size(
- AvailableSpace::MinContent,
- AvailableSpace::Definite(line_height),
+ let button = prepaint_gutter_button(
+ button?,
+ row,
+ line_height,
+ gutter_dimensions,
+ scroll_pixel_position,
+ gutter_hitbox,
+ cx,
);
- let indicator_size = button.layout_as_root(available_space, cx);
-
- let blame_width = gutter_dimensions
- .git_blame_entries_width
- .unwrap_or(Pixels::ZERO);
- let mut x = blame_width;
- let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
- - indicator_size.width
- - blame_width;
- x += available_width / 2.;
-
- let mut y = newest_selection_head.row() as f32 * line_height - scroll_pixel_position.y;
- y += (line_height - indicator_size.height) / 2.;
-
- button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
Some(button)
}
@@ -2351,6 +2399,10 @@ impl EditorElement {
}
});
+ for test_indicators in layout.test_indicators.iter_mut() {
+ test_indicators.paint(cx);
+ }
+
if let Some(indicator) = layout.code_actions_indicator.as_mut() {
indicator.paint(cx);
}
@@ -3224,6 +3276,39 @@ impl EditorElement {
}
}
+fn prepaint_gutter_button(
+ button: IconButton,
+ row: u32,
+ line_height: Pixels,
+ gutter_dimensions: &GutterDimensions,
+ scroll_pixel_position: gpui::Point<Pixels>,
+ gutter_hitbox: &Hitbox,
+ cx: &mut WindowContext<'_>,
+) -> AnyElement {
+ let mut button = button.into_any_element();
+ let available_space = size(
+ AvailableSpace::MinContent,
+ AvailableSpace::Definite(line_height),
+ );
+ let indicator_size = button.layout_as_root(available_space, cx);
+
+ let blame_width = gutter_dimensions
+ .git_blame_entries_width
+ .unwrap_or(Pixels::ZERO);
+
+ let mut x = blame_width;
+ let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
+ - indicator_size.width
+ - blame_width;
+ x += available_width / 2.;
+
+ let mut y = row as f32 * line_height - scroll_pixel_position.y;
+ y += (line_height - indicator_size.height) / 2.;
+
+ button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
+ button
+}
+
fn render_inline_blame_entry(
blame: &gpui::Model<GitBlame>,
blame_entry: BlameEntry,
@@ -3750,6 +3835,12 @@ impl Element for EditorElement {
cx,
);
+ let test_lines = self.editor.read(cx).runnable_display_rows(
+ start_anchor..end_anchor,
+ &snapshot.display_snapshot,
+ cx,
+ );
+
let (selections, active_rows, newest_selection_head) = self.layout_selections(
start_anchor,
end_anchor,
@@ -3939,18 +4030,32 @@ impl Element for EditorElement {
cx,
);
if gutter_settings.code_actions {
- code_actions_indicator = self.layout_code_actions_indicator(
- line_height,
- newest_selection_head,
- scroll_pixel_position,
- &gutter_dimensions,
- &gutter_hitbox,
- cx,
- );
+ let has_test_indicator = test_lines
+ .iter()
+ .any(|(line, _)| *line == newest_selection_head.row());
+ if !has_test_indicator {
+ code_actions_indicator = self.layout_code_actions_indicator(
+ line_height,
+ newest_selection_head,
+ scroll_pixel_position,
+ &gutter_dimensions,
+ &gutter_hitbox,
+ cx,
+ );
+ }
}
}
}
+ let test_indicators = self.layout_run_indicators(
+ test_lines,
+ line_height,
+ scroll_pixel_position,
+ &gutter_dimensions,
+ &gutter_hitbox,
+ cx,
+ );
+
if !context_menu_visible && !cx.has_active_drag() {
self.layout_hover_popovers(
&snapshot,
@@ -4051,6 +4156,7 @@ impl Element for EditorElement {
visible_cursors,
selections,
mouse_context_menu,
+ test_indicators,
code_actions_indicator,
fold_indicators,
tab_invisible,
@@ -4170,6 +4276,7 @@ pub struct EditorLayout {
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
max_row: u32,
code_actions_indicator: Option<AnyElement>,
+ test_indicators: Vec<AnyElement>,
fold_indicators: Vec<Option<AnyElement>>,
mouse_context_menu: Option<AnyElement>,
tab_invisible: ShapedLine,
@@ -79,7 +79,7 @@ pub fn deploy_context_menu(
.action(
"Code Actions",
Box::new(ToggleCodeActions {
- deployed_from_indicator: false,
+ deployed_from_indicator: None,
}),
)
.separator()
@@ -0,0 +1,118 @@
+use crate::Editor;
+
+use std::{path::Path, sync::Arc};
+
+use anyhow::Context;
+use gpui::WindowContext;
+use language::{BasicContextProvider, ContextProvider};
+use project::{Location, WorktreeId};
+use task::{TaskContext, TaskVariables};
+use util::ResultExt;
+use workspace::Workspace;
+
+pub(crate) fn task_context_for_location(
+ workspace: &Workspace,
+ location: Location,
+ cx: &mut WindowContext<'_>,
+) -> Option<TaskContext> {
+ let cwd = workspace::tasks::task_cwd(workspace, cx)
+ .log_err()
+ .flatten();
+
+ let buffer = location.buffer.clone();
+ let language_context_provider = buffer
+ .read(cx)
+ .language()
+ .and_then(|language| language.context_provider())
+ .unwrap_or_else(|| Arc::new(BasicContextProvider));
+
+ let worktree_abs_path = buffer
+ .read(cx)
+ .file()
+ .map(|file| WorktreeId::from_usize(file.worktree_id()))
+ .and_then(|worktree_id| {
+ workspace
+ .project()
+ .read(cx)
+ .worktree_for_id(worktree_id, cx)
+ .map(|worktree| worktree.read(cx).abs_path())
+ });
+ let task_variables = combine_task_variables(
+ worktree_abs_path.as_deref(),
+ location,
+ language_context_provider.as_ref(),
+ cx,
+ )
+ .log_err()?;
+ Some(TaskContext {
+ cwd,
+ task_variables,
+ })
+}
+
+pub(crate) fn task_context_with_editor(
+ workspace: &Workspace,
+ editor: &mut Editor,
+ cx: &mut WindowContext<'_>,
+) -> Option<TaskContext> {
+ let (selection, buffer, editor_snapshot) = {
+ let selection = editor.selections.newest::<usize>(cx);
+ let (buffer, _, _) = editor
+ .buffer()
+ .read(cx)
+ .point_to_buffer_offset(selection.start, cx)?;
+ let snapshot = editor.snapshot(cx);
+ Some((selection, buffer, snapshot))
+ }?;
+ let selection_range = selection.range();
+ let start = editor_snapshot
+ .display_snapshot
+ .buffer_snapshot
+ .anchor_after(selection_range.start)
+ .text_anchor;
+ let end = editor_snapshot
+ .display_snapshot
+ .buffer_snapshot
+ .anchor_after(selection_range.end)
+ .text_anchor;
+ let location = Location {
+ buffer,
+ range: start..end,
+ };
+ task_context_for_location(workspace, location, cx)
+}
+
+pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
+ let Some(editor) = workspace
+ .active_item(cx)
+ .and_then(|item| item.act_as::<Editor>(cx))
+ else {
+ return Default::default();
+ };
+ editor.update(cx, |editor, cx| {
+ task_context_with_editor(workspace, editor, cx).unwrap_or_default()
+ })
+}
+
+fn combine_task_variables(
+ worktree_abs_path: Option<&Path>,
+ location: Location,
+ context_provider: &dyn ContextProvider,
+ cx: &mut WindowContext<'_>,
+) -> anyhow::Result<TaskVariables> {
+ if context_provider.is_basic() {
+ context_provider
+ .build_context(worktree_abs_path, &location, cx)
+ .context("building basic provider context")
+ } else {
+ let mut basic_context = BasicContextProvider
+ .build_context(worktree_abs_path, &location, cx)
+ .context("building basic default context")?;
+ basic_context.extend(
+ context_provider
+ .build_context(worktree_abs_path, &location, cx)
+ .context("building provider context ")?,
+ );
+ Ok(basic_context)
+ }
+}
@@ -13,7 +13,7 @@ use crate::{
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
SyntaxSnapshot, ToTreeSitterPoint,
},
- LanguageScope, Outline,
+ LanguageScope, Outline, RunnableTag,
};
use anyhow::{anyhow, Context, Result};
pub use clock::ReplicaId;
@@ -501,6 +501,13 @@ pub enum CharKind {
Word,
}
+/// A runnable is a set of data about a region that could be resolved into a task
+pub struct Runnable {
+ pub tags: SmallVec<[RunnableTag; 1]>,
+ pub language: Arc<Language>,
+ pub buffer: BufferId,
+}
+
impl Buffer {
/// Create a new buffer with the given base text.
pub fn local<T: Into<String>>(base_text: T, cx: &mut ModelContext<Self>) -> Self {
@@ -2978,6 +2985,53 @@ impl BufferSnapshot {
})
}
+ pub fn runnable_ranges(
+ &self,
+ range: Range<Anchor>,
+ ) -> impl Iterator<Item = (Range<usize>, Runnable)> + '_ {
+ let offset_range = range.start.to_offset(self)..range.end.to_offset(self);
+
+ let mut syntax_matches = self.syntax.matches(offset_range, self, |grammar| {
+ grammar.runnable_config.as_ref().map(|config| &config.query)
+ });
+
+ let test_configs = syntax_matches
+ .grammars()
+ .iter()
+ .map(|grammar| grammar.runnable_config.as_ref())
+ .collect::<Vec<_>>();
+
+ iter::from_fn(move || {
+ let test_range = syntax_matches
+ .peek()
+ .and_then(|mat| {
+ test_configs[mat.grammar_index].and_then(|test_configs| {
+ let tags = SmallVec::from_iter(mat.captures.iter().filter_map(|capture| {
+ test_configs.runnable_tags.get(&capture.index).cloned()
+ }));
+
+ if tags.is_empty() {
+ return None;
+ }
+
+ Some((
+ mat.captures
+ .iter()
+ .find(|capture| capture.index == test_configs.run_capture_ix)?,
+ Runnable {
+ tags,
+ language: mat.language,
+ buffer: self.remote_id(),
+ },
+ ))
+ })
+ })
+ .map(|(mat, test_tags)| (mat.node.byte_range(), test_tags));
+ syntax_matches.advance();
+ test_range
+ })
+ }
+
/// Returns selections for remote peers intersecting the given range.
#[allow(clippy::type_complexity)]
pub fn remote_selections_in_range(
@@ -56,6 +56,7 @@ use std::{
},
};
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
+use task::RunnableTag;
pub use task_context::{BasicContextProvider, ContextProvider, ContextProviderWithTasks};
use theme::SyntaxTheme;
use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore};
@@ -836,6 +837,7 @@ pub struct Grammar {
pub(crate) highlights_query: Option<Query>,
pub(crate) brackets_config: Option<BracketConfig>,
pub(crate) redactions_config: Option<RedactionConfig>,
+ pub(crate) runnable_config: Option<RunnableConfig>,
pub(crate) indents_config: Option<IndentConfig>,
pub outline_config: Option<OutlineConfig>,
pub embedding_config: Option<EmbeddingConfig>,
@@ -882,6 +884,14 @@ struct RedactionConfig {
pub redaction_capture_ix: u32,
}
+struct RunnableConfig {
+ pub query: Query,
+ /// A mapping from captures indices to known test tags
+ pub runnable_tags: HashMap<u32, RunnableTag>,
+ /// index of the capture that corresponds to @run
+ pub run_capture_ix: u32,
+}
+
struct OverrideConfig {
query: Query,
values: HashMap<u32, (String, LanguageConfigOverride)>,
@@ -923,6 +933,7 @@ impl Language {
injection_config: None,
override_config: None,
redactions_config: None,
+ runnable_config: None,
error_query: Query::new(&ts_language, "(ERROR) @error").unwrap(),
ts_language,
highlight_map: Default::default(),
@@ -978,6 +989,11 @@ impl Language {
.with_redaction_query(query.as_ref())
.context("Error loading redaction query")?;
}
+ if let Some(query) = queries.runnables {
+ self = self
+ .with_runnable_query(query.as_ref())
+ .context("Error loading tests query")?;
+ }
Ok(self)
}
@@ -989,6 +1005,33 @@ impl Language {
Ok(self)
}
+ pub fn with_runnable_query(mut self, source: &str) -> Result<Self> {
+ let grammar = self
+ .grammar_mut()
+ .ok_or_else(|| anyhow!("cannot mutate grammar"))?;
+
+ let query = Query::new(&grammar.ts_language, source)?;
+ let mut run_capture_index = None;
+ let mut runnable_tags = HashMap::default();
+ for (ix, name) in query.capture_names().iter().enumerate() {
+ if *name == "run" {
+ run_capture_index = Some(ix as u32);
+ } else if !name.starts_with('_') {
+ runnable_tags.insert(ix as u32, RunnableTag(name.to_string().into()));
+ }
+ }
+
+ if let Some(run_capture_ix) = run_capture_index {
+ grammar.runnable_config = Some(RunnableConfig {
+ query,
+ run_capture_ix,
+ runnable_tags,
+ });
+ }
+
+ Ok(self)
+ }
+
pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
let grammar = self
.grammar_mut()
@@ -124,6 +124,7 @@ pub const QUERY_FILENAME_PREFIXES: &[(
("injections", |q| &mut q.injections),
("overrides", |q| &mut q.overrides),
("redactions", |q| &mut q.redactions),
+ ("runnables", |q| &mut q.runnables),
];
/// Tree-sitter language queries for a given language.
@@ -137,6 +138,7 @@ pub struct LanguageQueries {
pub injections: Option<Cow<'static, str>>,
pub overrides: Option<Cow<'static, str>>,
pub redactions: Option<Cow<'static, str>>,
+ pub runnables: Option<Cow<'static, str>>,
}
#[derive(Clone, Default)]
@@ -56,6 +56,7 @@ pub struct SyntaxMapCapture<'a> {
#[derive(Debug)]
pub struct SyntaxMapMatch<'a> {
+ pub language: Arc<Language>,
pub depth: usize,
pub pattern_index: usize,
pub captures: &'a [QueryCapture<'a>],
@@ -71,6 +72,7 @@ struct SyntaxMapCapturesLayer<'a> {
}
struct SyntaxMapMatchesLayer<'a> {
+ language: Arc<Language>,
depth: usize,
next_pattern_index: usize,
next_captures: Vec<QueryCapture<'a>>,
@@ -1016,6 +1018,7 @@ impl<'a> SyntaxMapMatches<'a> {
result.grammars.len() - 1
});
let mut layer = SyntaxMapMatchesLayer {
+ language: layer.language.clone(),
depth: layer.depth,
grammar_index,
matches,
@@ -1048,10 +1051,13 @@ impl<'a> SyntaxMapMatches<'a> {
pub fn peek(&self) -> Option<SyntaxMapMatch> {
let layer = self.layers.first()?;
+
if !layer.has_next {
return None;
}
+
Some(SyntaxMapMatch {
+ language: layer.language.clone(),
depth: layer.depth,
grammar_index: layer.grammar_index,
pattern_index: layer.next_pattern_index,
@@ -389,6 +389,7 @@ impl ContextProvider for RustContextProvider {
"--".into(),
"--nocapture".into(),
],
+ tags: vec!["rust-test".to_owned()],
..TaskTemplate::default()
},
TaskTemplate {
@@ -0,0 +1,7 @@
+(
+ (attribute_item (attribute) @_attribute
+ (#match? @_attribute ".*test.*"))
+ .
+ (function_item
+ name: (_) @run)
+) @rust-test
@@ -13,7 +13,7 @@ use language::{
language_settings::{language_settings, LanguageSettings},
AutoindentMode, Buffer, BufferChunks, BufferSnapshot, Capability, CharKind, Chunk, CursorShape,
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
- Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
+ Outline, OutlineItem, Point, PointUtf16, Runnable, Selection, TextDimension, ToOffset as _,
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
};
use smallvec::SmallVec;
@@ -3165,6 +3165,31 @@ impl MultiBufferSnapshot {
.flatten()
}
+ pub fn runnable_ranges(
+ &self,
+ range: Range<Anchor>,
+ ) -> impl Iterator<Item = (Range<usize>, Runnable)> + '_ {
+ let range = range.start.to_offset(self)..range.end.to_offset(self);
+ self.excerpts_for_range(range.clone())
+ .flat_map(move |(excerpt, excerpt_offset)| {
+ let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
+
+ excerpt
+ .buffer
+ .runnable_ranges(excerpt.range.context.clone())
+ .map(move |(mut match_range, runnable)| {
+ // Re-base onto the excerpts coordinates in the multibuffer
+ match_range.start =
+ excerpt_offset + (match_range.start - excerpt_buffer_start);
+ match_range.end = excerpt_offset + (match_range.end - excerpt_buffer_start);
+
+ (match_range, runnable)
+ })
+ .skip_while(move |(match_range, _)| match_range.end < range.start)
+ .take_while(move |(match_range, _)| match_range.start < range.end)
+ })
+ }
+
pub fn diagnostics_update_count(&self) -> usize {
self.diagnostics_update_count
}
@@ -7655,17 +7655,15 @@ impl Project {
} else {
let fs = self.fs.clone();
let task_abs_path = abs_path.clone();
+ let tasks_file_rx =
+ watch_config_file(&cx.background_executor(), fs, task_abs_path);
task_inventory.add_source(
TaskSourceKind::Worktree {
id: remote_worktree_id,
abs_path,
id_base: "local_tasks_for_worktree",
},
- |cx| {
- let tasks_file_rx =
- watch_config_file(&cx.background_executor(), fs, task_abs_path);
- StaticSource::new(TrackedFile::new(tasks_file_rx, cx), cx)
- },
+ StaticSource::new(TrackedFile::new(tasks_file_rx, cx)),
cx,
);
}
@@ -7677,23 +7675,20 @@ impl Project {
} else {
let fs = self.fs.clone();
let task_abs_path = abs_path.clone();
+ let tasks_file_rx =
+ watch_config_file(&cx.background_executor(), fs, task_abs_path);
task_inventory.add_source(
TaskSourceKind::Worktree {
id: remote_worktree_id,
abs_path,
id_base: "local_vscode_tasks_for_worktree",
},
- |cx| {
- let tasks_file_rx =
- watch_config_file(&cx.background_executor(), fs, task_abs_path);
- StaticSource::new(
- TrackedFile::new_convertible::<task::VsCodeTaskFile>(
- tasks_file_rx,
- cx,
- ),
+ StaticSource::new(
+ TrackedFile::new_convertible::<task::VsCodeTaskFile>(
+ tasks_file_rx,
cx,
- )
- },
+ ),
+ ),
cx,
);
}
@@ -14,7 +14,7 @@ use serde_json::json;
#[cfg(not(windows))]
use std::os;
use std::task::Poll;
-use task::{TaskContext, TaskSource, TaskTemplate, TaskTemplates};
+use task::{TaskContext, TaskTemplate, TaskTemplates};
use unindent::Unindent as _;
use util::{assert_set_eq, paths::PathMatcher, test::temp_tree};
use worktree::WorktreeModelHandle as _;
@@ -168,12 +168,11 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
let all_tasks = project
.update(cx, |project, cx| {
- project.task_inventory().update(cx, |inventory, cx| {
+ project.task_inventory().update(cx, |inventory, _| {
let (mut old, new) = inventory.used_and_current_resolved_tasks(
None,
Some(workree_id),
&task_context,
- cx,
);
old.extend(new);
old
@@ -215,13 +214,9 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
project.update(cx, |project, cx| {
let inventory = project.task_inventory();
- inventory.update(cx, |inventory, cx| {
- let (mut old, new) = inventory.used_and_current_resolved_tasks(
- None,
- Some(workree_id),
- &task_context,
- cx,
- );
+ inventory.update(cx, |inventory, _| {
+ let (mut old, new) =
+ inventory.used_and_current_resolved_tasks(None, Some(workree_id), &task_context);
old.extend(new);
let (_, resolved_task) = old
.into_iter()
@@ -231,41 +226,39 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
})
});
+ let tasks = serde_json::to_string(&TaskTemplates(vec![TaskTemplate {
+ label: "cargo check".to_string(),
+ command: "cargo".to_string(),
+ args: vec![
+ "check".to_string(),
+ "--all".to_string(),
+ "--all-targets".to_string(),
+ ],
+ env: HashMap::from_iter(Some((
+ "RUSTFLAGS".to_string(),
+ "-Zunstable-options".to_string(),
+ ))),
+ ..TaskTemplate::default()
+ }]))
+ .unwrap();
+ let (tx, rx) = futures::channel::mpsc::unbounded();
+
+ let templates = cx.update(|cx| TrackedFile::new(rx, cx));
+ tx.unbounded_send(tasks).unwrap();
+
+ let source = StaticSource::new(templates);
+ cx.run_until_parked();
+
cx.update(|cx| {
let all_tasks = project
.update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, cx| {
inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json"));
- inventory.add_source(
- global_task_source_kind.clone(),
- |cx| {
- cx.new_model(|_| {
- let source = TestTaskSource {
- tasks: TaskTemplates(vec![TaskTemplate {
- label: "cargo check".to_string(),
- command: "cargo".to_string(),
- args: vec![
- "check".to_string(),
- "--all".to_string(),
- "--all-targets".to_string(),
- ],
- env: HashMap::from_iter(Some((
- "RUSTFLAGS".to_string(),
- "-Zunstable-options".to_string(),
- ))),
- ..TaskTemplate::default()
- }]),
- };
- Box::new(source) as Box<_>
- })
- },
- cx,
- );
+ inventory.add_source(global_task_source_kind.clone(), source, cx);
let (mut old, new) = inventory.used_and_current_resolved_tasks(
None,
Some(workree_id),
&task_context,
- cx,
);
old.extend(new);
old
@@ -317,20 +310,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
});
}
-struct TestTaskSource {
- tasks: TaskTemplates,
-}
-
-impl TaskSource for TestTaskSource {
- fn as_any(&mut self) -> &mut dyn std::any::Any {
- self
- }
-
- fn tasks_to_schedule(&mut self, _: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates {
- self.tasks.clone()
- }
-}
-
#[gpui::test]
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -1,17 +1,18 @@
//! Project-wide storage of the tasks available, capable of updating itself from the sources set.
use std::{
- any::TypeId,
cmp::{self, Reverse},
path::{Path, PathBuf},
sync::Arc,
};
use collections::{hash_map, HashMap, VecDeque};
-use gpui::{AppContext, Context, Model, ModelContext, Subscription};
+use gpui::{AppContext, Context, Model, ModelContext};
use itertools::{Either, Itertools};
use language::Language;
-use task::{ResolvedTask, TaskContext, TaskId, TaskSource, TaskTemplate, VariableName};
+use task::{
+ static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, VariableName,
+};
use util::{post_inc, NumericPrefixWithSuffix};
use worktree::WorktreeId;
@@ -22,14 +23,12 @@ pub struct Inventory {
}
struct SourceInInventory {
- source: Model<Box<dyn TaskSource>>,
- _subscription: Subscription,
- type_id: TypeId,
+ source: StaticSource,
kind: TaskSourceKind,
}
/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum TaskSourceKind {
/// bash-like commands spawned by users, not associated with any path
UserInput,
@@ -95,7 +94,7 @@ impl Inventory {
pub fn add_source(
&mut self,
kind: TaskSourceKind,
- create_source: impl FnOnce(&mut ModelContext<Self>) -> Model<Box<dyn TaskSource>>,
+ source: StaticSource,
cx: &mut ModelContext<Self>,
) {
let abs_path = kind.abs_path();
@@ -106,16 +105,7 @@ impl Inventory {
}
}
- let source = create_source(cx);
- let type_id = source.read(cx).type_id();
- let source = SourceInInventory {
- _subscription: cx.observe(&source, |_, _, cx| {
- cx.notify();
- }),
- source,
- type_id,
- kind,
- };
+ let source = SourceInInventory { source, kind };
self.sources.push(source);
cx.notify();
}
@@ -136,31 +126,12 @@ impl Inventory {
self.sources.retain(|s| s.kind.worktree() != Some(worktree));
}
- pub fn source<T: TaskSource>(&self) -> Option<(Model<Box<dyn TaskSource>>, TaskSourceKind)> {
- let target_type_id = std::any::TypeId::of::<T>();
- self.sources.iter().find_map(
- |SourceInInventory {
- type_id,
- source,
- kind,
- ..
- }| {
- if &target_type_id == type_id {
- Some((source.clone(), kind.clone()))
- } else {
- None
- }
- },
- )
- }
-
/// Pulls its task sources relevant to the worktree and the language given,
/// returns all task templates with their source kinds, in no specific order.
pub fn list_tasks(
&self,
language: Option<Arc<Language>>,
worktree: Option<WorktreeId>,
- cx: &mut AppContext,
) -> Vec<(TaskSourceKind, TaskTemplate)> {
let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
name: language.name(),
@@ -180,7 +151,7 @@ impl Inventory {
.flat_map(|source| {
source
.source
- .update(cx, |source, cx| source.tasks_to_schedule(cx))
+ .tasks_to_schedule()
.0
.into_iter()
.map(|task| (&source.kind, task))
@@ -199,7 +170,6 @@ impl Inventory {
language: Option<Arc<Language>>,
worktree: Option<WorktreeId>,
task_context: &TaskContext,
- cx: &mut AppContext,
) -> (
Vec<(TaskSourceKind, ResolvedTask)>,
Vec<(TaskSourceKind, ResolvedTask)>,
@@ -246,7 +216,7 @@ impl Inventory {
.flat_map(|source| {
source
.source
- .update(cx, |source, cx| source.tasks_to_schedule(cx))
+ .tasks_to_schedule()
.0
.into_iter()
.map(|task| (&source.kind, task))
@@ -387,9 +357,12 @@ fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
#[cfg(test)]
mod test_inventory {
- use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
+ use gpui::{AppContext, Model, TestAppContext};
use itertools::Itertools;
- use task::{TaskContext, TaskId, TaskSource, TaskTemplate, TaskTemplates};
+ use task::{
+ static_source::{StaticSource, TrackedFile},
+ TaskContext, TaskTemplate, TaskTemplates,
+ };
use worktree::WorktreeId;
use crate::Inventory;
@@ -398,55 +371,28 @@ mod test_inventory {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TestTask {
- id: task::TaskId,
name: String,
}
- pub struct StaticTestSource {
- pub tasks: Vec<TestTask>,
- }
-
- impl StaticTestSource {
- pub(super) fn new(
- task_names: impl IntoIterator<Item = String>,
- cx: &mut AppContext,
- ) -> Model<Box<dyn TaskSource>> {
- cx.new_model(|_| {
- Box::new(Self {
- tasks: task_names
- .into_iter()
- .enumerate()
- .map(|(i, name)| TestTask {
- id: TaskId(format!("task_{i}_{name}")),
- name,
- })
- .collect(),
- }) as Box<dyn TaskSource>
- })
- }
- }
-
- impl TaskSource for StaticTestSource {
- fn tasks_to_schedule(
- &mut self,
- _cx: &mut ModelContext<Box<dyn TaskSource>>,
- ) -> TaskTemplates {
- TaskTemplates(
- self.tasks
- .clone()
- .into_iter()
- .map(|task| TaskTemplate {
- label: task.name,
- command: "test command".to_string(),
- ..TaskTemplate::default()
- })
- .collect(),
- )
- }
-
- fn as_any(&mut self) -> &mut dyn std::any::Any {
- self
- }
+ pub(super) fn static_test_source(
+ task_names: impl IntoIterator<Item = String>,
+ cx: &mut AppContext,
+ ) -> StaticSource {
+ let tasks = TaskTemplates(
+ task_names
+ .into_iter()
+ .map(|name| TaskTemplate {
+ label: name,
+ command: "test command".to_owned(),
+ ..TaskTemplate::default()
+ })
+ .collect(),
+ );
+ let (tx, rx) = futures::channel::mpsc::unbounded();
+ let file = TrackedFile::new(rx, cx);
+ tx.unbounded_send(serde_json::to_string(&tasks).unwrap())
+ .unwrap();
+ StaticSource::new(file)
}
pub(super) fn task_template_names(
@@ -454,9 +400,9 @@ mod test_inventory {
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<String> {
- inventory.update(cx, |inventory, cx| {
+ inventory.update(cx, |inventory, _| {
inventory
- .list_tasks(None, worktree, cx)
+ .list_tasks(None, worktree)
.into_iter()
.map(|(_, task)| task.label)
.sorted()
@@ -469,13 +415,9 @@ mod test_inventory {
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<String> {
- inventory.update(cx, |inventory, cx| {
- let (used, current) = inventory.used_and_current_resolved_tasks(
- None,
- worktree,
- &TaskContext::default(),
- cx,
- );
+ inventory.update(cx, |inventory, _| {
+ let (used, current) =
+ inventory.used_and_current_resolved_tasks(None, worktree, &TaskContext::default());
used.into_iter()
.chain(current)
.map(|(_, task)| task.original_task().label.clone())
@@ -488,9 +430,9 @@ mod test_inventory {
task_name: &str,
cx: &mut TestAppContext,
) {
- inventory.update(cx, |inventory, cx| {
+ inventory.update(cx, |inventory, _| {
let (task_source_kind, task) = inventory
- .list_tasks(None, None, cx)
+ .list_tasks(None, None)
.into_iter()
.find(|(_, task)| task.label == task_name)
.unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
@@ -508,13 +450,9 @@ mod test_inventory {
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<(TaskSourceKind, String)> {
- inventory.update(cx, |inventory, cx| {
- let (used, current) = inventory.used_and_current_resolved_tasks(
- None,
- worktree,
- &TaskContext::default(),
- cx,
- );
+ inventory.update(cx, |inventory, _| {
+ let (used, current) =
+ inventory.used_and_current_resolved_tasks(None, worktree, &TaskContext::default());
let mut all = used;
all.extend(current);
all.into_iter()
@@ -549,27 +487,25 @@ mod tests {
inventory.update(cx, |inventory, cx| {
inventory.add_source(
TaskSourceKind::UserInput,
- |cx| StaticTestSource::new(vec!["3_task".to_string()], cx),
+ static_test_source(vec!["3_task".to_string()], cx),
cx,
);
});
inventory.update(cx, |inventory, cx| {
inventory.add_source(
TaskSourceKind::UserInput,
- |cx| {
- StaticTestSource::new(
- vec![
- "1_task".to_string(),
- "2_task".to_string(),
- "1_a_task".to_string(),
- ],
- cx,
- )
- },
+ static_test_source(
+ vec![
+ "1_task".to_string(),
+ "2_task".to_string(),
+ "1_a_task".to_string(),
+ ],
+ cx,
+ ),
cx,
);
});
-
+ cx.run_until_parked();
let expected_initial_state = [
"1_a_task".to_string(),
"1_task".to_string(),
@@ -622,12 +558,11 @@ mod tests {
inventory.update(cx, |inventory, cx| {
inventory.add_source(
TaskSourceKind::UserInput,
- |cx| {
- StaticTestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx)
- },
+ static_test_source(vec!["10_hello".to_string(), "11_hello".to_string()], cx),
cx,
);
});
+ cx.run_until_parked();
let expected_updated_state = [
"10_hello".to_string(),
"11_hello".to_string(),
@@ -680,15 +615,11 @@ mod tests {
let worktree_path_1 = Path::new("worktree_path_1");
let worktree_2 = WorktreeId::from_usize(2);
let worktree_path_2 = Path::new("worktree_path_2");
+
inventory_with_statics.update(cx, |inventory, cx| {
inventory.add_source(
TaskSourceKind::UserInput,
- |cx| {
- StaticTestSource::new(
- vec!["user_input".to_string(), common_name.to_string()],
- cx,
- )
- },
+ static_test_source(vec!["user_input".to_string(), common_name.to_string()], cx),
cx,
);
inventory.add_source(
@@ -696,12 +627,10 @@ mod tests {
id_base: "test source",
abs_path: path_1.to_path_buf(),
},
- |cx| {
- StaticTestSource::new(
- vec!["static_source_1".to_string(), common_name.to_string()],
- cx,
- )
- },
+ static_test_source(
+ vec!["static_source_1".to_string(), common_name.to_string()],
+ cx,
+ ),
cx,
);
inventory.add_source(
@@ -709,12 +638,10 @@ mod tests {
id_base: "test source",
abs_path: path_2.to_path_buf(),
},
- |cx| {
- StaticTestSource::new(
- vec!["static_source_2".to_string(), common_name.to_string()],
- cx,
- )
- },
+ static_test_source(
+ vec!["static_source_2".to_string(), common_name.to_string()],
+ cx,
+ ),
cx,
);
inventory.add_source(
@@ -723,12 +650,7 @@ mod tests {
abs_path: worktree_path_1.to_path_buf(),
id_base: "test_source",
},
- |cx| {
- StaticTestSource::new(
- vec!["worktree_1".to_string(), common_name.to_string()],
- cx,
- )
- },
+ static_test_source(vec!["worktree_1".to_string(), common_name.to_string()], cx),
cx,
);
inventory.add_source(
@@ -737,16 +659,11 @@ mod tests {
abs_path: worktree_path_2.to_path_buf(),
id_base: "test_source",
},
- |cx| {
- StaticTestSource::new(
- vec!["worktree_2".to_string(), common_name.to_string()],
- cx,
- )
- },
+ static_test_source(vec!["worktree_2".to_string(), common_name.to_string()], cx),
cx,
);
});
-
+ cx.run_until_parked();
let worktree_independent_tasks = vec![
(
TaskSourceKind::AbsPath {
@@ -14,6 +14,7 @@ collections.workspace = true
futures.workspace = true
gpui.workspace = true
hex.workspace = true
+parking_lot.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json_lenient.workspace = true
@@ -6,9 +6,8 @@ mod task_template;
mod vscode_format;
use collections::{HashMap, HashSet};
-use gpui::ModelContext;
+use gpui::SharedString;
use serde::Serialize;
-use std::any::Any;
use std::borrow::Cow;
use std::path::PathBuf;
@@ -103,6 +102,8 @@ pub enum VariableName {
Column,
/// Text from the latest selection.
SelectedText,
+ /// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm
+ RunnableSymbol,
/// Custom variable, provided by the plugin or other external source.
/// Will be printed with `ZED_` prefix to avoid potential conflicts with other variables.
Custom(Cow<'static, str>),
@@ -132,6 +133,7 @@ impl std::fmt::Display for VariableName {
Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"),
Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
+ Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
Self::Custom(s) => write!(f, "{ZED_VARIABLE_NAME_PREFIX}CUSTOM_{s}"),
}
}
@@ -169,13 +171,6 @@ pub struct TaskContext {
pub task_variables: TaskVariables,
}
-/// [`Source`] produces tasks that can be scheduled.
-///
-/// Implementations of this trait could be e.g. [`StaticSource`] that parses tasks from a .json files and provides process templates to be spawned;
-/// another one could be a language server providing lenses with tests or build server listing all targets for a given project.
-pub trait TaskSource: Any {
- /// A way to erase the type of the source, processing and storing them generically.
- fn as_any(&mut self) -> &mut dyn Any;
- /// Collects all tasks available for scheduling.
- fn tasks_to_schedule(&mut self, cx: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates;
-}
+/// This is a new type representing a 'tag' on a 'runnable symbol', typically a test of main() function, found via treesitter.
+#[derive(Clone, Debug)]
+pub struct RunnableTag(pub SharedString);
@@ -1,134 +1,110 @@
//! A source of tasks, based on a static configuration, deserialized from the tasks config file, and related infrastructure for tracking changes to the file.
+use std::sync::Arc;
+
use futures::StreamExt;
-use gpui::{AppContext, Context, Model, ModelContext, Subscription};
+use gpui::AppContext;
+use parking_lot::RwLock;
use serde::Deserialize;
use util::ResultExt;
-use crate::{TaskSource, TaskTemplates};
+use crate::TaskTemplates;
use futures::channel::mpsc::UnboundedReceiver;
/// The source of tasks defined in a tasks config file.
pub struct StaticSource {
- tasks: TaskTemplates,
- _templates: Model<TrackedFile<TaskTemplates>>,
- _subscription: Subscription,
+ tasks: TrackedFile<TaskTemplates>,
}
/// A Wrapper around deserializable T that keeps track of its contents
/// via a provided channel. Once T value changes, the observers of [`TrackedFile`] are
/// notified.
pub struct TrackedFile<T> {
- parsed_contents: T,
+ parsed_contents: Arc<RwLock<T>>,
}
-impl<T: PartialEq + 'static> TrackedFile<T> {
+impl<T: PartialEq + 'static + Sync> TrackedFile<T> {
/// Initializes new [`TrackedFile`] with a type that's deserializable.
- pub fn new(mut tracker: UnboundedReceiver<String>, cx: &mut AppContext) -> Model<Self>
+ pub fn new(mut tracker: UnboundedReceiver<String>, cx: &mut AppContext) -> Self
where
- T: for<'a> Deserialize<'a> + Default,
+ T: for<'a> Deserialize<'a> + Default + Send,
{
- cx.new_model(move |cx| {
- cx.spawn(|tracked_file, mut cx| async move {
- while let Some(new_contents) = tracker.next().await {
- if !new_contents.trim().is_empty() {
- // String -> T (ZedTaskFormat)
- // String -> U (VsCodeFormat) -> Into::into T
- let Some(new_contents) =
- serde_json_lenient::from_str(&new_contents).log_err()
- else {
- continue;
- };
- tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile<T>, cx| {
- if tracked_file.parsed_contents != new_contents {
- tracked_file.parsed_contents = new_contents;
- cx.notify();
+ let parsed_contents: Arc<RwLock<T>> = Arc::default();
+ cx.background_executor()
+ .spawn({
+ let parsed_contents = parsed_contents.clone();
+ async move {
+ while let Some(new_contents) = tracker.next().await {
+ if Arc::strong_count(&parsed_contents) == 1 {
+ // We're no longer being observed. Stop polling.
+ break;
+ }
+ if !new_contents.trim().is_empty() {
+ let Some(new_contents) =
+ serde_json_lenient::from_str::<T>(&new_contents).log_err()
+ else {
+ continue;
};
- })?;
+ let mut contents = parsed_contents.write();
+ *contents = new_contents;
+ }
}
+ anyhow::Ok(())
}
- anyhow::Ok(())
})
.detach_and_log_err(cx);
- Self {
- parsed_contents: Default::default(),
- }
- })
+ Self { parsed_contents }
}
/// Initializes new [`TrackedFile`] with a type that's convertible from another deserializable type.
pub fn new_convertible<U: for<'a> Deserialize<'a> + TryInto<T, Error = anyhow::Error>>(
mut tracker: UnboundedReceiver<String>,
cx: &mut AppContext,
- ) -> Model<Self>
+ ) -> Self
where
- T: Default,
+ T: Default + Send,
{
- cx.new_model(move |cx| {
- cx.spawn(|tracked_file, mut cx| async move {
- while let Some(new_contents) = tracker.next().await {
- if !new_contents.trim().is_empty() {
- let Some(new_contents) =
- serde_json_lenient::from_str::<U>(&new_contents).log_err()
- else {
- continue;
- };
- let Some(new_contents) = new_contents.try_into().log_err() else {
- continue;
- };
- tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile<T>, cx| {
- if tracked_file.parsed_contents != new_contents {
- tracked_file.parsed_contents = new_contents;
- cx.notify();
+ let parsed_contents: Arc<RwLock<T>> = Arc::default();
+ cx.background_executor()
+ .spawn({
+ let parsed_contents = parsed_contents.clone();
+ async move {
+ while let Some(new_contents) = tracker.next().await {
+ if Arc::strong_count(&parsed_contents) == 1 {
+ // We're no longer being observed. Stop polling.
+ break;
+ }
+
+ if !new_contents.trim().is_empty() {
+ let Some(new_contents) =
+ serde_json_lenient::from_str::<U>(&new_contents).log_err()
+ else {
+ continue;
+ };
+ let Some(new_contents) = new_contents.try_into().log_err() else {
+ continue;
};
- })?;
+ let mut contents = parsed_contents.write();
+ *contents = new_contents;
+ }
}
+ anyhow::Ok(())
}
- anyhow::Ok(())
})
.detach_and_log_err(cx);
- Self {
- parsed_contents: Default::default(),
- }
- })
- }
-
- fn get(&self) -> &T {
- &self.parsed_contents
+ Self {
+ parsed_contents: Default::default(),
+ }
}
}
impl StaticSource {
/// Initializes the static source, reacting on tasks config changes.
- pub fn new(
- templates: Model<TrackedFile<TaskTemplates>>,
- cx: &mut AppContext,
- ) -> Model<Box<dyn TaskSource>> {
- cx.new_model(|cx| {
- let _subscription = cx.observe(
- &templates,
- move |source: &mut Box<(dyn TaskSource + 'static)>, new_templates, cx| {
- if let Some(static_source) = source.as_any().downcast_mut::<Self>() {
- static_source.tasks = new_templates.read(cx).get().clone();
- cx.notify();
- }
- },
- );
- Box::new(Self {
- tasks: TaskTemplates::default(),
- _templates: templates,
- _subscription,
- })
- })
- }
-}
-
-impl TaskSource for StaticSource {
- fn tasks_to_schedule(&mut self, _: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates {
- self.tasks.clone()
+ pub fn new(tasks: TrackedFile<TaskTemplates>) -> Self {
+ Self { tasks }
}
-
- fn as_any(&mut self) -> &mut dyn std::any::Any {
- self
+ /// Returns current list of tasks
+ pub fn tasks_to_schedule(&self) -> TaskTemplates {
+ self.tasks.parsed_contents.read().clone()
}
}
@@ -58,6 +58,10 @@ pub struct TaskTemplate {
/// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
#[serde(default)]
pub reveal: RevealStrategy,
+
+ /// Represents the tags which this template attaches to. Adding this removes this task from other UI.
+ #[serde(default)]
+ pub tags: Vec<String>,
}
/// What to do with the terminal pane and tab, after the command was started.
@@ -9,7 +9,6 @@ license = "GPL-3.0-or-later"
workspace = true
[dependencies]
-anyhow.workspace = true
editor.workspace = true
file_icons.workspace = true
fuzzy.workspace = true
@@ -1,18 +1,13 @@
-use std::{
- path::{Path, PathBuf},
- sync::Arc,
-};
+use std::sync::Arc;
use ::settings::Settings;
-use anyhow::Context;
-use editor::Editor;
+use editor::{tasks::task_context, Editor};
use gpui::{AppContext, ViewContext, WindowContext};
-use language::{BasicContextProvider, ContextProvider, Language};
+use language::Language;
use modal::TasksModal;
-use project::{Location, TaskSourceKind, WorktreeId};
-use task::{ResolvedTask, TaskContext, TaskTemplate, TaskVariables};
-use util::ResultExt;
-use workspace::Workspace;
+use project::WorktreeId;
+use workspace::tasks::schedule_task;
+use workspace::{tasks::schedule_resolved_task, Workspace};
mod modal;
mod settings;
@@ -97,9 +92,9 @@ fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) {
.update(&mut cx, |workspace, cx| {
let (worktree, language) = active_item_selection_properties(workspace, cx);
let tasks = workspace.project().update(cx, |project, cx| {
- project.task_inventory().update(cx, |inventory, cx| {
- inventory.list_tasks(language, worktree, cx)
- })
+ project
+ .task_inventory()
+ .update(cx, |inventory, _| inventory.list_tasks(language, worktree))
});
let (task_source_kind, target_task) =
tasks.into_iter().find(|(_, task)| task.label == name)?;
@@ -152,168 +147,6 @@ fn active_item_selection_properties(
(worktree_id, language)
}
-fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
- fn task_context_impl(workspace: &Workspace, cx: &mut WindowContext<'_>) -> Option<TaskContext> {
- let cwd = task_cwd(workspace, cx).log_err().flatten();
- let editor = workspace
- .active_item(cx)
- .and_then(|item| item.act_as::<Editor>(cx))?;
-
- let (selection, buffer, editor_snapshot) = editor.update(cx, |editor, cx| {
- let selection = editor.selections.newest::<usize>(cx);
- let (buffer, _, _) = editor
- .buffer()
- .read(cx)
- .point_to_buffer_offset(selection.start, cx)?;
- let snapshot = editor.snapshot(cx);
- Some((selection, buffer, snapshot))
- })?;
- let language_context_provider = buffer
- .read(cx)
- .language()
- .and_then(|language| language.context_provider())
- .unwrap_or_else(|| Arc::new(BasicContextProvider));
- let selection_range = selection.range();
- let start = editor_snapshot
- .display_snapshot
- .buffer_snapshot
- .anchor_after(selection_range.start)
- .text_anchor;
- let end = editor_snapshot
- .display_snapshot
- .buffer_snapshot
- .anchor_after(selection_range.end)
- .text_anchor;
- let worktree_abs_path = buffer
- .read(cx)
- .file()
- .map(|file| WorktreeId::from_usize(file.worktree_id()))
- .and_then(|worktree_id| {
- workspace
- .project()
- .read(cx)
- .worktree_for_id(worktree_id, cx)
- .map(|worktree| worktree.read(cx).abs_path())
- });
- let location = Location {
- buffer,
- range: start..end,
- };
- let task_variables = combine_task_variables(
- worktree_abs_path.as_deref(),
- location,
- language_context_provider.as_ref(),
- cx,
- )
- .log_err()?;
- Some(TaskContext {
- cwd,
- task_variables,
- })
- }
-
- task_context_impl(workspace, cx).unwrap_or_default()
-}
-
-fn combine_task_variables(
- worktree_abs_path: Option<&Path>,
- location: Location,
- context_provider: &dyn ContextProvider,
- cx: &mut WindowContext<'_>,
-) -> anyhow::Result<TaskVariables> {
- if context_provider.is_basic() {
- context_provider
- .build_context(worktree_abs_path, &location, cx)
- .context("building basic provider context")
- } else {
- let mut basic_context = BasicContextProvider
- .build_context(worktree_abs_path, &location, cx)
- .context("building basic default context")?;
- basic_context.extend(
- context_provider
- .build_context(worktree_abs_path, &location, cx)
- .context("building provider context ")?,
- );
- Ok(basic_context)
- }
-}
-
-fn schedule_task(
- workspace: &Workspace,
- task_source_kind: TaskSourceKind,
- task_to_resolve: &TaskTemplate,
- task_cx: &TaskContext,
- omit_history: bool,
- cx: &mut ViewContext<'_, Workspace>,
-) {
- if let Some(spawn_in_terminal) =
- task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
- {
- schedule_resolved_task(
- workspace,
- task_source_kind,
- spawn_in_terminal,
- omit_history,
- cx,
- );
- }
-}
-
-fn schedule_resolved_task(
- workspace: &Workspace,
- task_source_kind: TaskSourceKind,
- mut resolved_task: ResolvedTask,
- omit_history: bool,
- cx: &mut ViewContext<'_, Workspace>,
-) {
- if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
- if !omit_history {
- resolved_task.resolved = Some(spawn_in_terminal.clone());
- workspace.project().update(cx, |project, cx| {
- project.task_inventory().update(cx, |inventory, _| {
- inventory.task_scheduled(task_source_kind, resolved_task);
- })
- });
- }
- cx.emit(workspace::Event::SpawnTask(spawn_in_terminal));
- }
-}
-
-fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result<Option<PathBuf>> {
- let project = workspace.project().read(cx);
- let available_worktrees = project
- .worktrees()
- .filter(|worktree| {
- let worktree = worktree.read(cx);
- worktree.is_visible()
- && worktree.is_local()
- && worktree.root_entry().map_or(false, |e| e.is_dir())
- })
- .collect::<Vec<_>>();
- let cwd = match available_worktrees.len() {
- 0 => None,
- 1 => Some(available_worktrees[0].read(cx).abs_path()),
- _ => {
- let cwd_for_active_entry = project.active_entry().and_then(|entry_id| {
- available_worktrees.into_iter().find_map(|worktree| {
- let worktree = worktree.read(cx);
- if worktree.contains_entry(entry_id) {
- Some(worktree.abs_path())
- } else {
- None
- }
- })
- });
- anyhow::ensure!(
- cwd_for_active_entry.is_some(),
- "Cannot determine task cwd for multiple worktrees"
- );
- cwd_for_active_entry
- }
- };
- Ok(cwd.map(|path| path.to_path_buf()))
-}
-
#[cfg(test)]
mod tests {
use std::sync::Arc;
@@ -1,6 +1,6 @@
use std::sync::Arc;
-use crate::{active_item_selection_properties, schedule_resolved_task};
+use crate::active_item_selection_properties;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
impl_actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement,
@@ -16,7 +16,7 @@ use ui::{
Tooltip, WindowContext,
};
use util::ResultExt;
-use workspace::{ModalView, Workspace};
+use workspace::{tasks::schedule_resolved_task, ModalView, Workspace};
use serde::Deserialize;
@@ -211,12 +211,11 @@ impl PickerDelegate for TasksModalDelegate {
return Vec::new();
};
let (used, current) =
- picker.delegate.inventory.update(cx, |inventory, cx| {
+ picker.delegate.inventory.update(cx, |inventory, _| {
inventory.used_and_current_resolved_tasks(
language,
worktree,
&picker.delegate.task_context,
- cx,
)
});
picker.delegate.last_used_candidate_index = if used.is_empty() {
@@ -0,0 +1,83 @@
+use std::path::PathBuf;
+
+use project::TaskSourceKind;
+use task::{ResolvedTask, TaskContext, TaskTemplate};
+use ui::{ViewContext, WindowContext};
+
+use crate::Workspace;
+
+pub fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result<Option<PathBuf>> {
+ let project = workspace.project().read(cx);
+ let available_worktrees = project
+ .worktrees()
+ .filter(|worktree| {
+ let worktree = worktree.read(cx);
+ worktree.is_visible()
+ && worktree.is_local()
+ && worktree.root_entry().map_or(false, |e| e.is_dir())
+ })
+ .collect::<Vec<_>>();
+ let cwd = match available_worktrees.len() {
+ 0 => None,
+ 1 => Some(available_worktrees[0].read(cx).abs_path()),
+ _ => {
+ let cwd_for_active_entry = project.active_entry().and_then(|entry_id| {
+ available_worktrees.into_iter().find_map(|worktree| {
+ let worktree = worktree.read(cx);
+ if worktree.contains_entry(entry_id) {
+ Some(worktree.abs_path())
+ } else {
+ None
+ }
+ })
+ });
+ anyhow::ensure!(
+ cwd_for_active_entry.is_some(),
+ "Cannot determine task cwd for multiple worktrees"
+ );
+ cwd_for_active_entry
+ }
+ };
+ Ok(cwd.map(|path| path.to_path_buf()))
+}
+
+pub fn schedule_task(
+ workspace: &Workspace,
+ task_source_kind: TaskSourceKind,
+ task_to_resolve: &TaskTemplate,
+ task_cx: &TaskContext,
+ omit_history: bool,
+ cx: &mut ViewContext<'_, Workspace>,
+) {
+ if let Some(spawn_in_terminal) =
+ task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
+ {
+ schedule_resolved_task(
+ workspace,
+ task_source_kind,
+ spawn_in_terminal,
+ omit_history,
+ cx,
+ );
+ }
+}
+
+pub fn schedule_resolved_task(
+ workspace: &Workspace,
+ task_source_kind: TaskSourceKind,
+ mut resolved_task: ResolvedTask,
+ omit_history: bool,
+ cx: &mut ViewContext<'_, Workspace>,
+) {
+ if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
+ if !omit_history {
+ resolved_task.resolved = Some(spawn_in_terminal.clone());
+ workspace.project().update(cx, |project, cx| {
+ project.task_inventory().update(cx, |inventory, _| {
+ inventory.task_scheduled(task_source_kind, resolved_task);
+ })
+ });
+ }
+ cx.emit(crate::Event::SpawnTask(spawn_in_terminal));
+ }
+}
@@ -8,6 +8,7 @@ mod persistence;
pub mod searchable;
pub mod shared_screen;
mod status_bar;
+pub mod tasks;
mod toolbar;
mod workspace_settings;
@@ -297,7 +297,7 @@ fn init_ui(args: Args) {
load_user_themes_in_background(fs.clone(), cx);
watch_themes(fs.clone(), cx);
-
+ watch_languages(fs.clone(), languages.clone(), cx);
watch_file_types(fs.clone(), cx);
languages.set_theme(cx.theme().clone());
@@ -861,6 +861,37 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
.detach()
}
+#[cfg(debug_assertions)]
+fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>, cx: &mut AppContext) {
+ use std::time::Duration;
+
+ let path = {
+ let p = Path::new("crates/languages/src");
+ let Ok(full_path) = p.canonicalize() else {
+ return;
+ };
+ full_path
+ };
+
+ cx.spawn(|_| async move {
+ let mut events = fs.watch(path.as_path(), Duration::from_millis(100)).await;
+ while let Some(event) = events.next().await {
+ let has_language_file = event.iter().any(|path| {
+ path.extension()
+ .map(|ext| ext.to_string_lossy().as_ref() == "scm")
+ .unwrap_or(false)
+ });
+ if has_language_file {
+ languages.reload();
+ }
+ }
+ })
+ .detach()
+}
+
+#[cfg(not(debug_assertions))]
+fn watch_languages(_fs: Arc<dyn fs::Fs>, _languages: Arc<LanguageRegistry>, _cx: &mut AppContext) {}
+
#[cfg(debug_assertions)]
fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
use std::time::Duration;
@@ -168,19 +168,14 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
project.update(cx, |project, cx| {
let fs = app_state.fs.clone();
project.task_inventory().update(cx, |inventory, cx| {
+ let tasks_file_rx =
+ watch_config_file(&cx.background_executor(), fs, paths::TASKS.clone());
inventory.add_source(
TaskSourceKind::AbsPath {
id_base: "global_tasks",
abs_path: paths::TASKS.clone(),
},
- |cx| {
- let tasks_file_rx = watch_config_file(
- &cx.background_executor(),
- fs,
- paths::TASKS.clone(),
- );
- StaticSource::new(TrackedFile::new(tasks_file_rx, cx), cx)
- },
+ StaticSource::new(TrackedFile::new(tasks_file_rx, cx)),
cx,
);
})