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