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