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