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