Skip to repository content1480 lines · 53.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:33:12.391Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
runnables.rs
1use std::{collections::BTreeMap, mem, ops::Range, sync::Arc};
2
3use clock::Global;
4use collections::{HashMap, HashSet};
5use gpui::{
6 App, AppContext as _, AsyncWindowContext, ClickEvent, Context, Entity, Focusable as _,
7 MouseButton, Task, Window,
8};
9use language::{Buffer, BufferRow, Runnable};
10use lsp::LanguageServerName;
11use multi_buffer::{Anchor, BufferOffset, MultiBufferRow, MultiBufferSnapshot, ToPoint as _};
12use project::{Location, Project, TaskSourceKind, project_settings::ProjectSettings};
13use settings::Settings as _;
14use smallvec::SmallVec;
15use task::{ResolvedTask, RunnableTag, TaskContext, TaskTemplate, TaskVariables, VariableName};
16use text::{BufferId, OffsetRangeExt as _, ToOffset as _, ToPoint as _};
17use ui::{Clickable as _, Color, IconButton, IconSize, Toggleable as _};
18
19use crate::{
20 CodeActionSource, Editor, EditorSettings, EditorStyle, RangeToAnchorExt, SpawnNearestTask,
21 ToggleCodeActions, UPDATE_DEBOUNCE,
22 display_map::{DisplayPoint, DisplayRow},
23};
24
25#[derive(Debug)]
26pub(super) struct RunnableData {
27 runnables: HashMap<BufferId, (Global, BTreeMap<BufferRow, RunnableTasks>)>,
28 task_statuses: HashMap<(BufferId, BufferRow), RunnableTaskStatus>,
29 invalidate_buffer_data: HashSet<BufferId>,
30 runnables_update_task: Task<()>,
31}
32
33impl RunnableData {
34 pub fn new() -> Self {
35 Self {
36 runnables: HashMap::default(),
37 task_statuses: HashMap::default(),
38 invalidate_buffer_data: HashSet::default(),
39 runnables_update_task: Task::ready(()),
40 }
41 }
42
43 pub fn runnables(
44 &self,
45 (buffer_id, buffer_row): (BufferId, BufferRow),
46 ) -> Option<&RunnableTasks> {
47 self.runnables.get(&buffer_id)?.1.get(&buffer_row)
48 }
49
50 pub fn all_runnables(&self) -> impl Iterator<Item = &RunnableTasks> {
51 self.runnables
52 .values()
53 .flat_map(|(_, tasks)| tasks.values())
54 }
55
56 pub fn task_status(
57 &self,
58 (buffer_id, buffer_row): (BufferId, BufferRow),
59 ) -> Option<RunnableTaskStatus> {
60 self.task_statuses.get(&(buffer_id, buffer_row)).copied()
61 }
62
63 pub fn set_task_status(
64 &mut self,
65 (buffer_id, buffer_row): (BufferId, BufferRow),
66 status: RunnableTaskStatus,
67 ) {
68 self.task_statuses.insert((buffer_id, buffer_row), status);
69 }
70
71 pub fn clear_task_status(&mut self, (buffer_id, buffer_row): (BufferId, BufferRow)) {
72 self.task_statuses.remove(&(buffer_id, buffer_row));
73 }
74
75 pub fn task_key_for_offset(
76 &self,
77 buffer_id: BufferId,
78 offset: BufferOffset,
79 ) -> Option<(BufferId, BufferRow)> {
80 let (_, tasks_by_row) = self.runnables.get(&buffer_id)?;
81 tasks_by_row
82 .iter()
83 .find(|(_, tasks)| tasks.context_range.contains(&offset))
84 .or_else(|| {
85 tasks_by_row
86 .iter()
87 .find(|(_, tasks)| tasks.context_range.start >= offset)
88 })
89 .map(|(row, _)| (buffer_id, *row))
90 }
91
92 pub fn has_cached(&self, buffer_id: BufferId, version: &Global) -> bool {
93 self.runnables
94 .get(&buffer_id)
95 .is_some_and(|(cached_version, _)| !version.changed_since(cached_version))
96 }
97
98 #[cfg(test)]
99 pub fn insert(
100 &mut self,
101 buffer_id: BufferId,
102 buffer_row: BufferRow,
103 version: Global,
104 tasks: RunnableTasks,
105 ) {
106 self.runnables
107 .entry(buffer_id)
108 .or_insert_with(|| (version, BTreeMap::default()))
109 .1
110 .insert(buffer_row, tasks);
111 }
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub(crate) enum RunnableTaskStatus {
116 Running,
117 Passed,
118 Failed,
119}
120
121impl From<workspace::tasks::ScheduledTaskResult> for RunnableTaskStatus {
122 fn from(result: workspace::tasks::ScheduledTaskResult) -> Self {
123 match result {
124 workspace::tasks::ScheduledTaskResult::Success => Self::Passed,
125 workspace::tasks::ScheduledTaskResult::Failure
126 | workspace::tasks::ScheduledTaskResult::SpawnFailed
127 | workspace::tasks::ScheduledTaskResult::Cancelled => Self::Failed,
128 }
129 }
130}
131
132#[derive(Clone, Debug)]
133pub struct RunnableTasks {
134 pub templates: Vec<(TaskSourceKind, TaskTemplate)>,
135 pub offset: multi_buffer::Anchor,
136 // We need the column at which the task context evaluation should take place (when we're spawning it via gutter).
137 pub column: u32,
138 // Values of all named captures, including those starting with '_'
139 pub extra_variables: HashMap<String, String>,
140 // Full range of the tagged region. We use it to determine which `extra_variables` to grab for context resolution in e.g. a modal.
141 pub context_range: Range<BufferOffset>,
142}
143
144impl RunnableTasks {
145 pub fn resolve<'a>(
146 &'a self,
147 cx: &'a task::TaskContext,
148 ) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a {
149 self.templates.iter().filter_map(|(kind, template)| {
150 template
151 .resolve_task(&kind.to_id_base(), cx)
152 .map(|task| (kind.clone(), task))
153 })
154 }
155}
156
157#[derive(Clone)]
158pub struct ResolvedTasks {
159 pub templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
160 pub position: Anchor,
161}
162
163impl Editor {
164 pub fn refresh_runnables(
165 &mut self,
166 invalidate_buffer_data: Option<BufferId>,
167 window: &mut Window,
168 cx: &mut Context<Self>,
169 ) {
170 if !self.mode().is_full()
171 || !EditorSettings::get_global(cx).gutter.runnables
172 || !self.enable_runnables
173 {
174 self.clear_runnables(None);
175 return;
176 }
177 if let Some(buffer) = self.buffer().read(cx).as_singleton() {
178 let buffer_read = buffer.read(cx);
179 if buffer_read.file().is_none() {
180 self.clear_runnables(None);
181 return;
182 }
183 let buffer_id = buffer_read.remote_id();
184 if invalidate_buffer_data != Some(buffer_id)
185 && self.runnables.has_cached(buffer_id, &buffer_read.version())
186 {
187 return;
188 }
189 }
190 if let Some(buffer_id) = invalidate_buffer_data {
191 self.runnables.invalidate_buffer_data.insert(buffer_id);
192 }
193
194 let project = self.project().map(Entity::downgrade);
195 let lsp_task_sources = self.lsp_task_sources(true, true, cx);
196 let multi_buffer = self.buffer.downgrade();
197 self.runnables.runnables_update_task = cx.spawn_in(window, async move |editor, cx| {
198 cx.background_executor().timer(UPDATE_DEBOUNCE).await;
199 let Some(project) = project.and_then(|p| p.upgrade()) else {
200 return;
201 };
202
203 let hide_runnables = project.update(cx, |project, _| project.is_via_collab());
204 if hide_runnables {
205 return;
206 }
207 let lsp_tasks = if lsp_task_sources.is_empty() {
208 Vec::new()
209 } else {
210 let Ok(lsp_tasks) = cx
211 .update(|_, cx| crate::lsp_tasks(project.clone(), &lsp_task_sources, None, cx))
212 else {
213 return;
214 };
215 lsp_tasks.await
216 };
217 let new_rows = {
218 let Some((multi_buffer_snapshot, multi_buffer_query_range)) = editor
219 .update(cx, |editor, cx| {
220 let multi_buffer = editor.buffer().read(cx);
221 if multi_buffer.is_singleton() {
222 Some((multi_buffer.snapshot(cx), Anchor::Min..Anchor::Max))
223 } else {
224 let display_snapshot =
225 editor.display_map.update(cx, |map, cx| map.snapshot(cx));
226 let multi_buffer_query_range =
227 editor.multi_buffer_visible_range(&display_snapshot, cx);
228 let multi_buffer_snapshot = display_snapshot.buffer();
229 Some((
230 multi_buffer_snapshot.clone(),
231 multi_buffer_query_range.to_anchors(&multi_buffer_snapshot),
232 ))
233 }
234 })
235 .ok()
236 .flatten()
237 else {
238 return;
239 };
240 cx.background_spawn({
241 async move {
242 multi_buffer_snapshot
243 .runnable_ranges(multi_buffer_query_range)
244 .collect()
245 }
246 })
247 .await
248 };
249
250 let Ok(multi_buffer_snapshot) =
251 editor.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
252 else {
253 return;
254 };
255 let Ok(mut lsp_tasks_by_rows) = cx.update(|_, cx| {
256 lsp_tasks
257 .into_iter()
258 .flat_map(|(kind, tasks)| {
259 tasks.into_iter().filter_map(move |(location, task)| {
260 Some((kind.clone(), location?, task))
261 })
262 })
263 .fold(HashMap::default(), |mut acc, (kind, location, task)| {
264 let buffer = location.target.buffer;
265 let buffer_snapshot = buffer.read(cx).snapshot();
266 let offset =
267 multi_buffer_snapshot.anchor_in_excerpt(location.target.range.start);
268 if let Some(offset) = offset {
269 let task_buffer_range =
270 location.target.range.to_point(&buffer_snapshot);
271 let context_buffer_range =
272 task_buffer_range.to_offset(&buffer_snapshot);
273 let context_range = BufferOffset(context_buffer_range.start)
274 ..BufferOffset(context_buffer_range.end);
275
276 acc.entry((buffer_snapshot.remote_id(), task_buffer_range.start.row))
277 .or_insert_with(|| RunnableTasks {
278 templates: Vec::new(),
279 offset,
280 column: task_buffer_range.start.column,
281 extra_variables: HashMap::default(),
282 context_range,
283 })
284 .templates
285 .push((kind, task.original_task().clone()));
286 }
287
288 acc
289 })
290 }) else {
291 return;
292 };
293
294 let Ok(prefer_lsp) = multi_buffer.update(cx, |buffer, cx| {
295 buffer.language_settings(cx).tasks.prefer_lsp
296 }) else {
297 return;
298 };
299
300 let rows = Self::runnable_rows(
301 project,
302 multi_buffer_snapshot,
303 prefer_lsp && !lsp_tasks_by_rows.is_empty(),
304 new_rows,
305 cx.clone(),
306 )
307 .await;
308 editor
309 .update(cx, |editor, cx| {
310 for buffer_id in std::mem::take(&mut editor.runnables.invalidate_buffer_data) {
311 editor.clear_runnables(Some(buffer_id));
312 }
313
314 for ((buffer_id, row), mut new_tasks) in rows {
315 let Some(buffer) = editor.buffer().read(cx).buffer(buffer_id) else {
316 continue;
317 };
318
319 if let Some(lsp_tasks) = lsp_tasks_by_rows.remove(&(buffer_id, row)) {
320 new_tasks.templates.extend(lsp_tasks.templates);
321 }
322 editor.insert_runnables(
323 buffer_id,
324 buffer.read(cx).version(),
325 row,
326 new_tasks,
327 );
328 }
329 for ((buffer_id, row), new_tasks) in lsp_tasks_by_rows {
330 let Some(buffer) = editor.buffer().read(cx).buffer(buffer_id) else {
331 continue;
332 };
333 editor.insert_runnables(
334 buffer_id,
335 buffer.read(cx).version(),
336 row,
337 new_tasks,
338 );
339 }
340 })
341 .ok();
342 });
343 }
344
345 pub fn spawn_nearest_task(
346 &mut self,
347 action: &SpawnNearestTask,
348 window: &mut Window,
349 cx: &mut Context<Self>,
350 ) {
351 let Some((workspace, _)) = self.workspace.clone() else {
352 return;
353 };
354 let Some(project) = self.project.clone() else {
355 return;
356 };
357
358 // Try to find a closest, enclosing node using tree-sitter that has a task
359 let Some((buffer, buffer_row, tasks)) = self
360 .find_enclosing_node_task(cx)
361 // Or find the task that's closest in row-distance.
362 .or_else(|| self.find_closest_task(cx))
363 else {
364 return;
365 };
366
367 let buffer_id = buffer.read(cx).remote_id();
368 let editor = cx.weak_entity();
369 let reveal_strategy = action.reveal;
370 let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
371 cx.spawn_in(window, async move |_, cx| {
372 let context = task_context.await.ok().flatten()?;
373 let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?;
374
375 let resolved = &mut resolved_task.resolved;
376 resolved.reveal = reveal_strategy;
377
378 editor
379 .update(cx, |editor, cx| {
380 editor.set_runnable_task_status(
381 buffer_id,
382 buffer_row,
383 RunnableTaskStatus::Running,
384 cx,
385 );
386 })
387 .ok();
388 workspace
389 .update_in(cx, |workspace, window, cx| {
390 workspace.schedule_resolved_task_with_completion(
391 task_source_kind,
392 resolved_task,
393 false,
394 move |result, cx| {
395 editor
396 .update(cx, |editor, cx| {
397 editor.set_runnable_task_status(
398 buffer_id,
399 buffer_row,
400 RunnableTaskStatus::from(result),
401 cx,
402 );
403 })
404 .ok();
405 },
406 window,
407 cx,
408 );
409 })
410 .ok()
411 })
412 .detach();
413 }
414
415 pub fn clear_runnables(&mut self, for_buffer: Option<BufferId>) {
416 if let Some(buffer_id) = for_buffer {
417 self.runnables.runnables.remove(&buffer_id);
418 self.runnables
419 .task_statuses
420 .retain(|(status_buffer_id, _), _| *status_buffer_id != buffer_id);
421 } else {
422 self.runnables.runnables.clear();
423 self.runnables.task_statuses.clear();
424 }
425 self.runnables.invalidate_buffer_data.clear();
426 self.runnables.runnables_update_task = Task::ready(());
427 }
428
429 pub(crate) fn runnable_task_status(
430 &self,
431 buffer_id: BufferId,
432 buffer_row: BufferRow,
433 ) -> Option<RunnableTaskStatus> {
434 self.runnables.task_status((buffer_id, buffer_row))
435 }
436
437 pub(crate) fn set_runnable_task_status(
438 &mut self,
439 buffer_id: BufferId,
440 buffer_row: BufferRow,
441 status: RunnableTaskStatus,
442 cx: &mut Context<Self>,
443 ) {
444 self.runnables
445 .set_task_status((buffer_id, buffer_row), status);
446 cx.notify();
447 }
448
449 pub(crate) fn clear_runnable_task_status(
450 &mut self,
451 buffer_id: BufferId,
452 buffer_row: BufferRow,
453 cx: &mut Context<Self>,
454 ) {
455 self.runnables.clear_task_status((buffer_id, buffer_row));
456 cx.notify();
457 }
458
459 pub(crate) fn runnable_task_key_for_display_row(
460 &self,
461 display_row: DisplayRow,
462 window: &mut Window,
463 cx: &mut Context<Self>,
464 ) -> Option<(BufferId, BufferRow)> {
465 let snapshot = self.snapshot(window, cx);
466 let multibuffer_row =
467 MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(&snapshot).row);
468 let (buffer_snapshot, range) = snapshot
469 .buffer_snapshot()
470 .buffer_line_for_row(multibuffer_row)?;
471 Some((buffer_snapshot.remote_id(), range.start.row))
472 }
473
474 pub(crate) fn runnable_task_key_for_offset(
475 &self,
476 buffer_id: BufferId,
477 offset: BufferOffset,
478 ) -> Option<(BufferId, BufferRow)> {
479 self.runnables.task_key_for_offset(buffer_id, offset)
480 }
481
482 pub fn task_context(&self, window: &mut Window, cx: &mut App) -> Task<Option<TaskContext>> {
483 let Some(project) = self.project.clone() else {
484 return Task::ready(None);
485 };
486 let (selection, buffer, editor_snapshot) = {
487 let selection = self.selections.newest_adjusted(&self.display_snapshot(cx));
488 let Some((buffer, _)) = self
489 .buffer()
490 .read(cx)
491 .point_to_buffer_offset(selection.start, cx)
492 else {
493 return Task::ready(None);
494 };
495 let snapshot = self.snapshot(window, cx);
496 (selection, buffer, snapshot)
497 };
498 let selection_range = selection.range();
499 let Some((_, range)) = editor_snapshot
500 .display_snapshot
501 .buffer_snapshot()
502 .anchor_range_to_buffer_anchor_range(
503 editor_snapshot
504 .display_snapshot
505 .buffer_snapshot()
506 .anchor_after(selection_range.start)
507 ..editor_snapshot
508 .display_snapshot
509 .buffer_snapshot()
510 .anchor_before(selection_range.end),
511 )
512 else {
513 return Task::ready(None);
514 };
515 let location = Location { buffer, range };
516 let captured_variables = {
517 let mut variables = TaskVariables::default();
518 let buffer = location.buffer.read(cx);
519 let buffer_id = buffer.remote_id();
520 let snapshot = buffer.snapshot();
521 let starting_point = location.range.start.to_point(&snapshot);
522 let starting_offset = starting_point.to_offset(&snapshot);
523 for (_, tasks) in self
524 .runnables
525 .runnables
526 .get(&buffer_id)
527 .into_iter()
528 .flat_map(|(_, tasks)| tasks.range(0..starting_point.row + 1))
529 {
530 if !tasks
531 .context_range
532 .contains(&crate::BufferOffset(starting_offset))
533 {
534 continue;
535 }
536 for (capture_name, value) in tasks.extra_variables.iter() {
537 variables.insert(
538 VariableName::Custom(capture_name.to_owned().into()),
539 value.clone(),
540 );
541 }
542 }
543 variables
544 };
545
546 let task = project.update(cx, |project, cx| {
547 project.task_store().update(cx, |task_store, cx| {
548 task_store.task_context_for_location(captured_variables, location, cx)
549 })
550 });
551 cx.background_spawn(async move { task.await.ok().flatten() })
552 }
553
554 pub fn lsp_task_sources(
555 &self,
556 visible_only: bool,
557 skip_cached: bool,
558 cx: &mut Context<Self>,
559 ) -> HashMap<LanguageServerName, Vec<BufferId>> {
560 if !self.lsp_data_enabled() {
561 return HashMap::default();
562 }
563 let buffers = if visible_only {
564 self.visible_buffers(cx)
565 .into_iter()
566 .filter(|buffer| self.is_lsp_relevant(buffer.read(cx).file(), cx))
567 .collect()
568 } else {
569 self.buffer().read(cx).all_buffers()
570 };
571
572 let lsp_settings = &ProjectSettings::get_global(cx).lsp;
573
574 buffers
575 .into_iter()
576 .filter_map(|buffer| {
577 let lsp_tasks_source = buffer
578 .read(cx)
579 .language()?
580 .context_provider()?
581 .lsp_task_source()?;
582 if lsp_settings
583 .get(&lsp_tasks_source)
584 .is_none_or(|s| s.enable_lsp_tasks)
585 {
586 let buffer_id = buffer.read(cx).remote_id();
587 if skip_cached
588 && self
589 .runnables
590 .has_cached(buffer_id, &buffer.read(cx).version())
591 {
592 None
593 } else {
594 Some((lsp_tasks_source, buffer_id))
595 }
596 } else {
597 None
598 }
599 })
600 .fold(
601 HashMap::default(),
602 |mut acc, (lsp_task_source, buffer_id)| {
603 acc.entry(lsp_task_source)
604 .or_insert_with(Vec::new)
605 .push(buffer_id);
606 acc
607 },
608 )
609 }
610
611 pub fn find_enclosing_node_task(
612 &mut self,
613 cx: &mut Context<Self>,
614 ) -> Option<(Entity<Buffer>, u32, Arc<RunnableTasks>)> {
615 let snapshot = self.buffer.read(cx).snapshot(cx);
616 let anchor = self.selections.newest_anchor().head();
617 let (anchor, buffer_snapshot) = snapshot.anchor_to_buffer_anchor(anchor)?;
618 let offset = anchor.to_offset(buffer_snapshot);
619
620 let layer = buffer_snapshot.syntax_layer_at(offset)?;
621 let mut cursor = layer.node().walk();
622
623 while cursor.goto_first_child_for_byte(offset).is_some() {
624 if cursor.node().end_byte() == offset {
625 cursor.goto_next_sibling();
626 }
627 }
628
629 // Ascend to the smallest ancestor that contains the range and has a task.
630 loop {
631 let node = cursor.node();
632 let node_range = node.byte_range();
633 let symbol_start_row = buffer_snapshot.offset_to_point(node.start_byte()).row;
634
635 // Check if this node contains our offset
636 if node_range.start <= offset && node_range.end >= offset {
637 // If it contains offset, check for task
638 if let Some(tasks) = self
639 .runnables
640 .runnables
641 .get(&buffer_snapshot.remote_id())
642 .and_then(|(_, tasks)| tasks.get(&symbol_start_row))
643 {
644 let buffer = self.buffer.read(cx).buffer(buffer_snapshot.remote_id())?;
645 return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned())));
646 }
647 }
648
649 if !cursor.goto_parent() {
650 break;
651 }
652 }
653 None
654 }
655
656 pub(crate) fn render_run_indicator(
657 &self,
658 _style: &EditorStyle,
659 is_active: bool,
660 active_breakpoint: Option<Anchor>,
661 task_status: Option<RunnableTaskStatus>,
662 row: DisplayRow,
663 cx: &mut Context<Self>,
664 ) -> IconButton {
665 let (icon, color) = match task_status {
666 Some(RunnableTaskStatus::Running) => (ui::IconName::PlayOutlined, Color::Accent),
667 Some(RunnableTaskStatus::Passed) => (ui::IconName::Check, Color::Success),
668 Some(RunnableTaskStatus::Failed) => (ui::IconName::XCircle, Color::Error),
669 None => (ui::IconName::PlayOutlined, Color::Muted),
670 };
671
672 IconButton::new(("run_indicator", row.0 as usize), icon)
673 .shape(ui::IconButtonShape::Square)
674 .icon_size(IconSize::XSmall)
675 .icon_color(color)
676 .toggle_state(is_active)
677 .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| {
678 let quick_launch = match e {
679 ClickEvent::Keyboard(_) => true,
680 ClickEvent::Mouse(e) => e.down.button == MouseButton::Left,
681 ClickEvent::Touch(_) => true,
682 };
683
684 window.focus(&editor.focus_handle(cx), cx);
685 editor.toggle_code_actions(
686 &ToggleCodeActions {
687 deployed_from: Some(CodeActionSource::RunMenu(row)),
688 quick_launch,
689 },
690 window,
691 cx,
692 );
693 }))
694 .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
695 editor.set_gutter_context_menu(
696 row,
697 active_breakpoint,
698 event.position(),
699 window,
700 cx,
701 );
702 }))
703 }
704
705 fn insert_runnables(
706 &mut self,
707 buffer: BufferId,
708 version: Global,
709 row: BufferRow,
710 new_tasks: RunnableTasks,
711 ) {
712 let (old_version, tasks) = self.runnables.runnables.entry(buffer).or_default();
713 if !old_version.changed_since(&version) {
714 *old_version = version;
715 tasks.insert(row, new_tasks);
716 }
717 }
718
719 fn runnable_rows(
720 project: Entity<Project>,
721 snapshot: MultiBufferSnapshot,
722 prefer_lsp: bool,
723 runnable_ranges: Vec<(Range<Anchor>, language::RunnableRange)>,
724 cx: AsyncWindowContext,
725 ) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> {
726 cx.spawn(async move |cx| {
727 let mut runnable_rows = Vec::with_capacity(runnable_ranges.len());
728 for (run_range, mut runnable) in runnable_ranges {
729 let Some(tasks) = cx
730 .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
731 .ok()
732 else {
733 continue;
734 };
735 let mut tasks = tasks.await;
736
737 if prefer_lsp {
738 tasks.retain(|(task_kind, _)| {
739 !matches!(task_kind, TaskSourceKind::Language { .. })
740 });
741 }
742 if tasks.is_empty() {
743 continue;
744 }
745
746 let point = run_range.start.to_point(&snapshot);
747 let Some(row) = snapshot
748 .buffer_line_for_row(MultiBufferRow(point.row))
749 .map(|(_, range)| range.start.row)
750 else {
751 continue;
752 };
753
754 let context_range =
755 BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
756 runnable_rows.push((
757 (runnable.buffer_id, row),
758 RunnableTasks {
759 templates: tasks,
760 offset: run_range.start,
761 context_range,
762 column: point.column,
763 extra_variables: runnable.extra_captures,
764 },
765 ));
766 }
767 runnable_rows
768 })
769 }
770
771 fn templates_with_tags(
772 project: &Entity<Project>,
773 runnable: &mut Runnable,
774 cx: &mut App,
775 ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
776 let (inventory, worktree_id, buffer) = project.read_with(cx, |project, cx| {
777 let buffer = project.buffer_for_id(runnable.buffer, cx);
778 let worktree_id = buffer
779 .as_ref()
780 .and_then(|buffer| buffer.read(cx).file())
781 .map(|file| file.worktree_id(cx));
782
783 (
784 project.task_store().read(cx).task_inventory().cloned(),
785 worktree_id,
786 buffer,
787 )
788 });
789
790 let tags = mem::take(&mut runnable.tags);
791 let language = runnable.language.clone();
792 cx.spawn(async move |cx| {
793 let mut templates_with_tags = Vec::new();
794 if let Some(inventory) = inventory {
795 for RunnableTag(tag) in tags {
796 let new_tasks = inventory.update(cx, |inventory, cx| {
797 inventory.list_tasks(
798 buffer.clone(),
799 Some(language.clone()),
800 worktree_id,
801 cx,
802 )
803 });
804 templates_with_tags.extend(new_tasks.await.into_iter().filter(
805 move |(_, template)| {
806 template.tags.iter().any(|source_tag| source_tag == &tag)
807 },
808 ));
809 }
810 }
811 templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned());
812
813 if let Some((leading_tag_source, _)) = templates_with_tags.first() {
814 // Strongest source wins; if we have worktree tag binding, prefer that to
815 // global and language bindings;
816 // if we have a global binding, prefer that to language binding.
817 let first_mismatch = templates_with_tags
818 .iter()
819 .position(|(tag_source, _)| tag_source != leading_tag_source);
820 if let Some(index) = first_mismatch {
821 templates_with_tags.truncate(index);
822 }
823 }
824
825 templates_with_tags
826 })
827 }
828
829 fn find_closest_task(
830 &mut self,
831 cx: &mut Context<Self>,
832 ) -> Option<(Entity<Buffer>, u32, Arc<RunnableTasks>)> {
833 let cursor_row = self
834 .selections
835 .newest_adjusted(&self.display_snapshot(cx))
836 .head()
837 .row;
838
839 let ((buffer_id, row), tasks) = self
840 .runnables
841 .runnables
842 .iter()
843 .flat_map(|(buffer_id, (_, tasks))| {
844 tasks.iter().map(|(row, tasks)| ((*buffer_id, *row), tasks))
845 })
846 .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?;
847
848 let buffer = self.buffer.read(cx).buffer(buffer_id)?;
849 let tasks = Arc::new(tasks.to_owned());
850 Some((buffer, row, tasks))
851 }
852}
853
854#[cfg(test)]
855mod tests {
856 use std::{sync::Arc, time::Duration};
857
858 use futures::StreamExt as _;
859 use gpui::{AppContext as _, Entity, Task, TestAppContext};
860 use indoc::indoc;
861 use language::{ContextProvider, FakeLspAdapter};
862 use languages::rust_lang;
863 use lsp::LanguageServerName;
864 use multi_buffer::{MultiBuffer, PathKey};
865 use project::{
866 FakeFs, Project, ProjectPath,
867 lsp_store::lsp_ext_command::{
868 CargoRunnableArgs, Runnable, RunnableArgs, ShellRunnableArgs,
869 },
870 };
871 use serde_json::json;
872 use task::{TaskTemplate, TaskTemplates};
873 use text::Point;
874 use util::path;
875 use util::rel_path::rel_path;
876
877 use crate::{
878 Editor, UPDATE_DEBOUNCE, editor_tests::init_test, scroll::scroll_amount::ScrollAmount,
879 test::build_editor_with_project,
880 };
881
882 const FAKE_LSP_NAME: &str = "the-fake-language-server";
883
884 struct TestRustContextProvider;
885
886 impl ContextProvider for TestRustContextProvider {
887 fn associated_tasks(
888 &self,
889 _: Option<Entity<language::Buffer>>,
890 _: &gpui::App,
891 ) -> Task<Option<TaskTemplates>> {
892 Task::ready(Some(TaskTemplates(vec![
893 TaskTemplate {
894 label: "Run main".into(),
895 command: "cargo".into(),
896 args: vec!["run".into()],
897 tags: vec!["rust-main".into()],
898 ..TaskTemplate::default()
899 },
900 TaskTemplate {
901 label: "Run test".into(),
902 command: "cargo".into(),
903 args: vec!["test".into()],
904 tags: vec!["rust-test".into()],
905 ..TaskTemplate::default()
906 },
907 ])))
908 }
909 }
910
911 struct TestRustContextProviderWithLsp;
912
913 impl ContextProvider for TestRustContextProviderWithLsp {
914 fn associated_tasks(
915 &self,
916 _: Option<Entity<language::Buffer>>,
917 _: &gpui::App,
918 ) -> Task<Option<TaskTemplates>> {
919 Task::ready(Some(TaskTemplates(vec![TaskTemplate {
920 label: "Run test".into(),
921 command: "cargo".into(),
922 args: vec!["test".into()],
923 tags: vec!["rust-test".into()],
924 ..TaskTemplate::default()
925 }])))
926 }
927
928 fn lsp_task_source(&self) -> Option<LanguageServerName> {
929 Some(LanguageServerName::new_static(FAKE_LSP_NAME))
930 }
931 }
932
933 fn rust_lang_with_task_context() -> Arc<language::Language> {
934 Arc::new(
935 Arc::try_unwrap(rust_lang())
936 .unwrap()
937 .with_context_provider(Some(Arc::new(TestRustContextProvider))),
938 )
939 }
940
941 fn rust_lang_with_lsp_task_context() -> Arc<language::Language> {
942 Arc::new(
943 Arc::try_unwrap(rust_lang())
944 .unwrap()
945 .with_context_provider(Some(Arc::new(TestRustContextProviderWithLsp))),
946 )
947 }
948
949 fn collect_runnable_labels(
950 editor: &Editor,
951 ) -> Vec<(text::BufferId, language::BufferRow, Vec<String>)> {
952 let mut result = editor
953 .runnables
954 .runnables
955 .iter()
956 .flat_map(|(buffer_id, (_, tasks))| {
957 tasks.iter().map(move |(row, runnable_tasks)| {
958 let mut labels: Vec<String> = runnable_tasks
959 .templates
960 .iter()
961 .map(|(_, template)| template.label.clone())
962 .collect();
963 labels.sort();
964 (*buffer_id, *row, labels)
965 })
966 })
967 .collect::<Vec<_>>();
968 result.sort_by_key(|(id, row, _)| (*id, *row));
969 result
970 }
971
972 #[gpui::test]
973 async fn test_multi_buffer_runnables_on_scroll(cx: &mut TestAppContext) {
974 init_test(cx, |_| {});
975
976 let padding_lines = 50;
977 let mut first_rs = String::from("fn main() {\n println!(\"hello\");\n}\n");
978 for _ in 0..padding_lines {
979 first_rs.push_str("//\n");
980 }
981 let test_one_row = 3 + padding_lines as u32 + 1;
982 first_rs.push_str("#[test]\nfn test_one() {\n assert!(true);\n}\n");
983
984 let fs = FakeFs::new(cx.executor());
985 fs.insert_tree(
986 path!("/project"),
987 json!({
988 "first.rs": first_rs,
989 "second.rs": indoc! {"
990 #[test]
991 fn test_two() {
992 assert!(true);
993 }
994
995 #[test]
996 fn test_three() {
997 assert!(true);
998 }
999 "},
1000 }),
1001 )
1002 .await;
1003
1004 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1005 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1006 language_registry.add(rust_lang_with_task_context());
1007
1008 let buffer_1 = project
1009 .update(cx, |project, cx| {
1010 project.open_local_buffer(path!("/project/first.rs"), cx)
1011 })
1012 .await
1013 .unwrap();
1014 let buffer_2 = project
1015 .update(cx, |project, cx| {
1016 project.open_local_buffer(path!("/project/second.rs"), cx)
1017 })
1018 .await
1019 .unwrap();
1020
1021 let buffer_1_id = buffer_1.read_with(cx, |buffer, _| buffer.remote_id());
1022 let buffer_2_id = buffer_2.read_with(cx, |buffer, _| buffer.remote_id());
1023
1024 let multi_buffer = cx.new(|cx| {
1025 let mut multi_buffer = MultiBuffer::new(language::Capability::ReadWrite);
1026 let end = buffer_1.read(cx).max_point();
1027 multi_buffer.set_excerpts_for_path(
1028 PathKey::sorted(0),
1029 buffer_1.clone(),
1030 [Point::new(0, 0)..end],
1031 0,
1032 cx,
1033 );
1034 multi_buffer.set_excerpts_for_path(
1035 PathKey::sorted(1),
1036 buffer_2.clone(),
1037 [Point::new(0, 0)..Point::new(8, 1)],
1038 0,
1039 cx,
1040 );
1041 multi_buffer
1042 });
1043
1044 let editor = cx.add_window(|window, cx| {
1045 Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
1046 });
1047 cx.executor().advance_clock(Duration::from_millis(500));
1048 cx.executor().run_until_parked();
1049
1050 // Clear stale data from startup events, then refresh.
1051 // first.rs is long enough that second.rs is below the ~47-line viewport.
1052 editor
1053 .update(cx, |editor, window, cx| {
1054 editor.clear_runnables(None);
1055 editor.refresh_runnables(None, window, cx);
1056 })
1057 .unwrap();
1058 cx.executor().advance_clock(UPDATE_DEBOUNCE);
1059 cx.executor().run_until_parked();
1060 assert_eq!(
1061 editor
1062 .update(cx, |editor, _, _| collect_runnable_labels(editor))
1063 .unwrap(),
1064 vec![(buffer_1_id, 0, vec!["Run main".to_string()])],
1065 "Only fn main from first.rs should be visible before scrolling"
1066 );
1067
1068 // Scroll down to bring second.rs excerpts into view.
1069 editor
1070 .update(cx, |editor, window, cx| {
1071 editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
1072 })
1073 .unwrap();
1074 cx.executor().advance_clock(Duration::from_millis(200));
1075 cx.executor().run_until_parked();
1076
1077 let after_scroll = editor
1078 .update(cx, |editor, _, _| collect_runnable_labels(editor))
1079 .unwrap();
1080 assert_eq!(
1081 after_scroll,
1082 vec![
1083 (buffer_1_id, 0, vec!["Run main".to_string()]),
1084 (buffer_1_id, test_one_row, vec!["Run test".to_string()]),
1085 (buffer_2_id, 1, vec!["Run test".to_string()]),
1086 (buffer_2_id, 6, vec!["Run test".to_string()]),
1087 ],
1088 "Tree-sitter should detect both #[test] fns in second.rs after scroll"
1089 );
1090
1091 // Edit second.rs to invalidate its cache; first.rs data should persist.
1092 buffer_2.update(cx, |buffer, cx| {
1093 buffer.edit([(0..0, "// added comment\n")], None, cx);
1094 });
1095 editor
1096 .update(cx, |editor, window, cx| {
1097 editor.scroll_screen(&ScrollAmount::Page(-1.0), window, cx);
1098 })
1099 .unwrap();
1100 cx.executor().advance_clock(Duration::from_millis(200));
1101 cx.executor().run_until_parked();
1102
1103 assert_eq!(
1104 editor
1105 .update(cx, |editor, _, _| collect_runnable_labels(editor))
1106 .unwrap(),
1107 vec![
1108 (buffer_1_id, 0, vec!["Run main".to_string()]),
1109 (buffer_1_id, test_one_row, vec!["Run test".to_string()]),
1110 ],
1111 "first.rs runnables should survive an edit to second.rs"
1112 );
1113 }
1114
1115 #[gpui::test]
1116 async fn test_lsp_runnables_removed_after_edit(cx: &mut TestAppContext) {
1117 init_test(cx, |_| {});
1118
1119 let fs = FakeFs::new(cx.executor());
1120 fs.insert_tree(
1121 path!("/project"),
1122 json!({
1123 "main.rs": indoc! {"
1124 #[test]
1125 fn test_one() {
1126 assert!(true);
1127 }
1128
1129 fn helper() {}
1130 "},
1131 }),
1132 )
1133 .await;
1134
1135 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1136 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1137 language_registry.add(rust_lang_with_lsp_task_context());
1138
1139 let mut fake_servers = language_registry.register_fake_lsp(
1140 "Rust",
1141 FakeLspAdapter {
1142 name: FAKE_LSP_NAME,
1143 ..FakeLspAdapter::default()
1144 },
1145 );
1146
1147 let buffer = project
1148 .update(cx, |project, cx| {
1149 project.open_local_buffer(path!("/project/main.rs"), cx)
1150 })
1151 .await
1152 .unwrap();
1153
1154 let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
1155
1156 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
1157 let editor = cx.add_window(|window, cx| {
1158 build_editor_with_project(project.clone(), multi_buffer, window, cx)
1159 });
1160
1161 let fake_server = fake_servers.next().await.expect("fake LSP server");
1162
1163 use project::lsp_store::lsp_ext_command::Runnables;
1164 fake_server.set_request_handler::<Runnables, _, _>(move |params, _| async move {
1165 let text = params.text_document.uri.path().to_string();
1166 if text.contains("main.rs") {
1167 let uri = lsp::Uri::from_file_path(path!("/project/main.rs")).expect("valid uri");
1168 Ok(vec![Runnable {
1169 label: "LSP test_one".into(),
1170 location: Some(lsp::LocationLink {
1171 origin_selection_range: None,
1172 target_uri: uri,
1173 target_range: lsp::Range::new(
1174 lsp::Position::new(0, 0),
1175 lsp::Position::new(3, 1),
1176 ),
1177 target_selection_range: lsp::Range::new(
1178 lsp::Position::new(0, 0),
1179 lsp::Position::new(3, 1),
1180 ),
1181 }),
1182 args: RunnableArgs::Cargo(CargoRunnableArgs {
1183 environment: Default::default(),
1184 cwd: path!("/project").into(),
1185 override_cargo: None,
1186 workspace_root: None,
1187 cargo_args: vec!["test".into(), "test_one".into()],
1188 executable_args: Vec::new(),
1189 }),
1190 }])
1191 } else {
1192 Ok(Vec::new())
1193 }
1194 });
1195
1196 // Trigger a refresh to pick up both tree-sitter and LSP runnables.
1197 editor
1198 .update(cx, |editor, window, cx| {
1199 editor.refresh_runnables(None, window, cx);
1200 })
1201 .expect("editor update");
1202 cx.executor().advance_clock(UPDATE_DEBOUNCE);
1203 cx.executor().run_until_parked();
1204
1205 let labels = editor
1206 .update(cx, |editor, _, _| collect_runnable_labels(editor))
1207 .expect("editor update");
1208 assert_eq!(
1209 labels,
1210 vec![(buffer_id, 0, vec!["LSP test_one".to_string()]),],
1211 "LSP runnables should appear for #[test] fn"
1212 );
1213
1214 // Remove `#[test]` attribute so the function is no longer a test.
1215 buffer.update(cx, |buffer, cx| {
1216 let test_attr_end = buffer.text().find("\nfn test_one").expect("find fn");
1217 buffer.edit([(0..test_attr_end, "")], None, cx);
1218 });
1219
1220 // Also update the LSP handler to return no runnables.
1221 fake_server
1222 .set_request_handler::<Runnables, _, _>(move |_, _| async move { Ok(Vec::new()) });
1223
1224 cx.executor().advance_clock(UPDATE_DEBOUNCE);
1225 cx.executor().run_until_parked();
1226
1227 let labels = editor
1228 .update(cx, |editor, _, _| collect_runnable_labels(editor))
1229 .expect("editor update");
1230 assert_eq!(
1231 labels,
1232 Vec::<(text::BufferId, language::BufferRow, Vec<String>)>::new(),
1233 "Runnables should be removed after #[test] is deleted and LSP returns empty"
1234 );
1235 }
1236
1237 #[gpui::test]
1238 async fn test_no_runnables_for_unsaved_buffer(cx: &mut TestAppContext) {
1239 init_test(cx, |_| {});
1240
1241 let fs = FakeFs::new(cx.executor());
1242 fs.insert_tree(path!("/project"), json!({})).await;
1243
1244 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1245 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1246 language_registry.add(rust_lang_with_task_context());
1247
1248 let rust_language = language_registry.language_for_name("Rust").await.unwrap();
1249 let buffer = cx.new(|cx| {
1250 let mut buffer = language::Buffer::local(
1251 indoc! {"
1252 fn main() {
1253 println!(\"hello\");
1254 }
1255
1256 #[test]
1257 fn test_one() {
1258 assert!(true);
1259 }
1260 "},
1261 cx,
1262 );
1263 buffer.set_language(Some(rust_language), cx);
1264 buffer
1265 });
1266
1267 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
1268 let editor = cx.add_window(|window, cx| {
1269 build_editor_with_project(project.clone(), multi_buffer, window, cx)
1270 });
1271
1272 editor
1273 .update(cx, |editor, window, cx| {
1274 editor.refresh_runnables(None, window, cx);
1275 })
1276 .expect("editor update");
1277 cx.executor().advance_clock(UPDATE_DEBOUNCE);
1278 cx.executor().run_until_parked();
1279
1280 let labels = editor
1281 .update(cx, |editor, _, _| collect_runnable_labels(editor))
1282 .expect("editor update");
1283 assert_eq!(
1284 labels,
1285 Vec::<(text::BufferId, language::BufferRow, Vec<String>)>::new(),
1286 "No runnables should appear for an unsaved buffer without a file on disk"
1287 );
1288
1289 let worktree_id = project.update(cx, |project, cx| {
1290 project
1291 .worktrees(cx)
1292 .next()
1293 .expect("worktree")
1294 .read(cx)
1295 .id()
1296 });
1297 project
1298 .update(cx, |project, cx| {
1299 project.save_buffer_as(
1300 buffer.clone(),
1301 ProjectPath {
1302 worktree_id,
1303 path: rel_path("main.rs").into(),
1304 },
1305 cx,
1306 )
1307 })
1308 .await
1309 .expect("save buffer as");
1310
1311 editor
1312 .update(cx, |editor, window, cx| {
1313 editor.refresh_runnables(None, window, cx);
1314 })
1315 .expect("editor update");
1316 cx.executor().advance_clock(UPDATE_DEBOUNCE);
1317 cx.executor().run_until_parked();
1318
1319 let labels = editor
1320 .update(cx, |editor, _, _| collect_runnable_labels(editor))
1321 .expect("editor update");
1322 assert!(
1323 !labels.is_empty(),
1324 "Runnables should appear after the buffer is saved to disk"
1325 );
1326 }
1327
1328 // Verifies that a shell runnable from rust-analyzer produces
1329 // a task template that uses the shell program and args.
1330 #[gpui::test]
1331 async fn test_shell_runnable_produces_correct_task_template(cx: &mut TestAppContext) {
1332 init_test(cx, |_| {});
1333
1334 let fs = FakeFs::new(cx.executor());
1335 fs.insert_tree(
1336 path!("/project"),
1337 json!({
1338 "main.rs": indoc! {"
1339 #[test]
1340 fn test_one() {
1341 assert!(true);
1342 }
1343 "},
1344 }),
1345 )
1346 .await;
1347
1348 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1349 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1350 language_registry.add(rust_lang_with_lsp_task_context());
1351
1352 let mut fake_servers = language_registry.register_fake_lsp(
1353 "Rust",
1354 FakeLspAdapter {
1355 name: FAKE_LSP_NAME,
1356 ..FakeLspAdapter::default()
1357 },
1358 );
1359
1360 let buffer = project
1361 .update(cx, |project, cx| {
1362 project.open_local_buffer(path!("/project/main.rs"), cx)
1363 })
1364 .await
1365 .unwrap();
1366
1367 let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
1368
1369 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
1370 let editor = cx.add_window(|window, cx| {
1371 build_editor_with_project(project.clone(), multi_buffer, window, cx)
1372 });
1373
1374 let fake_server = fake_servers.next().await.expect("fake LSP server");
1375
1376 use project::lsp_store::lsp_ext_command::Runnables;
1377 fake_server.set_request_handler::<Runnables, _, _>(move |params, _| async move {
1378 let text = params.text_document.uri.path().to_string();
1379 if text.contains("main.rs") {
1380 let uri = lsp::Uri::from_file_path(path!("/project/main.rs")).expect("valid uri");
1381 Ok(vec![Runnable {
1382 label: "nextest test_one".into(),
1383 location: Some(lsp::LocationLink {
1384 origin_selection_range: None,
1385 target_uri: uri,
1386 target_range: lsp::Range::new(
1387 lsp::Position::new(0, 0),
1388 lsp::Position::new(3, 1),
1389 ),
1390 target_selection_range: lsp::Range::new(
1391 lsp::Position::new(0, 0),
1392 lsp::Position::new(3, 1),
1393 ),
1394 }),
1395 args: RunnableArgs::Shell(ShellRunnableArgs {
1396 environment: Default::default(),
1397 cwd: path!("/project").into(),
1398 program: "cargo".into(),
1399 args: vec![
1400 "nextest".into(),
1401 "run".into(),
1402 "--package".into(),
1403 "my-crate".into(),
1404 "--lib".into(),
1405 "--".into(),
1406 "test_one".into(),
1407 "--exact".into(),
1408 ],
1409 }),
1410 }])
1411 } else {
1412 Ok(Vec::new())
1413 }
1414 });
1415
1416 editor
1417 .update(cx, |editor, window, cx| {
1418 editor.refresh_runnables(None, window, cx);
1419 })
1420 .expect("editor update");
1421 cx.executor().advance_clock(UPDATE_DEBOUNCE);
1422 cx.executor().run_until_parked();
1423
1424 let labels = editor
1425 .update(cx, |editor, _, _| collect_runnable_labels(editor))
1426 .expect("editor update");
1427 assert_eq!(
1428 labels,
1429 vec![(buffer_id, 0, vec!["nextest test_one".to_string()])],
1430 "shell runnable should appear for #[test] fn"
1431 );
1432
1433 let templates = editor
1434 .update(cx, |editor, _, _| {
1435 editor
1436 .runnables
1437 .runnables
1438 .iter()
1439 .flat_map(|(_, (_, tasks))| {
1440 tasks.values().flat_map(|runnable_tasks| {
1441 runnable_tasks
1442 .templates
1443 .iter()
1444 .map(|(_, template)| {
1445 (
1446 template.label.clone(),
1447 template.command.clone(),
1448 template.args.clone(),
1449 )
1450 })
1451 .collect::<Vec<_>>()
1452 })
1453 })
1454 .collect::<Vec<_>>()
1455 })
1456 .expect("editor update");
1457
1458 let (label, command, args) = templates
1459 .iter()
1460 .find(|(label, _, _)| label == "nextest test_one")
1461 .expect("shell runnable task template should exist");
1462 assert_eq!(label, "nextest test_one");
1463 assert_eq!(command, "cargo");
1464 assert_eq!(
1465 args,
1466 &[
1467 "nextest",
1468 "run",
1469 "--package",
1470 "my-crate",
1471 "--lib",
1472 "--",
1473 "test_one",
1474 "--exact",
1475 ],
1476 "shell runnable should preserve program args"
1477 );
1478 }
1479}
1480