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