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