runnables.rs

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