runnables.rs

  1use std::{collections::BTreeMap, mem, ops::Range, sync::Arc};
  2
  3use clock::Global;
  4use collections::HashMap;
  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::{
 12    Anchor, BufferOffset, MultiBufferOffset, MultiBufferRow, MultiBufferSnapshot, ToPoint as _,
 13};
 14use project::{
 15    Location, Project, TaskSourceKind,
 16    debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
 17    project_settings::ProjectSettings,
 18};
 19use settings::Settings as _;
 20use smallvec::SmallVec;
 21use task::{ResolvedTask, RunnableTag, TaskContext, TaskTemplate, TaskVariables, VariableName};
 22use text::{BufferId, OffsetRangeExt as _, ToOffset as _, ToPoint as _};
 23use ui::{Clickable as _, Color, IconButton, IconSize, Toggleable as _};
 24
 25use crate::{
 26    CodeActionSource, Editor, EditorSettings, EditorStyle, RangeToAnchorExt, SpawnNearestTask,
 27    ToggleCodeActions, UPDATE_DEBOUNCE, display_map::DisplayRow,
 28};
 29
 30#[derive(Debug)]
 31pub(super) struct RunnableData {
 32    runnables: HashMap<BufferId, (Global, BTreeMap<BufferRow, RunnableTasks>)>,
 33    runnables_update_task: Task<()>,
 34}
 35
 36impl RunnableData {
 37    pub fn new() -> Self {
 38        Self {
 39            runnables: HashMap::default(),
 40            runnables_update_task: Task::ready(()),
 41        }
 42    }
 43
 44    pub fn runnables(
 45        &self,
 46        (buffer_id, buffer_row): (BufferId, BufferRow),
 47    ) -> Option<&RunnableTasks> {
 48        self.runnables.get(&buffer_id)?.1.get(&buffer_row)
 49    }
 50
 51    pub fn all_runnables(&self) -> impl Iterator<Item = &RunnableTasks> {
 52        self.runnables
 53            .values()
 54            .flat_map(|(_, tasks)| tasks.values())
 55    }
 56
 57    pub fn has_cached(&self, buffer_id: BufferId, version: &Global) -> bool {
 58        self.runnables
 59            .get(&buffer_id)
 60            .is_some_and(|(cached_version, _)| !version.changed_since(cached_version))
 61    }
 62
 63    #[cfg(test)]
 64    pub fn insert(
 65        &mut self,
 66        buffer_id: BufferId,
 67        buffer_row: BufferRow,
 68        version: Global,
 69        tasks: RunnableTasks,
 70    ) {
 71        self.runnables
 72            .entry(buffer_id)
 73            .or_insert_with(|| (version, BTreeMap::default()))
 74            .1
 75            .insert(buffer_row, tasks);
 76    }
 77}
 78
 79#[derive(Clone, Debug)]
 80pub struct RunnableTasks {
 81    pub templates: Vec<(TaskSourceKind, TaskTemplate)>,
 82    pub offset: multi_buffer::Anchor,
 83    // We need the column at which the task context evaluation should take place (when we're spawning it via gutter).
 84    pub column: u32,
 85    // Values of all named captures, including those starting with '_'
 86    pub extra_variables: HashMap<String, String>,
 87    // Full range of the tagged region. We use it to determine which `extra_variables` to grab for context resolution in e.g. a modal.
 88    pub context_range: Range<BufferOffset>,
 89}
 90
 91impl RunnableTasks {
 92    pub fn resolve<'a>(
 93        &'a self,
 94        cx: &'a task::TaskContext,
 95    ) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a {
 96        self.templates.iter().filter_map(|(kind, template)| {
 97            template
 98                .resolve_task(&kind.to_id_base(), cx)
 99                .map(|task| (kind.clone(), task))
100        })
101    }
102}
103
104#[derive(Clone)]
105pub struct ResolvedTasks {
106    pub templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
107    pub position: Anchor,
108}
109
110impl Editor {
111    pub fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context<Self>) {
112        if !self.mode().is_full()
113            || !EditorSettings::get_global(cx).gutter.runnables
114            || !self.enable_runnables
115        {
116            self.clear_runnables(None);
117            return;
118        }
119        if let Some(buffer) = self.buffer().read(cx).as_singleton() {
120            if self
121                .runnables
122                .has_cached(buffer.read(cx).remote_id(), &buffer.read(cx).version())
123            {
124                return;
125            }
126        }
127
128        let project = self.project().map(Entity::downgrade);
129        let lsp_task_sources = self.lsp_task_sources(true, true, cx);
130        let multi_buffer = self.buffer.downgrade();
131        self.runnables.runnables_update_task = cx.spawn_in(window, async move |editor, cx| {
132            cx.background_executor().timer(UPDATE_DEBOUNCE).await;
133            let Some(project) = project.and_then(|p| p.upgrade()) else {
134                return;
135            };
136
137            let hide_runnables = project.update(cx, |project, _| project.is_via_collab());
138            if hide_runnables {
139                return;
140            }
141            let lsp_tasks = if lsp_task_sources.is_empty() {
142                Vec::new()
143            } else {
144                let Ok(lsp_tasks) = cx
145                    .update(|_, cx| crate::lsp_tasks(project.clone(), &lsp_task_sources, None, cx))
146                else {
147                    return;
148                };
149                lsp_tasks.await
150            };
151            let new_rows = {
152                let Some((multi_buffer_snapshot, multi_buffer_query_range)) = editor
153                    .update(cx, |editor, cx| {
154                        let multi_buffer = editor.buffer().read(cx);
155                        if multi_buffer.is_singleton() {
156                            Some((multi_buffer.snapshot(cx), Anchor::min()..Anchor::max()))
157                        } else {
158                            let display_snapshot =
159                                editor.display_map.update(cx, |map, cx| map.snapshot(cx));
160                            let multi_buffer_query_range =
161                                editor.multi_buffer_visible_range(&display_snapshot, cx);
162                            let multi_buffer_snapshot = display_snapshot.buffer();
163                            Some((
164                                multi_buffer_snapshot.clone(),
165                                multi_buffer_query_range.to_anchors(&multi_buffer_snapshot),
166                            ))
167                        }
168                    })
169                    .ok()
170                    .flatten()
171                else {
172                    return;
173                };
174                cx.background_spawn({
175                    async move {
176                        multi_buffer_snapshot
177                            .runnable_ranges(multi_buffer_query_range)
178                            .collect()
179                    }
180                })
181                .await
182            };
183
184            let Ok(multi_buffer_snapshot) =
185                editor.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
186            else {
187                return;
188            };
189            let Ok(mut lsp_tasks_by_rows) = cx.update(|_, cx| {
190                lsp_tasks
191                    .into_iter()
192                    .flat_map(|(kind, tasks)| {
193                        tasks.into_iter().filter_map(move |(location, task)| {
194                            Some((kind.clone(), location?, task))
195                        })
196                    })
197                    .fold(HashMap::default(), |mut acc, (kind, location, task)| {
198                        let buffer = location.target.buffer;
199                        let buffer_snapshot = buffer.read(cx).snapshot();
200                        let offset = multi_buffer_snapshot.excerpts().find_map(
201                            |(excerpt_id, snapshot, _)| {
202                                if snapshot.remote_id() == buffer_snapshot.remote_id() {
203                                    multi_buffer_snapshot
204                                        .anchor_in_excerpt(excerpt_id, location.target.range.start)
205                                } else {
206                                    None
207                                }
208                            },
209                        );
210                        if let Some(offset) = offset {
211                            let task_buffer_range =
212                                location.target.range.to_point(&buffer_snapshot);
213                            let context_buffer_range =
214                                task_buffer_range.to_offset(&buffer_snapshot);
215                            let context_range = BufferOffset(context_buffer_range.start)
216                                ..BufferOffset(context_buffer_range.end);
217
218                            acc.entry((buffer_snapshot.remote_id(), task_buffer_range.start.row))
219                                .or_insert_with(|| RunnableTasks {
220                                    templates: Vec::new(),
221                                    offset,
222                                    column: task_buffer_range.start.column,
223                                    extra_variables: HashMap::default(),
224                                    context_range,
225                                })
226                                .templates
227                                .push((kind, task.original_task().clone()));
228                        }
229
230                        acc
231                    })
232            }) else {
233                return;
234            };
235
236            let Ok(prefer_lsp) = multi_buffer.update(cx, |buffer, cx| {
237                buffer.language_settings(cx).tasks.prefer_lsp
238            }) else {
239                return;
240            };
241
242            let rows = Self::runnable_rows(
243                project,
244                multi_buffer_snapshot,
245                prefer_lsp && !lsp_tasks_by_rows.is_empty(),
246                new_rows,
247                cx.clone(),
248            )
249            .await;
250            editor
251                .update(cx, |editor, cx| {
252                    for ((buffer_id, row), mut new_tasks) in rows {
253                        let Some(buffer) = editor.buffer().read(cx).buffer(buffer_id) else {
254                            continue;
255                        };
256
257                        if let Some(lsp_tasks) = lsp_tasks_by_rows.remove(&(buffer_id, row)) {
258                            new_tasks.templates.extend(lsp_tasks.templates);
259                        }
260                        editor.insert_runnables(
261                            buffer_id,
262                            buffer.read(cx).version(),
263                            row,
264                            new_tasks,
265                        );
266                    }
267                    for ((buffer_id, row), new_tasks) in lsp_tasks_by_rows {
268                        let Some(buffer) = editor.buffer().read(cx).buffer(buffer_id) else {
269                            continue;
270                        };
271                        editor.insert_runnables(
272                            buffer_id,
273                            buffer.read(cx).version(),
274                            row,
275                            new_tasks,
276                        );
277                    }
278                })
279                .ok();
280        });
281    }
282
283    pub fn spawn_nearest_task(
284        &mut self,
285        action: &SpawnNearestTask,
286        window: &mut Window,
287        cx: &mut Context<Self>,
288    ) {
289        let Some((workspace, _)) = self.workspace.clone() else {
290            return;
291        };
292        let Some(project) = self.project.clone() else {
293            return;
294        };
295
296        // Try to find a closest, enclosing node using tree-sitter that has a task
297        let Some((buffer, buffer_row, tasks)) = self
298            .find_enclosing_node_task(cx)
299            // Or find the task that's closest in row-distance.
300            .or_else(|| self.find_closest_task(cx))
301        else {
302            return;
303        };
304
305        let reveal_strategy = action.reveal;
306        let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
307        cx.spawn_in(window, async move |_, cx| {
308            let context = task_context.await?;
309            let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?;
310
311            let resolved = &mut resolved_task.resolved;
312            resolved.reveal = reveal_strategy;
313
314            workspace
315                .update_in(cx, |workspace, window, cx| {
316                    workspace.schedule_resolved_task(
317                        task_source_kind,
318                        resolved_task,
319                        false,
320                        window,
321                        cx,
322                    );
323                })
324                .ok()
325        })
326        .detach();
327    }
328
329    pub fn clear_runnables(&mut self, for_buffer: Option<BufferId>) {
330        if let Some(buffer_id) = for_buffer {
331            self.runnables.runnables.remove(&buffer_id);
332        } else {
333            self.runnables.runnables.clear();
334        }
335        self.runnables.runnables_update_task = Task::ready(());
336    }
337
338    pub fn task_context(&self, window: &mut Window, cx: &mut App) -> Task<Option<TaskContext>> {
339        let Some(project) = self.project.clone() else {
340            return Task::ready(None);
341        };
342        let (selection, buffer, editor_snapshot) = {
343            let selection = self.selections.newest_adjusted(&self.display_snapshot(cx));
344            let Some((buffer, _)) = self
345                .buffer()
346                .read(cx)
347                .point_to_buffer_offset(selection.start, cx)
348            else {
349                return Task::ready(None);
350            };
351            let snapshot = self.snapshot(window, cx);
352            (selection, buffer, snapshot)
353        };
354        let selection_range = selection.range();
355        let start = editor_snapshot
356            .display_snapshot
357            .buffer_snapshot()
358            .anchor_after(selection_range.start)
359            .text_anchor;
360        let end = editor_snapshot
361            .display_snapshot
362            .buffer_snapshot()
363            .anchor_after(selection_range.end)
364            .text_anchor;
365        let location = Location {
366            buffer,
367            range: start..end,
368        };
369        let captured_variables = {
370            let mut variables = TaskVariables::default();
371            let buffer = location.buffer.read(cx);
372            let buffer_id = buffer.remote_id();
373            let snapshot = buffer.snapshot();
374            let starting_point = location.range.start.to_point(&snapshot);
375            let starting_offset = starting_point.to_offset(&snapshot);
376            for (_, tasks) in self
377                .runnables
378                .runnables
379                .get(&buffer_id)
380                .into_iter()
381                .flat_map(|(_, tasks)| tasks.range(0..starting_point.row + 1))
382            {
383                if !tasks
384                    .context_range
385                    .contains(&crate::BufferOffset(starting_offset))
386                {
387                    continue;
388                }
389                for (capture_name, value) in tasks.extra_variables.iter() {
390                    variables.insert(
391                        VariableName::Custom(capture_name.to_owned().into()),
392                        value.clone(),
393                    );
394                }
395            }
396            variables
397        };
398
399        project.update(cx, |project, cx| {
400            project.task_store().update(cx, |task_store, cx| {
401                task_store.task_context_for_location(captured_variables, location, cx)
402            })
403        })
404    }
405
406    pub fn lsp_task_sources(
407        &self,
408        visible_only: bool,
409        skip_cached: bool,
410        cx: &mut Context<Self>,
411    ) -> HashMap<LanguageServerName, Vec<BufferId>> {
412        if !self.lsp_data_enabled() {
413            return HashMap::default();
414        }
415        let buffers = if visible_only {
416            self.visible_excerpts(true, cx)
417                .into_values()
418                .map(|(buffer, _, _)| buffer)
419                .collect()
420        } else {
421            self.buffer().read(cx).all_buffers()
422        };
423
424        let lsp_settings = &ProjectSettings::get_global(cx).lsp;
425
426        buffers
427            .into_iter()
428            .filter_map(|buffer| {
429                let lsp_tasks_source = buffer
430                    .read(cx)
431                    .language()?
432                    .context_provider()?
433                    .lsp_task_source()?;
434                if lsp_settings
435                    .get(&lsp_tasks_source)
436                    .is_none_or(|s| s.enable_lsp_tasks)
437                {
438                    let buffer_id = buffer.read(cx).remote_id();
439                    if skip_cached
440                        && self
441                            .runnables
442                            .has_cached(buffer_id, &buffer.read(cx).version())
443                    {
444                        None
445                    } else {
446                        Some((lsp_tasks_source, buffer_id))
447                    }
448                } else {
449                    None
450                }
451            })
452            .fold(
453                HashMap::default(),
454                |mut acc, (lsp_task_source, buffer_id)| {
455                    acc.entry(lsp_task_source)
456                        .or_insert_with(Vec::new)
457                        .push(buffer_id);
458                    acc
459                },
460            )
461    }
462
463    pub fn find_enclosing_node_task(
464        &mut self,
465        cx: &mut Context<Self>,
466    ) -> Option<(Entity<Buffer>, u32, Arc<RunnableTasks>)> {
467        let snapshot = self.buffer.read(cx).snapshot(cx);
468        let offset = self
469            .selections
470            .newest::<MultiBufferOffset>(&self.display_snapshot(cx))
471            .head();
472        let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
473        let offset = excerpt.map_offset_to_buffer(offset);
474        let buffer_id = excerpt.buffer().remote_id();
475
476        let layer = excerpt.buffer().syntax_layer_at(offset)?;
477        let mut cursor = layer.node().walk();
478
479        while cursor.goto_first_child_for_byte(offset.0).is_some() {
480            if cursor.node().end_byte() == offset.0 {
481                cursor.goto_next_sibling();
482            }
483        }
484
485        // Ascend to the smallest ancestor that contains the range and has a task.
486        loop {
487            let node = cursor.node();
488            let node_range = node.byte_range();
489            let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row;
490
491            // Check if this node contains our offset
492            if node_range.start <= offset.0 && node_range.end >= offset.0 {
493                // If it contains offset, check for task
494                if let Some(tasks) = self
495                    .runnables
496                    .runnables
497                    .get(&buffer_id)
498                    .and_then(|(_, tasks)| tasks.get(&symbol_start_row))
499                {
500                    let buffer = self.buffer.read(cx).buffer(buffer_id)?;
501                    return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned())));
502                }
503            }
504
505            if !cursor.goto_parent() {
506                break;
507            }
508        }
509        None
510    }
511
512    pub fn render_run_indicator(
513        &self,
514        _style: &EditorStyle,
515        is_active: bool,
516        row: DisplayRow,
517        breakpoint: Option<(Anchor, Breakpoint, Option<BreakpointSessionState>)>,
518        cx: &mut Context<Self>,
519    ) -> IconButton {
520        let color = Color::Muted;
521        let position = breakpoint.as_ref().map(|(anchor, _, _)| *anchor);
522
523        IconButton::new(
524            ("run_indicator", row.0 as usize),
525            ui::IconName::PlayOutlined,
526        )
527        .shape(ui::IconButtonShape::Square)
528        .icon_size(IconSize::XSmall)
529        .icon_color(color)
530        .toggle_state(is_active)
531        .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| {
532            let quick_launch = match e {
533                ClickEvent::Keyboard(_) => true,
534                ClickEvent::Mouse(e) => e.down.button == MouseButton::Left,
535            };
536
537            window.focus(&editor.focus_handle(cx), cx);
538            editor.toggle_code_actions(
539                &ToggleCodeActions {
540                    deployed_from: Some(CodeActionSource::RunMenu(row)),
541                    quick_launch,
542                },
543                window,
544                cx,
545            );
546        }))
547        .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
548            editor.set_breakpoint_context_menu(row, position, event.position(), window, cx);
549        }))
550    }
551
552    fn insert_runnables(
553        &mut self,
554        buffer: BufferId,
555        version: Global,
556        row: BufferRow,
557        new_tasks: RunnableTasks,
558    ) {
559        let (old_version, tasks) = self.runnables.runnables.entry(buffer).or_default();
560        if !old_version.changed_since(&version) {
561            *old_version = version;
562            tasks.insert(row, new_tasks);
563        }
564    }
565
566    fn runnable_rows(
567        project: Entity<Project>,
568        snapshot: MultiBufferSnapshot,
569        prefer_lsp: bool,
570        runnable_ranges: Vec<(Range<MultiBufferOffset>, language::RunnableRange)>,
571        cx: AsyncWindowContext,
572    ) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> {
573        cx.spawn(async move |cx| {
574            let mut runnable_rows = Vec::with_capacity(runnable_ranges.len());
575            for (run_range, mut runnable) in runnable_ranges {
576                let Some(tasks) = cx
577                    .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
578                    .ok()
579                else {
580                    continue;
581                };
582                let mut tasks = tasks.await;
583
584                if prefer_lsp {
585                    tasks.retain(|(task_kind, _)| {
586                        !matches!(task_kind, TaskSourceKind::Language { .. })
587                    });
588                }
589                if tasks.is_empty() {
590                    continue;
591                }
592
593                let point = run_range.start.to_point(&snapshot);
594                let Some(row) = snapshot
595                    .buffer_line_for_row(MultiBufferRow(point.row))
596                    .map(|(_, range)| range.start.row)
597                else {
598                    continue;
599                };
600
601                let context_range =
602                    BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
603                runnable_rows.push((
604                    (runnable.buffer_id, row),
605                    RunnableTasks {
606                        templates: tasks,
607                        offset: snapshot.anchor_before(run_range.start),
608                        context_range,
609                        column: point.column,
610                        extra_variables: runnable.extra_captures,
611                    },
612                ));
613            }
614            runnable_rows
615        })
616    }
617
618    fn templates_with_tags(
619        project: &Entity<Project>,
620        runnable: &mut Runnable,
621        cx: &mut App,
622    ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
623        let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
624            let (worktree_id, file) = project
625                .buffer_for_id(runnable.buffer, cx)
626                .and_then(|buffer| buffer.read(cx).file())
627                .map(|file| (file.worktree_id(cx), file.clone()))
628                .unzip();
629
630            (
631                project.task_store().read(cx).task_inventory().cloned(),
632                worktree_id,
633                file,
634            )
635        });
636
637        let tags = mem::take(&mut runnable.tags);
638        let language = runnable.language.clone();
639        cx.spawn(async move |cx| {
640            let mut templates_with_tags = Vec::new();
641            if let Some(inventory) = inventory {
642                for RunnableTag(tag) in tags {
643                    let new_tasks = inventory.update(cx, |inventory, cx| {
644                        inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx)
645                    });
646                    templates_with_tags.extend(new_tasks.await.into_iter().filter(
647                        move |(_, template)| {
648                            template.tags.iter().any(|source_tag| source_tag == &tag)
649                        },
650                    ));
651                }
652            }
653            templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned());
654
655            if let Some((leading_tag_source, _)) = templates_with_tags.first() {
656                // Strongest source wins; if we have worktree tag binding, prefer that to
657                // global and language bindings;
658                // if we have a global binding, prefer that to language binding.
659                let first_mismatch = templates_with_tags
660                    .iter()
661                    .position(|(tag_source, _)| tag_source != leading_tag_source);
662                if let Some(index) = first_mismatch {
663                    templates_with_tags.truncate(index);
664                }
665            }
666
667            templates_with_tags
668        })
669    }
670
671    fn find_closest_task(
672        &mut self,
673        cx: &mut Context<Self>,
674    ) -> Option<(Entity<Buffer>, u32, Arc<RunnableTasks>)> {
675        let cursor_row = self
676            .selections
677            .newest_adjusted(&self.display_snapshot(cx))
678            .head()
679            .row;
680
681        let ((buffer_id, row), tasks) = self
682            .runnables
683            .runnables
684            .iter()
685            .flat_map(|(buffer_id, (_, tasks))| {
686                tasks.iter().map(|(row, tasks)| ((*buffer_id, *row), tasks))
687            })
688            .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?;
689
690        let buffer = self.buffer.read(cx).buffer(buffer_id)?;
691        let tasks = Arc::new(tasks.to_owned());
692        Some((buffer, row, tasks))
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use std::{sync::Arc, time::Duration};
699
700    use gpui::{AppContext as _, Task, TestAppContext};
701    use indoc::indoc;
702    use language::ContextProvider;
703    use languages::rust_lang;
704    use multi_buffer::{MultiBuffer, PathKey};
705    use project::{FakeFs, Project};
706    use serde_json::json;
707    use task::{TaskTemplate, TaskTemplates};
708    use text::Point;
709    use util::path;
710
711    use crate::{
712        Editor, UPDATE_DEBOUNCE, editor_tests::init_test, scroll::scroll_amount::ScrollAmount,
713    };
714
715    struct TestRustContextProvider;
716
717    impl ContextProvider for TestRustContextProvider {
718        fn associated_tasks(
719            &self,
720            _: Option<Arc<dyn language::File>>,
721            _: &gpui::App,
722        ) -> Task<Option<TaskTemplates>> {
723            Task::ready(Some(TaskTemplates(vec![
724                TaskTemplate {
725                    label: "Run main".into(),
726                    command: "cargo".into(),
727                    args: vec!["run".into()],
728                    tags: vec!["rust-main".into()],
729                    ..TaskTemplate::default()
730                },
731                TaskTemplate {
732                    label: "Run test".into(),
733                    command: "cargo".into(),
734                    args: vec!["test".into()],
735                    tags: vec!["rust-test".into()],
736                    ..TaskTemplate::default()
737                },
738            ])))
739        }
740    }
741
742    fn rust_lang_with_task_context() -> Arc<language::Language> {
743        Arc::new(
744            Arc::try_unwrap(rust_lang())
745                .unwrap()
746                .with_context_provider(Some(Arc::new(TestRustContextProvider))),
747        )
748    }
749
750    fn collect_runnable_labels(
751        editor: &Editor,
752    ) -> Vec<(text::BufferId, language::BufferRow, Vec<String>)> {
753        let mut result = editor
754            .runnables
755            .runnables
756            .iter()
757            .flat_map(|(buffer_id, (_, tasks))| {
758                tasks.iter().map(move |(row, runnable_tasks)| {
759                    let mut labels: Vec<String> = runnable_tasks
760                        .templates
761                        .iter()
762                        .map(|(_, template)| template.label.clone())
763                        .collect();
764                    labels.sort();
765                    (*buffer_id, *row, labels)
766                })
767            })
768            .collect::<Vec<_>>();
769        result.sort_by_key(|(id, row, _)| (*id, *row));
770        result
771    }
772
773    #[gpui::test]
774    async fn test_multi_buffer_runnables_on_scroll(cx: &mut TestAppContext) {
775        init_test(cx, |_| {});
776
777        let padding_lines = 50;
778        let mut first_rs = String::from("fn main() {\n    println!(\"hello\");\n}\n");
779        for _ in 0..padding_lines {
780            first_rs.push_str("//\n");
781        }
782        let test_one_row = 3 + padding_lines as u32 + 1;
783        first_rs.push_str("#[test]\nfn test_one() {\n    assert!(true);\n}\n");
784
785        let fs = FakeFs::new(cx.executor());
786        fs.insert_tree(
787            path!("/project"),
788            json!({
789                "first.rs": first_rs,
790                "second.rs": indoc! {"
791                    #[test]
792                    fn test_two() {
793                        assert!(true);
794                    }
795
796                    #[test]
797                    fn test_three() {
798                        assert!(true);
799                    }
800                "},
801            }),
802        )
803        .await;
804
805        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
806        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
807        language_registry.add(rust_lang_with_task_context());
808
809        let buffer_1 = project
810            .update(cx, |project, cx| {
811                project.open_local_buffer(path!("/project/first.rs"), cx)
812            })
813            .await
814            .unwrap();
815        let buffer_2 = project
816            .update(cx, |project, cx| {
817                project.open_local_buffer(path!("/project/second.rs"), cx)
818            })
819            .await
820            .unwrap();
821
822        let buffer_1_id = buffer_1.read_with(cx, |buffer, _| buffer.remote_id());
823        let buffer_2_id = buffer_2.read_with(cx, |buffer, _| buffer.remote_id());
824
825        let multi_buffer = cx.new(|cx| {
826            let mut multi_buffer = MultiBuffer::new(language::Capability::ReadWrite);
827            let end = buffer_1.read(cx).max_point();
828            multi_buffer.set_excerpts_for_path(
829                PathKey::sorted(0),
830                buffer_1.clone(),
831                [Point::new(0, 0)..end],
832                0,
833                cx,
834            );
835            multi_buffer.set_excerpts_for_path(
836                PathKey::sorted(1),
837                buffer_2.clone(),
838                [Point::new(0, 0)..Point::new(8, 1)],
839                0,
840                cx,
841            );
842            multi_buffer
843        });
844
845        let editor = cx.add_window(|window, cx| {
846            Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
847        });
848        cx.executor().advance_clock(Duration::from_millis(500));
849        cx.executor().run_until_parked();
850
851        // Clear stale data from startup events, then refresh.
852        // first.rs is long enough that second.rs is below the ~47-line viewport.
853        editor
854            .update(cx, |editor, window, cx| {
855                editor.clear_runnables(None);
856                editor.refresh_runnables(window, cx);
857            })
858            .unwrap();
859        cx.executor().advance_clock(UPDATE_DEBOUNCE);
860        cx.executor().run_until_parked();
861        assert_eq!(
862            editor
863                .update(cx, |editor, _, _| collect_runnable_labels(editor))
864                .unwrap(),
865            vec![(buffer_1_id, 0, vec!["Run main".to_string()])],
866            "Only fn main from first.rs should be visible before scrolling"
867        );
868
869        // Scroll down to bring second.rs excerpts into view.
870        editor
871            .update(cx, |editor, window, cx| {
872                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
873            })
874            .unwrap();
875        cx.executor().advance_clock(Duration::from_millis(200));
876        cx.executor().run_until_parked();
877
878        let after_scroll = editor
879            .update(cx, |editor, _, _| collect_runnable_labels(editor))
880            .unwrap();
881        assert_eq!(
882            after_scroll,
883            vec![
884                (buffer_1_id, 0, vec!["Run main".to_string()]),
885                (buffer_1_id, test_one_row, vec!["Run test".to_string()]),
886                (buffer_2_id, 1, vec!["Run test".to_string()]),
887                (buffer_2_id, 6, vec!["Run test".to_string()]),
888            ],
889            "Tree-sitter should detect both #[test] fns in second.rs after scroll"
890        );
891
892        // Edit second.rs to invalidate its cache; first.rs data should persist.
893        buffer_2.update(cx, |buffer, cx| {
894            buffer.edit([(0..0, "// added comment\n")], None, cx);
895        });
896        editor
897            .update(cx, |editor, window, cx| {
898                editor.scroll_screen(&ScrollAmount::Page(-1.0), window, cx);
899            })
900            .unwrap();
901        cx.executor().advance_clock(Duration::from_millis(200));
902        cx.executor().run_until_parked();
903
904        assert_eq!(
905            editor
906                .update(cx, |editor, _, _| collect_runnable_labels(editor))
907                .unwrap(),
908            vec![
909                (buffer_1_id, 0, vec!["Run main".to_string()]),
910                (buffer_1_id, test_one_row, vec!["Run test".to_string()]),
911            ],
912            "first.rs runnables should survive an edit to second.rs"
913        );
914    }
915}