Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:33:34.439Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

runnables.rs

1480 lines · 53.9 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.