From 5a71d8c7f1d2887daf62869fb6012c33d59a3cd0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sun, 5 May 2024 16:32:48 +0200 Subject: [PATCH] Add support for detecting tests in source files, and implement it for Rust (#11195) Continuing work from #10873 Release Notes: - N/A --------- Co-authored-by: Mikayla --- Cargo.lock | 3 +- crates/collab/src/tests/editor_tests.rs | 2 +- crates/editor/Cargo.toml | 1 + crates/editor/src/actions.rs | 8 +- crates/editor/src/editor.rs | 477 ++++++++++++++++++++--- crates/editor/src/element.rs | 179 +++++++-- crates/editor/src/mouse_context_menu.rs | 2 +- crates/editor/src/tasks.rs | 118 ++++++ crates/language/src/buffer.rs | 56 ++- crates/language/src/language.rs | 43 ++ crates/language/src/language_registry.rs | 2 + crates/language/src/syntax_map.rs | 6 + crates/languages/src/rust.rs | 1 + crates/languages/src/rust/runnables.scm | 7 + crates/multi_buffer/src/multi_buffer.rs | 27 +- crates/project/src/project.rs | 25 +- crates/project/src/project_tests.rs | 79 ++-- crates/project/src/task_inventory.rs | 221 ++++------- crates/task/Cargo.toml | 1 + crates/task/src/lib.rs | 19 +- crates/task/src/static_source.rs | 152 +++----- crates/task/src/task_template.rs | 4 + crates/tasks_ui/Cargo.toml | 1 - crates/tasks_ui/src/lib.rs | 185 +-------- crates/tasks_ui/src/modal.rs | 7 +- crates/workspace/src/tasks.rs | 83 ++++ crates/workspace/src/workspace.rs | 1 + crates/zed/src/main.rs | 33 +- crates/zed/src/zed.rs | 11 +- 29 files changed, 1148 insertions(+), 606 deletions(-) create mode 100644 crates/editor/src/tasks.rs create mode 100644 crates/languages/src/rust/runnables.scm create mode 100644 crates/workspace/src/tasks.rs diff --git a/Cargo.lock b/Cargo.lock index bdd14f4ea0..f87a7c0473 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 097a2842c8..e46702ad94 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -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, ); diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index cd851042aa..113952d7fe 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -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 diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 143a5f4a52..1002df6ad0 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -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, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct ToggleTestRunner { + #[serde(default)] + pub deployed_from_row: Option, } #[derive(PartialEq, Clone, Deserialize, Default)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e03ef6c08d..01d290fb5b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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>, expect_bounds_change: Option>, + tasks: HashMap, } #[derive(Clone)] @@ -1180,12 +1198,106 @@ impl CompletionsMenu { } #[derive(Clone)] +struct CodeActionContents { + tasks: Option>, + actions: Option>, +} + +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 + '_ { + 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 { + 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, selected_item: usize, scroll_handle: UniformListScrollHandle, - deployed_from_indicator: bool, + deployed_from_indicator: Option, } 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) { 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::(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, ) -> Option { @@ -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, + ) -> 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>, @@ -7400,6 +7689,80 @@ impl Editor { self.select_larger_syntax_node_stack = stack; } + fn runnable_display_rows( + &self, + range: Range, + 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, + ) { + 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, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9c925b4b00..c5fed1fdf4 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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, + gutter_dimensions: &GutterDimensions, + gutter_hitbox: &Hitbox, + cx: &mut WindowContext, + ) -> Vec { + 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 { 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, + 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, 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)>, max_row: u32, code_actions_indicator: Option, + test_indicators: Vec, fold_indicators: Vec>, mouse_context_menu: Option, tab_invisible: ShapedLine, diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index ac97fa0dc0..c3a6be3394 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -79,7 +79,7 @@ pub fn deploy_context_menu( .action( "Code Actions", Box::new(ToggleCodeActions { - deployed_from_indicator: false, + deployed_from_indicator: None, }), ) .separator() diff --git a/crates/editor/src/tasks.rs b/crates/editor/src/tasks.rs new file mode 100644 index 0000000000..384d3d7ae9 --- /dev/null +++ b/crates/editor/src/tasks.rs @@ -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 { + 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 { + let (selection, buffer, editor_snapshot) = { + let selection = editor.selections.newest::(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::(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 { + 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) + } +} diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 5bebc16e4e..49b2f6a30f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -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, + pub buffer: BufferId, +} + impl Buffer { /// Create a new buffer with the given base text. pub fn local>(base_text: T, cx: &mut ModelContext) -> Self { @@ -2978,6 +2985,53 @@ impl BufferSnapshot { }) } + pub fn runnable_ranges( + &self, + range: Range, + ) -> impl Iterator, 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::>(); + + 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( diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index dc92f0a2d3..b701ed36c7 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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, pub(crate) brackets_config: Option, pub(crate) redactions_config: Option, + pub(crate) runnable_config: Option, pub(crate) indents_config: Option, pub outline_config: Option, pub embedding_config: Option, @@ -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, + /// index of the capture that corresponds to @run + pub run_capture_ix: u32, +} + struct OverrideConfig { query: Query, values: HashMap, @@ -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 { + 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 { let grammar = self .grammar_mut() diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index d6cf7a4d37..cf0b869d61 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -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>, pub overrides: Option>, pub redactions: Option>, + pub runnables: Option>, } #[derive(Clone, Default)] diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 4dd96d0773..765ff33096 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -56,6 +56,7 @@ pub struct SyntaxMapCapture<'a> { #[derive(Debug)] pub struct SyntaxMapMatch<'a> { + pub language: Arc, pub depth: usize, pub pattern_index: usize, pub captures: &'a [QueryCapture<'a>], @@ -71,6 +72,7 @@ struct SyntaxMapCapturesLayer<'a> { } struct SyntaxMapMatchesLayer<'a> { + language: Arc, depth: usize, next_pattern_index: usize, next_captures: Vec>, @@ -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 { 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, diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 084af44120..c9758ae20e 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -389,6 +389,7 @@ impl ContextProvider for RustContextProvider { "--".into(), "--nocapture".into(), ], + tags: vec!["rust-test".to_owned()], ..TaskTemplate::default() }, TaskTemplate { diff --git a/crates/languages/src/rust/runnables.scm b/crates/languages/src/rust/runnables.scm new file mode 100644 index 0000000000..47035bb0b3 --- /dev/null +++ b/crates/languages/src/rust/runnables.scm @@ -0,0 +1,7 @@ +( + (attribute_item (attribute) @_attribute + (#match? @_attribute ".*test.*")) + . + (function_item + name: (_) @run) +) @rust-test diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 6ebcf98526..5d6a69b901 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -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, + ) -> impl Iterator, 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 } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 28c6182016..160a371be5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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::( - tasks_file_rx, - cx, - ), + StaticSource::new( + TrackedFile::new_convertible::( + tasks_file_rx, cx, - ) - }, + ), + ), cx, ); } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 763e316e7a..e61fe15599 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -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>) -> TaskTemplates { - self.tasks.clone() - } -} - #[gpui::test] async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { init_test(cx); diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 302aa82d50..73ce0b1739 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -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>, - _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) -> Model>, + source: StaticSource, cx: &mut ModelContext, ) { 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(&self) -> Option<(Model>, TaskSourceKind)> { - let target_type_id = std::any::TypeId::of::(); - 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>, worktree: Option, - 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>, worktree: Option, 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 { #[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, - } - - impl StaticTestSource { - pub(super) fn new( - task_names: impl IntoIterator, - cx: &mut AppContext, - ) -> Model> { - 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 - }) - } - } - - impl TaskSource for StaticTestSource { - fn tasks_to_schedule( - &mut self, - _cx: &mut ModelContext>, - ) -> 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, + 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, cx: &mut TestAppContext, ) -> Vec { - 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, cx: &mut TestAppContext, ) -> Vec { - 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, 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 { diff --git a/crates/task/Cargo.toml b/crates/task/Cargo.toml index a85fdda379..43e3060a4e 100644 --- a/crates/task/Cargo.toml +++ b/crates/task/Cargo.toml @@ -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 diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 2e3a0e778d..1e57dc7cde 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -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>) -> 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); diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs index 393a4cfe4b..324183b688 100644 --- a/crates/task/src/static_source.rs +++ b/crates/task/src/static_source.rs @@ -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>, - _subscription: Subscription, + tasks: TrackedFile, } /// 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 { - parsed_contents: T, + parsed_contents: Arc>, } -impl TrackedFile { +impl TrackedFile { /// Initializes new [`TrackedFile`] with a type that's deserializable. - pub fn new(mut tracker: UnboundedReceiver, cx: &mut AppContext) -> Model + pub fn new(mut tracker: UnboundedReceiver, 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, cx| { - if tracked_file.parsed_contents != new_contents { - tracked_file.parsed_contents = new_contents; - cx.notify(); + let parsed_contents: Arc> = 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::(&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 Deserialize<'a> + TryInto>( mut tracker: UnboundedReceiver, cx: &mut AppContext, - ) -> Model + ) -> 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::(&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, cx| { - if tracked_file.parsed_contents != new_contents { - tracked_file.parsed_contents = new_contents; - cx.notify(); + let parsed_contents: Arc> = 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::(&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>, - cx: &mut AppContext, - ) -> Model> { - 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::() { - 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>) -> TaskTemplates { - self.tasks.clone() - } - - fn as_any(&mut self) -> &mut dyn std::any::Any { - self + pub fn new(tasks: TrackedFile) -> Self { + Self { tasks } + } + /// Returns current list of tasks + pub fn tasks_to_schedule(&self) -> TaskTemplates { + self.tasks.parsed_contents.read().clone() } } diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 397c83e94c..f2e974a3a2 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -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, } /// What to do with the terminal pane and tab, after the command was started. diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml index 67f62e2dcf..ca6af81ccd 100644 --- a/crates/tasks_ui/Cargo.toml +++ b/crates/tasks_ui/Cargo.toml @@ -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 diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 2b11fba49a..20a72e37de 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -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) { .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 { - let cwd = task_cwd(workspace, cx).log_err().flatten(); - let editor = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx))?; - - let (selection, buffer, editor_snapshot) = editor.update(cx, |editor, cx| { - let selection = editor.selections.newest::(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 { - 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> { - 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::>(); - 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; diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 3d895197a4..1a67278620 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -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() { diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs new file mode 100644 index 0000000000..4b0bc11d2b --- /dev/null +++ b/crates/workspace/src/tasks.rs @@ -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> { + 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::>(); + 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)); + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index aab9a860c9..326a70e85a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -8,6 +8,7 @@ mod persistence; pub mod searchable; pub mod shared_screen; mod status_bar; +pub mod tasks; mod toolbar; mod workspace_settings; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3b2e96965e..f03cd0d639 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -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, cx: &mut AppContext) { .detach() } +#[cfg(debug_assertions)] +fn watch_languages(fs: Arc, languages: Arc, 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, _languages: Arc, _cx: &mut AppContext) {} + #[cfg(debug_assertions)] fn watch_file_types(fs: Arc, cx: &mut AppContext) { use std::time::Duration; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 14cc9febd2..05eae13153 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -168,19 +168,14 @@ pub fn initialize_workspace(app_state: Arc, 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, ); })