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<Anchor>, language::RunnableRange)>,
 588        cx: AsyncWindowContext,
 589    ) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> {
 590        cx.spawn(async move |cx| {
 591            let mut runnable_rows = Vec::with_capacity(runnable_ranges.len());
 592            for (run_range, mut runnable) in runnable_ranges {
 593                let Some(tasks) = cx
 594                    .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
 595                    .ok()
 596                else {
 597                    continue;
 598                };
 599                let mut tasks = tasks.await;
 600
 601                if prefer_lsp {
 602                    tasks.retain(|(task_kind, _)| {
 603                        !matches!(task_kind, TaskSourceKind::Language { .. })
 604                    });
 605                }
 606                if tasks.is_empty() {
 607                    continue;
 608                }
 609
 610                let point = run_range.start.to_point(&snapshot);
 611                let Some(row) = snapshot
 612                    .buffer_line_for_row(MultiBufferRow(point.row))
 613                    .map(|(_, range)| range.start.row)
 614                else {
 615                    continue;
 616                };
 617
 618                let context_range =
 619                    BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
 620                runnable_rows.push((
 621                    (runnable.buffer_id, row),
 622                    RunnableTasks {
 623                        templates: tasks,
 624                        offset: run_range.start,
 625                        context_range,
 626                        column: point.column,
 627                        extra_variables: runnable.extra_captures,
 628                    },
 629                ));
 630            }
 631            runnable_rows
 632        })
 633    }
 634
 635    fn templates_with_tags(
 636        project: &Entity<Project>,
 637        runnable: &mut Runnable,
 638        cx: &mut App,
 639    ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
 640        let (inventory, worktree_id, buffer) = project.read_with(cx, |project, cx| {
 641            let buffer = project.buffer_for_id(runnable.buffer, cx);
 642            let worktree_id = buffer
 643                .as_ref()
 644                .and_then(|buffer| buffer.read(cx).file())
 645                .map(|file| file.worktree_id(cx));
 646
 647            (
 648                project.task_store().read(cx).task_inventory().cloned(),
 649                worktree_id,
 650                buffer,
 651            )
 652        });
 653
 654        let tags = mem::take(&mut runnable.tags);
 655        let language = runnable.language.clone();
 656        cx.spawn(async move |cx| {
 657            let mut templates_with_tags = Vec::new();
 658            if let Some(inventory) = inventory {
 659                for RunnableTag(tag) in tags {
 660                    let new_tasks = inventory.update(cx, |inventory, cx| {
 661                        inventory.list_tasks(
 662                            buffer.clone(),
 663                            Some(language.clone()),
 664                            worktree_id,
 665                            cx,
 666                        )
 667                    });
 668                    templates_with_tags.extend(new_tasks.await.into_iter().filter(
 669                        move |(_, template)| {
 670                            template.tags.iter().any(|source_tag| source_tag == &tag)
 671                        },
 672                    ));
 673                }
 674            }
 675            templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned());
 676
 677            if let Some((leading_tag_source, _)) = templates_with_tags.first() {
 678                // Strongest source wins; if we have worktree tag binding, prefer that to
 679                // global and language bindings;
 680                // if we have a global binding, prefer that to language binding.
 681                let first_mismatch = templates_with_tags
 682                    .iter()
 683                    .position(|(tag_source, _)| tag_source != leading_tag_source);
 684                if let Some(index) = first_mismatch {
 685                    templates_with_tags.truncate(index);
 686                }
 687            }
 688
 689            templates_with_tags
 690        })
 691    }
 692
 693    fn find_closest_task(
 694        &mut self,
 695        cx: &mut Context<Self>,
 696    ) -> Option<(Entity<Buffer>, u32, Arc<RunnableTasks>)> {
 697        let cursor_row = self
 698            .selections
 699            .newest_adjusted(&self.display_snapshot(cx))
 700            .head()
 701            .row;
 702
 703        let ((buffer_id, row), tasks) = self
 704            .runnables
 705            .runnables
 706            .iter()
 707            .flat_map(|(buffer_id, (_, tasks))| {
 708                tasks.iter().map(|(row, tasks)| ((*buffer_id, *row), tasks))
 709            })
 710            .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?;
 711
 712        let buffer = self.buffer.read(cx).buffer(buffer_id)?;
 713        let tasks = Arc::new(tasks.to_owned());
 714        Some((buffer, row, tasks))
 715    }
 716}
 717
 718#[cfg(test)]
 719mod tests {
 720    use std::{sync::Arc, time::Duration};
 721
 722    use futures::StreamExt as _;
 723    use gpui::{AppContext as _, Entity, Task, TestAppContext};
 724    use indoc::indoc;
 725    use language::{ContextProvider, FakeLspAdapter};
 726    use languages::rust_lang;
 727    use lsp::LanguageServerName;
 728    use multi_buffer::{MultiBuffer, PathKey};
 729    use project::{
 730        FakeFs, Project,
 731        lsp_store::lsp_ext_command::{CargoRunnableArgs, Runnable, RunnableArgs, RunnableKind},
 732    };
 733    use serde_json::json;
 734    use task::{TaskTemplate, TaskTemplates};
 735    use text::Point;
 736    use util::path;
 737
 738    use crate::{
 739        Editor, UPDATE_DEBOUNCE, editor_tests::init_test, scroll::scroll_amount::ScrollAmount,
 740        test::build_editor_with_project,
 741    };
 742
 743    const FAKE_LSP_NAME: &str = "the-fake-language-server";
 744
 745    struct TestRustContextProvider;
 746
 747    impl ContextProvider for TestRustContextProvider {
 748        fn associated_tasks(
 749            &self,
 750            _: Option<Entity<language::Buffer>>,
 751            _: &gpui::App,
 752        ) -> Task<Option<TaskTemplates>> {
 753            Task::ready(Some(TaskTemplates(vec![
 754                TaskTemplate {
 755                    label: "Run main".into(),
 756                    command: "cargo".into(),
 757                    args: vec!["run".into()],
 758                    tags: vec!["rust-main".into()],
 759                    ..TaskTemplate::default()
 760                },
 761                TaskTemplate {
 762                    label: "Run test".into(),
 763                    command: "cargo".into(),
 764                    args: vec!["test".into()],
 765                    tags: vec!["rust-test".into()],
 766                    ..TaskTemplate::default()
 767                },
 768            ])))
 769        }
 770    }
 771
 772    struct TestRustContextProviderWithLsp;
 773
 774    impl ContextProvider for TestRustContextProviderWithLsp {
 775        fn associated_tasks(
 776            &self,
 777            _: Option<Entity<language::Buffer>>,
 778            _: &gpui::App,
 779        ) -> Task<Option<TaskTemplates>> {
 780            Task::ready(Some(TaskTemplates(vec![TaskTemplate {
 781                label: "Run test".into(),
 782                command: "cargo".into(),
 783                args: vec!["test".into()],
 784                tags: vec!["rust-test".into()],
 785                ..TaskTemplate::default()
 786            }])))
 787        }
 788
 789        fn lsp_task_source(&self) -> Option<LanguageServerName> {
 790            Some(LanguageServerName::new_static(FAKE_LSP_NAME))
 791        }
 792    }
 793
 794    fn rust_lang_with_task_context() -> Arc<language::Language> {
 795        Arc::new(
 796            Arc::try_unwrap(rust_lang())
 797                .unwrap()
 798                .with_context_provider(Some(Arc::new(TestRustContextProvider))),
 799        )
 800    }
 801
 802    fn rust_lang_with_lsp_task_context() -> Arc<language::Language> {
 803        Arc::new(
 804            Arc::try_unwrap(rust_lang())
 805                .unwrap()
 806                .with_context_provider(Some(Arc::new(TestRustContextProviderWithLsp))),
 807        )
 808    }
 809
 810    fn collect_runnable_labels(
 811        editor: &Editor,
 812    ) -> Vec<(text::BufferId, language::BufferRow, Vec<String>)> {
 813        let mut result = editor
 814            .runnables
 815            .runnables
 816            .iter()
 817            .flat_map(|(buffer_id, (_, tasks))| {
 818                tasks.iter().map(move |(row, runnable_tasks)| {
 819                    let mut labels: Vec<String> = runnable_tasks
 820                        .templates
 821                        .iter()
 822                        .map(|(_, template)| template.label.clone())
 823                        .collect();
 824                    labels.sort();
 825                    (*buffer_id, *row, labels)
 826                })
 827            })
 828            .collect::<Vec<_>>();
 829        result.sort_by_key(|(id, row, _)| (*id, *row));
 830        result
 831    }
 832
 833    #[gpui::test]
 834    async fn test_multi_buffer_runnables_on_scroll(cx: &mut TestAppContext) {
 835        init_test(cx, |_| {});
 836
 837        let padding_lines = 50;
 838        let mut first_rs = String::from("fn main() {\n    println!(\"hello\");\n}\n");
 839        for _ in 0..padding_lines {
 840            first_rs.push_str("//\n");
 841        }
 842        let test_one_row = 3 + padding_lines as u32 + 1;
 843        first_rs.push_str("#[test]\nfn test_one() {\n    assert!(true);\n}\n");
 844
 845        let fs = FakeFs::new(cx.executor());
 846        fs.insert_tree(
 847            path!("/project"),
 848            json!({
 849                "first.rs": first_rs,
 850                "second.rs": indoc! {"
 851                    #[test]
 852                    fn test_two() {
 853                        assert!(true);
 854                    }
 855
 856                    #[test]
 857                    fn test_three() {
 858                        assert!(true);
 859                    }
 860                "},
 861            }),
 862        )
 863        .await;
 864
 865        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 866        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 867        language_registry.add(rust_lang_with_task_context());
 868
 869        let buffer_1 = project
 870            .update(cx, |project, cx| {
 871                project.open_local_buffer(path!("/project/first.rs"), cx)
 872            })
 873            .await
 874            .unwrap();
 875        let buffer_2 = project
 876            .update(cx, |project, cx| {
 877                project.open_local_buffer(path!("/project/second.rs"), cx)
 878            })
 879            .await
 880            .unwrap();
 881
 882        let buffer_1_id = buffer_1.read_with(cx, |buffer, _| buffer.remote_id());
 883        let buffer_2_id = buffer_2.read_with(cx, |buffer, _| buffer.remote_id());
 884
 885        let multi_buffer = cx.new(|cx| {
 886            let mut multi_buffer = MultiBuffer::new(language::Capability::ReadWrite);
 887            let end = buffer_1.read(cx).max_point();
 888            multi_buffer.set_excerpts_for_path(
 889                PathKey::sorted(0),
 890                buffer_1.clone(),
 891                [Point::new(0, 0)..end],
 892                0,
 893                cx,
 894            );
 895            multi_buffer.set_excerpts_for_path(
 896                PathKey::sorted(1),
 897                buffer_2.clone(),
 898                [Point::new(0, 0)..Point::new(8, 1)],
 899                0,
 900                cx,
 901            );
 902            multi_buffer
 903        });
 904
 905        let editor = cx.add_window(|window, cx| {
 906            Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
 907        });
 908        cx.executor().advance_clock(Duration::from_millis(500));
 909        cx.executor().run_until_parked();
 910
 911        // Clear stale data from startup events, then refresh.
 912        // first.rs is long enough that second.rs is below the ~47-line viewport.
 913        editor
 914            .update(cx, |editor, window, cx| {
 915                editor.clear_runnables(None);
 916                editor.refresh_runnables(None, window, cx);
 917            })
 918            .unwrap();
 919        cx.executor().advance_clock(UPDATE_DEBOUNCE);
 920        cx.executor().run_until_parked();
 921        assert_eq!(
 922            editor
 923                .update(cx, |editor, _, _| collect_runnable_labels(editor))
 924                .unwrap(),
 925            vec![(buffer_1_id, 0, vec!["Run main".to_string()])],
 926            "Only fn main from first.rs should be visible before scrolling"
 927        );
 928
 929        // Scroll down to bring second.rs excerpts into view.
 930        editor
 931            .update(cx, |editor, window, cx| {
 932                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
 933            })
 934            .unwrap();
 935        cx.executor().advance_clock(Duration::from_millis(200));
 936        cx.executor().run_until_parked();
 937
 938        let after_scroll = editor
 939            .update(cx, |editor, _, _| collect_runnable_labels(editor))
 940            .unwrap();
 941        assert_eq!(
 942            after_scroll,
 943            vec![
 944                (buffer_1_id, 0, vec!["Run main".to_string()]),
 945                (buffer_1_id, test_one_row, vec!["Run test".to_string()]),
 946                (buffer_2_id, 1, vec!["Run test".to_string()]),
 947                (buffer_2_id, 6, vec!["Run test".to_string()]),
 948            ],
 949            "Tree-sitter should detect both #[test] fns in second.rs after scroll"
 950        );
 951
 952        // Edit second.rs to invalidate its cache; first.rs data should persist.
 953        buffer_2.update(cx, |buffer, cx| {
 954            buffer.edit([(0..0, "// added comment\n")], None, cx);
 955        });
 956        editor
 957            .update(cx, |editor, window, cx| {
 958                editor.scroll_screen(&ScrollAmount::Page(-1.0), window, cx);
 959            })
 960            .unwrap();
 961        cx.executor().advance_clock(Duration::from_millis(200));
 962        cx.executor().run_until_parked();
 963
 964        assert_eq!(
 965            editor
 966                .update(cx, |editor, _, _| collect_runnable_labels(editor))
 967                .unwrap(),
 968            vec![
 969                (buffer_1_id, 0, vec!["Run main".to_string()]),
 970                (buffer_1_id, test_one_row, vec!["Run test".to_string()]),
 971            ],
 972            "first.rs runnables should survive an edit to second.rs"
 973        );
 974    }
 975
 976    #[gpui::test]
 977    async fn test_lsp_runnables_removed_after_edit(cx: &mut TestAppContext) {
 978        init_test(cx, |_| {});
 979
 980        let fs = FakeFs::new(cx.executor());
 981        fs.insert_tree(
 982            path!("/project"),
 983            json!({
 984                "main.rs": indoc! {"
 985                    #[test]
 986                    fn test_one() {
 987                        assert!(true);
 988                    }
 989
 990                    fn helper() {}
 991                "},
 992            }),
 993        )
 994        .await;
 995
 996        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 997        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 998        language_registry.add(rust_lang_with_lsp_task_context());
 999
1000        let mut fake_servers = language_registry.register_fake_lsp(
1001            "Rust",
1002            FakeLspAdapter {
1003                name: FAKE_LSP_NAME,
1004                ..FakeLspAdapter::default()
1005            },
1006        );
1007
1008        let buffer = project
1009            .update(cx, |project, cx| {
1010                project.open_local_buffer(path!("/project/main.rs"), cx)
1011            })
1012            .await
1013            .unwrap();
1014
1015        let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
1016
1017        let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
1018        let editor = cx.add_window(|window, cx| {
1019            build_editor_with_project(project.clone(), multi_buffer, window, cx)
1020        });
1021
1022        let fake_server = fake_servers.next().await.expect("fake LSP server");
1023
1024        use project::lsp_store::lsp_ext_command::Runnables;
1025        fake_server.set_request_handler::<Runnables, _, _>(move |params, _| async move {
1026            let text = params.text_document.uri.path().to_string();
1027            if text.contains("main.rs") {
1028                let uri = lsp::Uri::from_file_path(path!("/project/main.rs")).expect("valid uri");
1029                Ok(vec![Runnable {
1030                    label: "LSP test_one".into(),
1031                    location: Some(lsp::LocationLink {
1032                        origin_selection_range: None,
1033                        target_uri: uri,
1034                        target_range: lsp::Range::new(
1035                            lsp::Position::new(0, 0),
1036                            lsp::Position::new(3, 1),
1037                        ),
1038                        target_selection_range: lsp::Range::new(
1039                            lsp::Position::new(0, 0),
1040                            lsp::Position::new(3, 1),
1041                        ),
1042                    }),
1043                    kind: RunnableKind::Cargo,
1044                    args: RunnableArgs::Cargo(CargoRunnableArgs {
1045                        environment: Default::default(),
1046                        cwd: path!("/project").into(),
1047                        override_cargo: None,
1048                        workspace_root: None,
1049                        cargo_args: vec!["test".into(), "test_one".into()],
1050                        executable_args: Vec::new(),
1051                    }),
1052                }])
1053            } else {
1054                Ok(Vec::new())
1055            }
1056        });
1057
1058        // Trigger a refresh to pick up both tree-sitter and LSP runnables.
1059        editor
1060            .update(cx, |editor, window, cx| {
1061                editor.refresh_runnables(None, window, cx);
1062            })
1063            .expect("editor update");
1064        cx.executor().advance_clock(UPDATE_DEBOUNCE);
1065        cx.executor().run_until_parked();
1066
1067        let labels = editor
1068            .update(cx, |editor, _, _| collect_runnable_labels(editor))
1069            .expect("editor update");
1070        assert_eq!(
1071            labels,
1072            vec![(buffer_id, 0, vec!["LSP test_one".to_string()]),],
1073            "LSP runnables should appear for #[test] fn"
1074        );
1075
1076        // Remove `#[test]` attribute so the function is no longer a test.
1077        buffer.update(cx, |buffer, cx| {
1078            let test_attr_end = buffer.text().find("\nfn test_one").expect("find fn");
1079            buffer.edit([(0..test_attr_end, "")], None, cx);
1080        });
1081
1082        // Also update the LSP handler to return no runnables.
1083        fake_server
1084            .set_request_handler::<Runnables, _, _>(move |_, _| async move { Ok(Vec::new()) });
1085
1086        cx.executor().advance_clock(UPDATE_DEBOUNCE);
1087        cx.executor().run_until_parked();
1088
1089        let labels = editor
1090            .update(cx, |editor, _, _| collect_runnable_labels(editor))
1091            .expect("editor update");
1092        assert_eq!(
1093            labels,
1094            Vec::<(text::BufferId, language::BufferRow, Vec<String>)>::new(),
1095            "Runnables should be removed after #[test] is deleted and LSP returns empty"
1096        );
1097    }
1098}