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::{
 719            CargoRunnableArgs, Runnable, RunnableArgs, ShellRunnableArgs,
 720        },
 721    };
 722    use serde_json::json;
 723    use task::{TaskTemplate, TaskTemplates};
 724    use text::Point;
 725    use util::path;
 726    use util::rel_path::rel_path;
 727
 728    use crate::{
 729        Editor, UPDATE_DEBOUNCE, editor_tests::init_test, scroll::scroll_amount::ScrollAmount,
 730        test::build_editor_with_project,
 731    };
 732
 733    const FAKE_LSP_NAME: &str = "the-fake-language-server";
 734
 735    struct TestRustContextProvider;
 736
 737    impl ContextProvider for TestRustContextProvider {
 738        fn associated_tasks(
 739            &self,
 740            _: Option<Entity<language::Buffer>>,
 741            _: &gpui::App,
 742        ) -> Task<Option<TaskTemplates>> {
 743            Task::ready(Some(TaskTemplates(vec![
 744                TaskTemplate {
 745                    label: "Run main".into(),
 746                    command: "cargo".into(),
 747                    args: vec!["run".into()],
 748                    tags: vec!["rust-main".into()],
 749                    ..TaskTemplate::default()
 750                },
 751                TaskTemplate {
 752                    label: "Run test".into(),
 753                    command: "cargo".into(),
 754                    args: vec!["test".into()],
 755                    tags: vec!["rust-test".into()],
 756                    ..TaskTemplate::default()
 757                },
 758            ])))
 759        }
 760    }
 761
 762    struct TestRustContextProviderWithLsp;
 763
 764    impl ContextProvider for TestRustContextProviderWithLsp {
 765        fn associated_tasks(
 766            &self,
 767            _: Option<Entity<language::Buffer>>,
 768            _: &gpui::App,
 769        ) -> Task<Option<TaskTemplates>> {
 770            Task::ready(Some(TaskTemplates(vec![TaskTemplate {
 771                label: "Run test".into(),
 772                command: "cargo".into(),
 773                args: vec!["test".into()],
 774                tags: vec!["rust-test".into()],
 775                ..TaskTemplate::default()
 776            }])))
 777        }
 778
 779        fn lsp_task_source(&self) -> Option<LanguageServerName> {
 780            Some(LanguageServerName::new_static(FAKE_LSP_NAME))
 781        }
 782    }
 783
 784    fn rust_lang_with_task_context() -> Arc<language::Language> {
 785        Arc::new(
 786            Arc::try_unwrap(rust_lang())
 787                .unwrap()
 788                .with_context_provider(Some(Arc::new(TestRustContextProvider))),
 789        )
 790    }
 791
 792    fn rust_lang_with_lsp_task_context() -> Arc<language::Language> {
 793        Arc::new(
 794            Arc::try_unwrap(rust_lang())
 795                .unwrap()
 796                .with_context_provider(Some(Arc::new(TestRustContextProviderWithLsp))),
 797        )
 798    }
 799
 800    fn collect_runnable_labels(
 801        editor: &Editor,
 802    ) -> Vec<(text::BufferId, language::BufferRow, Vec<String>)> {
 803        let mut result = editor
 804            .runnables
 805            .runnables
 806            .iter()
 807            .flat_map(|(buffer_id, (_, tasks))| {
 808                tasks.iter().map(move |(row, runnable_tasks)| {
 809                    let mut labels: Vec<String> = runnable_tasks
 810                        .templates
 811                        .iter()
 812                        .map(|(_, template)| template.label.clone())
 813                        .collect();
 814                    labels.sort();
 815                    (*buffer_id, *row, labels)
 816                })
 817            })
 818            .collect::<Vec<_>>();
 819        result.sort_by_key(|(id, row, _)| (*id, *row));
 820        result
 821    }
 822
 823    #[gpui::test]
 824    async fn test_multi_buffer_runnables_on_scroll(cx: &mut TestAppContext) {
 825        init_test(cx, |_| {});
 826
 827        let padding_lines = 50;
 828        let mut first_rs = String::from("fn main() {\n    println!(\"hello\");\n}\n");
 829        for _ in 0..padding_lines {
 830            first_rs.push_str("//\n");
 831        }
 832        let test_one_row = 3 + padding_lines as u32 + 1;
 833        first_rs.push_str("#[test]\nfn test_one() {\n    assert!(true);\n}\n");
 834
 835        let fs = FakeFs::new(cx.executor());
 836        fs.insert_tree(
 837            path!("/project"),
 838            json!({
 839                "first.rs": first_rs,
 840                "second.rs": indoc! {"
 841                    #[test]
 842                    fn test_two() {
 843                        assert!(true);
 844                    }
 845
 846                    #[test]
 847                    fn test_three() {
 848                        assert!(true);
 849                    }
 850                "},
 851            }),
 852        )
 853        .await;
 854
 855        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 856        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 857        language_registry.add(rust_lang_with_task_context());
 858
 859        let buffer_1 = project
 860            .update(cx, |project, cx| {
 861                project.open_local_buffer(path!("/project/first.rs"), cx)
 862            })
 863            .await
 864            .unwrap();
 865        let buffer_2 = project
 866            .update(cx, |project, cx| {
 867                project.open_local_buffer(path!("/project/second.rs"), cx)
 868            })
 869            .await
 870            .unwrap();
 871
 872        let buffer_1_id = buffer_1.read_with(cx, |buffer, _| buffer.remote_id());
 873        let buffer_2_id = buffer_2.read_with(cx, |buffer, _| buffer.remote_id());
 874
 875        let multi_buffer = cx.new(|cx| {
 876            let mut multi_buffer = MultiBuffer::new(language::Capability::ReadWrite);
 877            let end = buffer_1.read(cx).max_point();
 878            multi_buffer.set_excerpts_for_path(
 879                PathKey::sorted(0),
 880                buffer_1.clone(),
 881                [Point::new(0, 0)..end],
 882                0,
 883                cx,
 884            );
 885            multi_buffer.set_excerpts_for_path(
 886                PathKey::sorted(1),
 887                buffer_2.clone(),
 888                [Point::new(0, 0)..Point::new(8, 1)],
 889                0,
 890                cx,
 891            );
 892            multi_buffer
 893        });
 894
 895        let editor = cx.add_window(|window, cx| {
 896            Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
 897        });
 898        cx.executor().advance_clock(Duration::from_millis(500));
 899        cx.executor().run_until_parked();
 900
 901        // Clear stale data from startup events, then refresh.
 902        // first.rs is long enough that second.rs is below the ~47-line viewport.
 903        editor
 904            .update(cx, |editor, window, cx| {
 905                editor.clear_runnables(None);
 906                editor.refresh_runnables(None, window, cx);
 907            })
 908            .unwrap();
 909        cx.executor().advance_clock(UPDATE_DEBOUNCE);
 910        cx.executor().run_until_parked();
 911        assert_eq!(
 912            editor
 913                .update(cx, |editor, _, _| collect_runnable_labels(editor))
 914                .unwrap(),
 915            vec![(buffer_1_id, 0, vec!["Run main".to_string()])],
 916            "Only fn main from first.rs should be visible before scrolling"
 917        );
 918
 919        // Scroll down to bring second.rs excerpts into view.
 920        editor
 921            .update(cx, |editor, window, cx| {
 922                editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
 923            })
 924            .unwrap();
 925        cx.executor().advance_clock(Duration::from_millis(200));
 926        cx.executor().run_until_parked();
 927
 928        let after_scroll = editor
 929            .update(cx, |editor, _, _| collect_runnable_labels(editor))
 930            .unwrap();
 931        assert_eq!(
 932            after_scroll,
 933            vec![
 934                (buffer_1_id, 0, vec!["Run main".to_string()]),
 935                (buffer_1_id, test_one_row, vec!["Run test".to_string()]),
 936                (buffer_2_id, 1, vec!["Run test".to_string()]),
 937                (buffer_2_id, 6, vec!["Run test".to_string()]),
 938            ],
 939            "Tree-sitter should detect both #[test] fns in second.rs after scroll"
 940        );
 941
 942        // Edit second.rs to invalidate its cache; first.rs data should persist.
 943        buffer_2.update(cx, |buffer, cx| {
 944            buffer.edit([(0..0, "// added comment\n")], None, cx);
 945        });
 946        editor
 947            .update(cx, |editor, window, cx| {
 948                editor.scroll_screen(&ScrollAmount::Page(-1.0), window, cx);
 949            })
 950            .unwrap();
 951        cx.executor().advance_clock(Duration::from_millis(200));
 952        cx.executor().run_until_parked();
 953
 954        assert_eq!(
 955            editor
 956                .update(cx, |editor, _, _| collect_runnable_labels(editor))
 957                .unwrap(),
 958            vec![
 959                (buffer_1_id, 0, vec!["Run main".to_string()]),
 960                (buffer_1_id, test_one_row, vec!["Run test".to_string()]),
 961            ],
 962            "first.rs runnables should survive an edit to second.rs"
 963        );
 964    }
 965
 966    #[gpui::test]
 967    async fn test_lsp_runnables_removed_after_edit(cx: &mut TestAppContext) {
 968        init_test(cx, |_| {});
 969
 970        let fs = FakeFs::new(cx.executor());
 971        fs.insert_tree(
 972            path!("/project"),
 973            json!({
 974                "main.rs": indoc! {"
 975                    #[test]
 976                    fn test_one() {
 977                        assert!(true);
 978                    }
 979
 980                    fn helper() {}
 981                "},
 982            }),
 983        )
 984        .await;
 985
 986        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 987        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 988        language_registry.add(rust_lang_with_lsp_task_context());
 989
 990        let mut fake_servers = language_registry.register_fake_lsp(
 991            "Rust",
 992            FakeLspAdapter {
 993                name: FAKE_LSP_NAME,
 994                ..FakeLspAdapter::default()
 995            },
 996        );
 997
 998        let buffer = project
 999            .update(cx, |project, cx| {
1000                project.open_local_buffer(path!("/project/main.rs"), cx)
1001            })
1002            .await
1003            .unwrap();
1004
1005        let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
1006
1007        let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
1008        let editor = cx.add_window(|window, cx| {
1009            build_editor_with_project(project.clone(), multi_buffer, window, cx)
1010        });
1011
1012        let fake_server = fake_servers.next().await.expect("fake LSP server");
1013
1014        use project::lsp_store::lsp_ext_command::Runnables;
1015        fake_server.set_request_handler::<Runnables, _, _>(move |params, _| async move {
1016            let text = params.text_document.uri.path().to_string();
1017            if text.contains("main.rs") {
1018                let uri = lsp::Uri::from_file_path(path!("/project/main.rs")).expect("valid uri");
1019                Ok(vec![Runnable {
1020                    label: "LSP test_one".into(),
1021                    location: Some(lsp::LocationLink {
1022                        origin_selection_range: None,
1023                        target_uri: uri,
1024                        target_range: lsp::Range::new(
1025                            lsp::Position::new(0, 0),
1026                            lsp::Position::new(3, 1),
1027                        ),
1028                        target_selection_range: lsp::Range::new(
1029                            lsp::Position::new(0, 0),
1030                            lsp::Position::new(3, 1),
1031                        ),
1032                    }),
1033                    args: RunnableArgs::Cargo(CargoRunnableArgs {
1034                        environment: Default::default(),
1035                        cwd: path!("/project").into(),
1036                        override_cargo: None,
1037                        workspace_root: None,
1038                        cargo_args: vec!["test".into(), "test_one".into()],
1039                        executable_args: Vec::new(),
1040                    }),
1041                }])
1042            } else {
1043                Ok(Vec::new())
1044            }
1045        });
1046
1047        // Trigger a refresh to pick up both tree-sitter and LSP runnables.
1048        editor
1049            .update(cx, |editor, window, cx| {
1050                editor.refresh_runnables(None, window, cx);
1051            })
1052            .expect("editor update");
1053        cx.executor().advance_clock(UPDATE_DEBOUNCE);
1054        cx.executor().run_until_parked();
1055
1056        let labels = editor
1057            .update(cx, |editor, _, _| collect_runnable_labels(editor))
1058            .expect("editor update");
1059        assert_eq!(
1060            labels,
1061            vec![(buffer_id, 0, vec!["LSP test_one".to_string()]),],
1062            "LSP runnables should appear for #[test] fn"
1063        );
1064
1065        // Remove `#[test]` attribute so the function is no longer a test.
1066        buffer.update(cx, |buffer, cx| {
1067            let test_attr_end = buffer.text().find("\nfn test_one").expect("find fn");
1068            buffer.edit([(0..test_attr_end, "")], None, cx);
1069        });
1070
1071        // Also update the LSP handler to return no runnables.
1072        fake_server
1073            .set_request_handler::<Runnables, _, _>(move |_, _| async move { Ok(Vec::new()) });
1074
1075        cx.executor().advance_clock(UPDATE_DEBOUNCE);
1076        cx.executor().run_until_parked();
1077
1078        let labels = editor
1079            .update(cx, |editor, _, _| collect_runnable_labels(editor))
1080            .expect("editor update");
1081        assert_eq!(
1082            labels,
1083            Vec::<(text::BufferId, language::BufferRow, Vec<String>)>::new(),
1084            "Runnables should be removed after #[test] is deleted and LSP returns empty"
1085        );
1086    }
1087
1088    #[gpui::test]
1089    async fn test_no_runnables_for_unsaved_buffer(cx: &mut TestAppContext) {
1090        init_test(cx, |_| {});
1091
1092        let fs = FakeFs::new(cx.executor());
1093        fs.insert_tree(path!("/project"), json!({})).await;
1094
1095        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1096        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1097        language_registry.add(rust_lang_with_task_context());
1098
1099        let rust_language = language_registry.language_for_name("Rust").await.unwrap();
1100        let buffer = cx.new(|cx| {
1101            let mut buffer = language::Buffer::local(
1102                indoc! {"
1103                    fn main() {
1104                        println!(\"hello\");
1105                    }
1106
1107                    #[test]
1108                    fn test_one() {
1109                        assert!(true);
1110                    }
1111                "},
1112                cx,
1113            );
1114            buffer.set_language(Some(rust_language), cx);
1115            buffer
1116        });
1117
1118        let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
1119        let editor = cx.add_window(|window, cx| {
1120            build_editor_with_project(project.clone(), multi_buffer, window, cx)
1121        });
1122
1123        editor
1124            .update(cx, |editor, window, cx| {
1125                editor.refresh_runnables(None, window, cx);
1126            })
1127            .expect("editor update");
1128        cx.executor().advance_clock(UPDATE_DEBOUNCE);
1129        cx.executor().run_until_parked();
1130
1131        let labels = editor
1132            .update(cx, |editor, _, _| collect_runnable_labels(editor))
1133            .expect("editor update");
1134        assert_eq!(
1135            labels,
1136            Vec::<(text::BufferId, language::BufferRow, Vec<String>)>::new(),
1137            "No runnables should appear for an unsaved buffer without a file on disk"
1138        );
1139
1140        let worktree_id = project.update(cx, |project, cx| {
1141            project
1142                .worktrees(cx)
1143                .next()
1144                .expect("worktree")
1145                .read(cx)
1146                .id()
1147        });
1148        project
1149            .update(cx, |project, cx| {
1150                project.save_buffer_as(
1151                    buffer.clone(),
1152                    ProjectPath {
1153                        worktree_id,
1154                        path: rel_path("main.rs").into(),
1155                    },
1156                    cx,
1157                )
1158            })
1159            .await
1160            .expect("save buffer as");
1161
1162        editor
1163            .update(cx, |editor, window, cx| {
1164                editor.refresh_runnables(None, window, cx);
1165            })
1166            .expect("editor update");
1167        cx.executor().advance_clock(UPDATE_DEBOUNCE);
1168        cx.executor().run_until_parked();
1169
1170        let labels = editor
1171            .update(cx, |editor, _, _| collect_runnable_labels(editor))
1172            .expect("editor update");
1173        assert!(
1174            !labels.is_empty(),
1175            "Runnables should appear after the buffer is saved to disk"
1176        );
1177    }
1178
1179    // Verifies that a shell runnable from rust-analyzer produces
1180    // a task template that uses the shell program and args.
1181    #[gpui::test]
1182    async fn test_shell_runnable_produces_correct_task_template(cx: &mut TestAppContext) {
1183        init_test(cx, |_| {});
1184
1185        let fs = FakeFs::new(cx.executor());
1186        fs.insert_tree(
1187            path!("/project"),
1188            json!({
1189                "main.rs": indoc! {"
1190                    #[test]
1191                    fn test_one() {
1192                        assert!(true);
1193                    }
1194                "},
1195            }),
1196        )
1197        .await;
1198
1199        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1200        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1201        language_registry.add(rust_lang_with_lsp_task_context());
1202
1203        let mut fake_servers = language_registry.register_fake_lsp(
1204            "Rust",
1205            FakeLspAdapter {
1206                name: FAKE_LSP_NAME,
1207                ..FakeLspAdapter::default()
1208            },
1209        );
1210
1211        let buffer = project
1212            .update(cx, |project, cx| {
1213                project.open_local_buffer(path!("/project/main.rs"), cx)
1214            })
1215            .await
1216            .unwrap();
1217
1218        let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
1219
1220        let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
1221        let editor = cx.add_window(|window, cx| {
1222            build_editor_with_project(project.clone(), multi_buffer, window, cx)
1223        });
1224
1225        let fake_server = fake_servers.next().await.expect("fake LSP server");
1226
1227        use project::lsp_store::lsp_ext_command::Runnables;
1228        fake_server.set_request_handler::<Runnables, _, _>(move |params, _| async move {
1229            let text = params.text_document.uri.path().to_string();
1230            if text.contains("main.rs") {
1231                let uri = lsp::Uri::from_file_path(path!("/project/main.rs")).expect("valid uri");
1232                Ok(vec![Runnable {
1233                    label: "nextest test_one".into(),
1234                    location: Some(lsp::LocationLink {
1235                        origin_selection_range: None,
1236                        target_uri: uri,
1237                        target_range: lsp::Range::new(
1238                            lsp::Position::new(0, 0),
1239                            lsp::Position::new(3, 1),
1240                        ),
1241                        target_selection_range: lsp::Range::new(
1242                            lsp::Position::new(0, 0),
1243                            lsp::Position::new(3, 1),
1244                        ),
1245                    }),
1246                    args: RunnableArgs::Shell(ShellRunnableArgs {
1247                        environment: Default::default(),
1248                        cwd: path!("/project").into(),
1249                        program: "cargo".into(),
1250                        args: vec![
1251                            "nextest".into(),
1252                            "run".into(),
1253                            "--package".into(),
1254                            "my-crate".into(),
1255                            "--lib".into(),
1256                            "--".into(),
1257                            "test_one".into(),
1258                            "--exact".into(),
1259                        ],
1260                    }),
1261                }])
1262            } else {
1263                Ok(Vec::new())
1264            }
1265        });
1266
1267        editor
1268            .update(cx, |editor, window, cx| {
1269                editor.refresh_runnables(None, window, cx);
1270            })
1271            .expect("editor update");
1272        cx.executor().advance_clock(UPDATE_DEBOUNCE);
1273        cx.executor().run_until_parked();
1274
1275        let labels = editor
1276            .update(cx, |editor, _, _| collect_runnable_labels(editor))
1277            .expect("editor update");
1278        assert_eq!(
1279            labels,
1280            vec![(buffer_id, 0, vec!["nextest test_one".to_string()])],
1281            "shell runnable should appear for #[test] fn"
1282        );
1283
1284        let templates = editor
1285            .update(cx, |editor, _, _| {
1286                editor
1287                    .runnables
1288                    .runnables
1289                    .iter()
1290                    .flat_map(|(_, (_, tasks))| {
1291                        tasks.iter().flat_map(|(_, runnable_tasks)| {
1292                            runnable_tasks
1293                                .templates
1294                                .iter()
1295                                .map(|(_, template)| {
1296                                    (
1297                                        template.label.clone(),
1298                                        template.command.clone(),
1299                                        template.args.clone(),
1300                                    )
1301                                })
1302                                .collect::<Vec<_>>()
1303                        })
1304                    })
1305                    .collect::<Vec<_>>()
1306            })
1307            .expect("editor update");
1308
1309        let (label, command, args) = templates
1310            .iter()
1311            .find(|(label, _, _)| label == "nextest test_one")
1312            .expect("shell runnable task template should exist");
1313        assert_eq!(label, "nextest test_one");
1314        assert_eq!(command, "cargo");
1315        assert_eq!(
1316            args,
1317            &[
1318                "nextest",
1319                "run",
1320                "--package",
1321                "my-crate",
1322                "--lib",
1323                "--",
1324                "test_one",
1325                "--exact",
1326            ],
1327            "shell runnable should preserve program args"
1328        );
1329    }
1330}