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::{
  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}