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}