runnables.rs

   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}