Detailed changes
@@ -35,13 +35,13 @@ mod lsp_ext;
mod mouse_context_menu;
pub mod movement;
mod persistence;
+mod runnables;
mod rust_analyzer_ext;
pub mod scroll;
mod selections_collection;
pub mod semantic_tokens;
mod split;
pub mod split_editor_view;
-pub mod tasks;
#[cfg(test)]
mod code_completion_tests;
@@ -133,8 +133,8 @@ use language::{
BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
DiagnosticEntryRef, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
IndentSize, Language, LanguageName, LanguageRegistry, LanguageScope, LocalFile, OffsetRangeExt,
- OutlineItem, Point, Runnable, Selection, SelectionGoal, TextObject, TransactionId,
- TreeSitterOptions, WordsQuery,
+ OutlineItem, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
+ WordsQuery,
language_settings::{
self, LanguageSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
@@ -158,7 +158,7 @@ use project::{
BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent,
CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, InlayId,
InvalidationStrategy, Location, LocationLink, LspAction, PrepareRenameResponse, Project,
- ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind,
+ ProjectItem, ProjectPath, ProjectTransaction,
debugger::{
breakpoint_store::{
Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState,
@@ -200,7 +200,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
-use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
+use task::TaskVariables;
use text::{BufferId, FromAnchor, OffsetUtf16, Rope, ToOffset as _, ToPoint as _};
use theme::{
AccentColors, ActiveTheme, GlobalTheme, PlayerColor, StatusColors, SyntaxTheme, Theme,
@@ -231,6 +231,7 @@ use crate::{
InlineValueCache,
inlay_hints::{LspInlayHintData, inlay_hint_settings},
},
+ runnables::{ResolvedTasks, RunnableData, RunnableTasks},
scroll::{ScrollOffset, ScrollPixelOffset},
selections_collection::resolve_selections_wrapping_blocks,
semantic_tokens::SemanticTokenState,
@@ -857,37 +858,6 @@ impl BufferSerialization {
}
}
-#[derive(Clone, Debug)]
-struct RunnableTasks {
- templates: Vec<(TaskSourceKind, TaskTemplate)>,
- offset: multi_buffer::Anchor,
- // We need the column at which the task context evaluation should take place (when we're spawning it via gutter).
- column: u32,
- // Values of all named captures, including those starting with '_'
- extra_variables: HashMap<String, String>,
- // Full range of the tagged region. We use it to determine which `extra_variables` to grab for context resolution in e.g. a modal.
- context_range: Range<BufferOffset>,
-}
-
-impl RunnableTasks {
- fn resolve<'a>(
- &'a self,
- cx: &'a task::TaskContext,
- ) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a {
- self.templates.iter().filter_map(|(kind, template)| {
- template
- .resolve_task(&kind.to_id_base(), cx)
- .map(|task| (kind.clone(), task))
- })
- }
-}
-
-#[derive(Clone)]
-pub struct ResolvedTasks {
- templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
- position: Anchor,
-}
-
/// Addons allow storing per-editor state in other crates (e.g. Vim)
pub trait Addon: 'static {
fn extend_key_context(&self, _: &mut KeyContext, _: &App) {}
@@ -1295,8 +1265,7 @@ pub struct Editor {
last_bounds: Option<Bounds<Pixels>>,
last_position_map: Option<Rc<PositionMap>>,
expect_bounds_change: Option<Bounds<Pixels>>,
- tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
- tasks_update_task: Option<Task<()>>,
+ runnables: RunnableData,
breakpoint_store: Option<Entity<BreakpointStore>>,
gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>),
pub(crate) gutter_diff_review_indicator: (Option<PhantomDiffReviewIndicator>, Option<Task<()>>),
@@ -2173,16 +2142,9 @@ impl Editor {
editor.registered_buffers.clear();
editor.register_visible_buffers(cx);
editor.invalidate_semantic_tokens(None);
+ editor.refresh_runnables(window, cx);
editor.update_lsp_data(None, window, cx);
editor.refresh_inlay_hints(InlayHintRefreshReason::ServerRemoved, cx);
- if editor.tasks_update_task.is_none() {
- editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
- }
- }
- project::Event::LanguageServerAdded(..) => {
- if editor.tasks_update_task.is_none() {
- editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
- }
}
project::Event::SnippetEdit(id, snippet_edits) => {
// todo(lw): Non singletons
@@ -2210,6 +2172,7 @@ impl Editor {
let buffer_id = *buffer_id;
if editor.buffer().read(cx).buffer(buffer_id).is_some() {
editor.register_buffer(buffer_id, cx);
+ editor.refresh_runnables(window, cx);
editor.update_lsp_data(Some(buffer_id), window, cx);
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
refresh_linked_ranges(editor, window, cx);
@@ -2288,7 +2251,7 @@ impl Editor {
&task_inventory,
window,
|editor, _, window, cx| {
- editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
+ editor.refresh_runnables(window, cx);
},
));
};
@@ -2529,7 +2492,6 @@ impl Editor {
}),
blame: None,
blame_subscription: None,
- tasks: BTreeMap::default(),
breakpoint_store,
gutter_breakpoint_indicator: (None, None),
@@ -2565,7 +2527,7 @@ impl Editor {
]
})
.unwrap_or_default(),
- tasks_update_task: None,
+ runnables: RunnableData::new(),
pull_diagnostics_task: Task::ready(()),
colors: None,
refresh_colors_task: Task::ready(()),
@@ -2632,7 +2594,6 @@ impl Editor {
cx.notify();
}));
}
- editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
editor._subscriptions.extend(project_subscriptions);
editor._subscriptions.push(cx.subscribe_in(
@@ -2668,6 +2629,7 @@ impl Editor {
);
if !editor.buffer().read(cx).is_singleton() {
editor.update_lsp_data(None, window, cx);
+ editor.refresh_runnables(window, cx);
}
})
.ok();
@@ -5791,18 +5753,11 @@ impl Editor {
let display_snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let multi_buffer = self.buffer().read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
- let multi_buffer_visible_start = self
- .scroll_manager
- .native_anchor(&display_snapshot, cx)
- .anchor
- .to_point(&multi_buffer_snapshot);
- let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
- multi_buffer_visible_start
- + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0),
- Bias::Left,
- );
multi_buffer_snapshot
- .range_to_buffer_ranges(multi_buffer_visible_start..=multi_buffer_visible_end)
+ .range_to_buffer_ranges(
+ self.multi_buffer_visible_range(&display_snapshot, cx)
+ .to_inclusive(),
+ )
.into_iter()
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
.filter_map(|(buffer, excerpt_visible_range, excerpt_id)| {
@@ -6737,8 +6692,8 @@ impl Editor {
};
let buffer_id = buffer.read(cx).remote_id();
let tasks = self
- .tasks
- .get(&(buffer_id, buffer_row))
+ .runnables
+ .runnables((buffer_id, buffer_row))
.map(|t| Arc::new(t.to_owned()));
if !self.focus_handle.is_focused(window) {
@@ -7789,24 +7744,13 @@ impl Editor {
self.debounced_selection_highlight_complete = false;
}
if on_buffer_edit || query_changed {
- let multi_buffer_visible_start = self
- .scroll_manager
- .native_anchor(&display_snapshot, cx)
- .anchor
- .to_point(&multi_buffer_snapshot);
- let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
- multi_buffer_visible_start
- + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0),
- Bias::Left,
- );
- let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
self.quick_selection_highlight_task = Some((
query_range.clone(),
self.update_selection_occurrence_highlights(
snapshot.buffer.clone(),
query_text.clone(),
query_range.clone(),
- multi_buffer_visible_range,
+ self.multi_buffer_visible_range(&display_snapshot, cx),
false,
window,
cx,
@@ -7841,6 +7785,27 @@ impl Editor {
}
}
+ pub fn multi_buffer_visible_range(
+ &self,
+ display_snapshot: &DisplaySnapshot,
+ cx: &App,
+ ) -> Range<Point> {
+ let visible_start = self
+ .scroll_manager
+ .native_anchor(display_snapshot, cx)
+ .anchor
+ .to_point(display_snapshot.buffer_snapshot())
+ .to_display_point(display_snapshot);
+
+ let mut target_end = visible_start;
+ *target_end.row_mut() += self.visible_line_count().unwrap_or(0.).ceil() as u32;
+
+ visible_start.to_point(display_snapshot)
+ ..display_snapshot
+ .clip_point(target_end, Bias::Right)
+ .to_point(display_snapshot)
+ }
+
pub fn refresh_edit_prediction(
&mut self,
debounce: bool,
@@ -8809,19 +8774,6 @@ impl Editor {
Some(self.edit_prediction_provider.as_ref()?.provider.clone())
}
- fn clear_tasks(&mut self) {
- self.tasks.clear()
- }
-
- fn insert_tasks(&mut self, key: (BufferId, BufferRow), value: RunnableTasks) {
- if self.tasks.insert(key, value).is_some() {
- // This case should hopefully be rare, but just in case...
- log::error!(
- "multiple different run targets found on a single line, only the last target will be rendered"
- )
- }
- }
-
/// Get all display points of breakpoints that will be rendered within editor
///
/// This function is used to handle overlaps between breakpoints and Code action/runner symbol.
@@ -9199,156 +9151,6 @@ impl Editor {
})
}
- pub fn spawn_nearest_task(
- &mut self,
- action: &SpawnNearestTask,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let Some((workspace, _)) = self.workspace.clone() else {
- return;
- };
- let Some(project) = self.project.clone() else {
- return;
- };
-
- // Try to find a closest, enclosing node using tree-sitter that has a task
- let Some((buffer, buffer_row, tasks)) = self
- .find_enclosing_node_task(cx)
- // Or find the task that's closest in row-distance.
- .or_else(|| self.find_closest_task(cx))
- else {
- return;
- };
-
- let reveal_strategy = action.reveal;
- let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
- cx.spawn_in(window, async move |_, cx| {
- let context = task_context.await?;
- let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?;
-
- let resolved = &mut resolved_task.resolved;
- resolved.reveal = reveal_strategy;
-
- workspace
- .update_in(cx, |workspace, window, cx| {
- workspace.schedule_resolved_task(
- task_source_kind,
- resolved_task,
- false,
- window,
- cx,
- );
- })
- .ok()
- })
- .detach();
- }
-
- fn find_closest_task(
- &mut self,
- cx: &mut Context<Self>,
- ) -> Option<(Entity<Buffer>, u32, Arc<RunnableTasks>)> {
- let cursor_row = self
- .selections
- .newest_adjusted(&self.display_snapshot(cx))
- .head()
- .row;
-
- let ((buffer_id, row), tasks) = self
- .tasks
- .iter()
- .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?;
-
- let buffer = self.buffer.read(cx).buffer(*buffer_id)?;
- let tasks = Arc::new(tasks.to_owned());
- Some((buffer, *row, tasks))
- }
-
- fn find_enclosing_node_task(
- &mut self,
- cx: &mut Context<Self>,
- ) -> Option<(Entity<Buffer>, u32, Arc<RunnableTasks>)> {
- let snapshot = self.buffer.read(cx).snapshot(cx);
- let offset = self
- .selections
- .newest::<MultiBufferOffset>(&self.display_snapshot(cx))
- .head();
- let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
- let offset = excerpt.map_offset_to_buffer(offset);
- let buffer_id = excerpt.buffer().remote_id();
-
- let layer = excerpt.buffer().syntax_layer_at(offset)?;
- let mut cursor = layer.node().walk();
-
- while cursor.goto_first_child_for_byte(offset.0).is_some() {
- if cursor.node().end_byte() == offset.0 {
- cursor.goto_next_sibling();
- }
- }
-
- // Ascend to the smallest ancestor that contains the range and has a task.
- loop {
- let node = cursor.node();
- let node_range = node.byte_range();
- let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row;
-
- // Check if this node contains our offset
- if node_range.start <= offset.0 && node_range.end >= offset.0 {
- // If it contains offset, check for task
- if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) {
- let buffer = self.buffer.read(cx).buffer(buffer_id)?;
- return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned())));
- }
- }
-
- if !cursor.goto_parent() {
- break;
- }
- }
- None
- }
-
- fn render_run_indicator(
- &self,
- _style: &EditorStyle,
- is_active: bool,
- row: DisplayRow,
- breakpoint: Option<(Anchor, Breakpoint, Option<BreakpointSessionState>)>,
- cx: &mut Context<Self>,
- ) -> IconButton {
- let color = Color::Muted;
- let position = breakpoint.as_ref().map(|(anchor, _, _)| *anchor);
-
- IconButton::new(
- ("run_indicator", row.0 as usize),
- ui::IconName::PlayOutlined,
- )
- .shape(ui::IconButtonShape::Square)
- .icon_size(IconSize::XSmall)
- .icon_color(color)
- .toggle_state(is_active)
- .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| {
- let quick_launch = match e {
- ClickEvent::Keyboard(_) => true,
- ClickEvent::Mouse(e) => e.down.button == MouseButton::Left,
- };
-
- window.focus(&editor.focus_handle(cx), cx);
- editor.toggle_code_actions(
- &ToggleCodeActions {
- deployed_from: Some(CodeActionSource::RunMenu(row)),
- quick_launch,
- },
- window,
- cx,
- );
- }))
- .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
- editor.set_breakpoint_context_menu(row, position, event.position(), window, cx);
- }))
- }
-
pub fn context_menu_visible(&self) -> bool {
!self.edit_prediction_preview_is_active()
&& self
@@ -17153,241 +16955,6 @@ impl Editor {
});
}
- fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Task<()> {
- if !self.mode().is_full()
- || !EditorSettings::get_global(cx).gutter.runnables
- || !self.enable_runnables
- {
- self.clear_tasks();
- return Task::ready(());
- }
- let project = self.project().map(Entity::downgrade);
- let task_sources = self.lsp_task_sources(cx);
- let multi_buffer = self.buffer.downgrade();
- let lsp_data_enabled = self.lsp_data_enabled();
- cx.spawn_in(window, async move |editor, cx| {
- cx.background_executor().timer(UPDATE_DEBOUNCE).await;
- let Some(project) = project.and_then(|p| p.upgrade()) else {
- return;
- };
- let Ok(display_snapshot) = editor.update(cx, |this, cx| {
- this.display_map.update(cx, |map, cx| map.snapshot(cx))
- }) else {
- return;
- };
-
- let hide_runnables = project.update(cx, |project, _| project.is_via_collab());
- if hide_runnables {
- return;
- }
- let new_rows = cx
- .background_spawn({
- let snapshot = display_snapshot.clone();
- async move {
- snapshot
- .buffer_snapshot()
- .runnable_ranges(Anchor::min()..Anchor::max())
- .collect()
- }
- })
- .await;
- let lsp_tasks = if lsp_data_enabled {
- let Ok(lsp_tasks) =
- cx.update(|_, cx| crate::lsp_tasks(project.clone(), &task_sources, None, cx))
- else {
- return;
- };
- lsp_tasks.await
- } else {
- Vec::new()
- };
-
- let Ok(mut lsp_tasks_by_rows) = cx.update(|_, cx| {
- lsp_tasks
- .into_iter()
- .flat_map(|(kind, tasks)| {
- tasks.into_iter().filter_map(move |(location, task)| {
- Some((kind.clone(), location?, task))
- })
- })
- .fold(HashMap::default(), |mut acc, (kind, location, task)| {
- let buffer = location.target.buffer;
- let buffer_snapshot = buffer.read(cx).snapshot();
- let offset = display_snapshot.buffer_snapshot().excerpts().find_map(
- |(excerpt_id, snapshot, _)| {
- if snapshot.remote_id() == buffer_snapshot.remote_id() {
- display_snapshot
- .buffer_snapshot()
- .anchor_in_excerpt(excerpt_id, location.target.range.start)
- } else {
- None
- }
- },
- );
- if let Some(offset) = offset {
- let task_buffer_range =
- location.target.range.to_point(&buffer_snapshot);
- let context_buffer_range =
- task_buffer_range.to_offset(&buffer_snapshot);
- let context_range = BufferOffset(context_buffer_range.start)
- ..BufferOffset(context_buffer_range.end);
-
- acc.entry((buffer_snapshot.remote_id(), task_buffer_range.start.row))
- .or_insert_with(|| RunnableTasks {
- templates: Vec::new(),
- offset,
- column: task_buffer_range.start.column,
- extra_variables: HashMap::default(),
- context_range,
- })
- .templates
- .push((kind, task.original_task().clone()));
- }
-
- acc
- })
- }) else {
- return;
- };
-
- let Ok(prefer_lsp) = multi_buffer.update(cx, |buffer, cx| {
- buffer.language_settings(cx).tasks.prefer_lsp
- }) else {
- return;
- };
-
- let rows = Self::runnable_rows(
- project,
- display_snapshot,
- prefer_lsp && !lsp_tasks_by_rows.is_empty(),
- new_rows,
- cx.clone(),
- )
- .await;
- editor
- .update(cx, |editor, _| {
- editor.clear_tasks();
- for (key, mut value) in rows {
- if let Some(lsp_tasks) = lsp_tasks_by_rows.remove(&key) {
- value.templates.extend(lsp_tasks.templates);
- }
-
- editor.insert_tasks(key, value);
- }
- for (key, value) in lsp_tasks_by_rows {
- editor.insert_tasks(key, value);
- }
- })
- .ok();
- })
- }
-
- fn runnable_rows(
- project: Entity<Project>,
- snapshot: DisplaySnapshot,
- prefer_lsp: bool,
- runnable_ranges: Vec<(Range<MultiBufferOffset>, language::RunnableRange)>,
- cx: AsyncWindowContext,
- ) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> {
- cx.spawn(async move |cx| {
- let mut runnable_rows = Vec::with_capacity(runnable_ranges.len());
- for (run_range, mut runnable) in runnable_ranges {
- let Some(tasks) = cx
- .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
- .ok()
- else {
- continue;
- };
- let mut tasks = tasks.await;
-
- if prefer_lsp {
- tasks.retain(|(task_kind, _)| {
- !matches!(task_kind, TaskSourceKind::Language { .. })
- });
- }
- if tasks.is_empty() {
- continue;
- }
-
- let point = run_range.start.to_point(&snapshot.buffer_snapshot());
- let Some(row) = snapshot
- .buffer_snapshot()
- .buffer_line_for_row(MultiBufferRow(point.row))
- .map(|(_, range)| range.start.row)
- else {
- continue;
- };
-
- let context_range =
- BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
- runnable_rows.push((
- (runnable.buffer_id, row),
- RunnableTasks {
- templates: tasks,
- offset: snapshot.buffer_snapshot().anchor_before(run_range.start),
- context_range,
- column: point.column,
- extra_variables: runnable.extra_captures,
- },
- ));
- }
- runnable_rows
- })
- }
-
- fn templates_with_tags(
- project: &Entity<Project>,
- runnable: &mut Runnable,
- cx: &mut App,
- ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
- let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
- let (worktree_id, file) = project
- .buffer_for_id(runnable.buffer, cx)
- .and_then(|buffer| buffer.read(cx).file())
- .map(|file| (file.worktree_id(cx), file.clone()))
- .unzip();
-
- (
- project.task_store().read(cx).task_inventory().cloned(),
- worktree_id,
- file,
- )
- });
-
- let tags = mem::take(&mut runnable.tags);
- let language = runnable.language.clone();
- cx.spawn(async move |cx| {
- let mut templates_with_tags = Vec::new();
- if let Some(inventory) = inventory {
- for RunnableTag(tag) in tags {
- let new_tasks = inventory.update(cx, |inventory, cx| {
- inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx)
- });
- templates_with_tags.extend(new_tasks.await.into_iter().filter(
- move |(_, template)| {
- template.tags.iter().any(|source_tag| source_tag == &tag)
- },
- ));
- }
- }
- templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned());
-
- if let Some((leading_tag_source, _)) = templates_with_tags.first() {
- // Strongest source wins; if we have worktree tag binding, prefer that to
- // global and language bindings;
- // if we have a global binding, prefer that to language binding.
- let first_mismatch = templates_with_tags
- .iter()
- .position(|(tag_source, _)| tag_source != leading_tag_source);
- if let Some(index) = first_mismatch {
- templates_with_tags.truncate(index);
- }
- }
-
- templates_with_tags
- })
- }
-
pub fn move_to_enclosing_bracket(
&mut self,
_: &MoveToEnclosingBracket,
@@ -24184,7 +23751,6 @@ impl Editor {
predecessor,
excerpts,
} => {
- self.tasks_update_task = Some(self.refresh_runnables(window, cx));
let buffer_id = buffer.read(cx).remote_id();
if self.buffer.read(cx).diff_for(buffer_id).is_none()
&& let Some(project) = &self.project
@@ -24202,6 +23768,7 @@ impl Editor {
.invalidate_buffer(&buffer.read(cx).remote_id());
self.update_lsp_data(Some(buffer_id), window, cx);
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+ self.refresh_runnables(window, cx);
self.colorize_brackets(false, cx);
self.refresh_selected_text_highlights(&self.display_snapshot(cx), true, window, cx);
cx.emit(EditorEvent::ExcerptsAdded {
@@ -24220,8 +23787,7 @@ impl Editor {
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
for buffer_id in removed_buffer_ids {
self.registered_buffers.remove(buffer_id);
- self.tasks
- .retain(|(task_buffer_id, _), _| task_buffer_id != buffer_id);
+ self.clear_runnables(Some(*buffer_id));
self.semantic_token_state.invalidate_buffer(buffer_id);
self.display_map.update(cx, |display_map, cx| {
display_map.invalidate_semantic_highlights(*buffer_id);
@@ -24263,10 +23829,12 @@ impl Editor {
}
self.colorize_brackets(false, cx);
self.update_lsp_data(None, window, cx);
+ self.refresh_runnables(window, cx);
cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() })
}
multi_buffer::Event::Reparsed(buffer_id) => {
- self.tasks_update_task = Some(self.refresh_runnables(window, cx));
+ self.clear_runnables(Some(*buffer_id));
+ self.refresh_runnables(window, cx);
self.refresh_selected_text_highlights(&self.display_snapshot(cx), true, window, cx);
self.colorize_brackets(true, cx);
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
@@ -24274,7 +23842,7 @@ impl Editor {
cx.emit(EditorEvent::Reparsed(*buffer_id));
}
multi_buffer::Event::DiffHunksToggled => {
- self.tasks_update_task = Some(self.refresh_runnables(window, cx));
+ self.refresh_runnables(window, cx);
}
multi_buffer::Event::LanguageChanged(buffer_id, is_fresh_language) => {
if !is_fresh_language {
@@ -24410,7 +23978,7 @@ impl Editor {
.unwrap_or(DiagnosticSeverity::Hint);
self.set_max_diagnostics_severity(new_severity, cx);
}
- self.tasks_update_task = Some(self.refresh_runnables(window, cx));
+ self.refresh_runnables(window, cx);
self.update_edit_prediction_settings(cx);
self.refresh_edit_prediction(true, false, window, cx);
self.refresh_inline_values(cx);
@@ -5,6 +5,7 @@ use crate::{
edit_prediction_tests::FakeEditPredictionDelegate,
element::StickyHeader,
linked_editing_ranges::LinkedEditingRanges,
+ runnables::RunnableTasks,
scroll::scroll_amount::ScrollAmount,
test::{
assert_text_with_selections, build_editor, editor_content_with_blocks,
@@ -24403,20 +24404,24 @@ async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
editor.update_in(cx, |editor, window, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
- editor.tasks.insert(
- (buffer.read(cx).remote_id(), 3),
+ editor.runnables.insert(
+ buffer.read(cx).remote_id(),
+ 3,
+ buffer.read(cx).version(),
RunnableTasks {
- templates: vec![],
+ templates: Vec::new(),
offset: snapshot.anchor_before(MultiBufferOffset(43)),
column: 0,
extra_variables: HashMap::default(),
context_range: BufferOffset(43)..BufferOffset(85),
},
);
- editor.tasks.insert(
- (buffer.read(cx).remote_id(), 8),
+ editor.runnables.insert(
+ buffer.read(cx).remote_id(),
+ 8,
+ buffer.read(cx).version(),
RunnableTasks {
- templates: vec![],
+ templates: Vec::new(),
offset: snapshot.anchor_before(MultiBufferOffset(86)),
column: 0,
extra_variables: HashMap::default(),
@@ -3275,9 +3275,9 @@ impl EditorElement {
snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
editor
- .tasks
- .iter()
- .filter_map(|(_, tasks)| {
+ .runnables
+ .all_runnables()
+ .filter_map(|tasks| {
let multibuffer_point = tasks.offset.to_point(&snapshot.buffer_snapshot());
if multibuffer_point < offset_range_start
|| multibuffer_point > offset_range_end
@@ -0,0 +1,915 @@
+use std::{collections::BTreeMap, mem, ops::Range, sync::Arc};
+
+use clock::Global;
+use collections::HashMap;
+use gpui::{
+ App, AppContext as _, AsyncWindowContext, ClickEvent, Context, Entity, Focusable as _,
+ MouseButton, Task, Window,
+};
+use language::{Buffer, BufferRow, Runnable};
+use lsp::LanguageServerName;
+use multi_buffer::{
+ Anchor, BufferOffset, MultiBufferOffset, MultiBufferRow, MultiBufferSnapshot, ToPoint as _,
+};
+use project::{
+ Location, Project, TaskSourceKind,
+ debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
+ project_settings::ProjectSettings,
+};
+use settings::Settings as _;
+use smallvec::SmallVec;
+use task::{ResolvedTask, RunnableTag, TaskContext, TaskTemplate, TaskVariables, VariableName};
+use text::{BufferId, OffsetRangeExt as _, ToOffset as _, ToPoint as _};
+use ui::{Clickable as _, Color, IconButton, IconSize, Toggleable as _};
+
+use crate::{
+ CodeActionSource, Editor, EditorSettings, EditorStyle, RangeToAnchorExt, SpawnNearestTask,
+ ToggleCodeActions, UPDATE_DEBOUNCE, display_map::DisplayRow,
+};
+
+#[derive(Debug)]
+pub(super) struct RunnableData {
+ runnables: HashMap<BufferId, (Global, BTreeMap<BufferRow, RunnableTasks>)>,
+ runnables_update_task: Task<()>,
+}
+
+impl RunnableData {
+ pub fn new() -> Self {
+ Self {
+ runnables: HashMap::default(),
+ runnables_update_task: Task::ready(()),
+ }
+ }
+
+ pub fn runnables(
+ &self,
+ (buffer_id, buffer_row): (BufferId, BufferRow),
+ ) -> Option<&RunnableTasks> {
+ self.runnables.get(&buffer_id)?.1.get(&buffer_row)
+ }
+
+ pub fn all_runnables(&self) -> impl Iterator<Item = &RunnableTasks> {
+ self.runnables
+ .values()
+ .flat_map(|(_, tasks)| tasks.values())
+ }
+
+ pub fn has_cached(&self, buffer_id: BufferId, version: &Global) -> bool {
+ self.runnables
+ .get(&buffer_id)
+ .is_some_and(|(cached_version, _)| !version.changed_since(cached_version))
+ }
+
+ #[cfg(test)]
+ pub fn insert(
+ &mut self,
+ buffer_id: BufferId,
+ buffer_row: BufferRow,
+ version: Global,
+ tasks: RunnableTasks,
+ ) {
+ self.runnables
+ .entry(buffer_id)
+ .or_insert_with(|| (version, BTreeMap::default()))
+ .1
+ .insert(buffer_row, tasks);
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct RunnableTasks {
+ pub templates: Vec<(TaskSourceKind, TaskTemplate)>,
+ pub offset: multi_buffer::Anchor,
+ // We need the column at which the task context evaluation should take place (when we're spawning it via gutter).
+ pub column: u32,
+ // Values of all named captures, including those starting with '_'
+ pub extra_variables: HashMap<String, String>,
+ // Full range of the tagged region. We use it to determine which `extra_variables` to grab for context resolution in e.g. a modal.
+ pub context_range: Range<BufferOffset>,
+}
+
+impl RunnableTasks {
+ pub fn resolve<'a>(
+ &'a self,
+ cx: &'a task::TaskContext,
+ ) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a {
+ self.templates.iter().filter_map(|(kind, template)| {
+ template
+ .resolve_task(&kind.to_id_base(), cx)
+ .map(|task| (kind.clone(), task))
+ })
+ }
+}
+
+#[derive(Clone)]
+pub struct ResolvedTasks {
+ pub templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
+ pub position: Anchor,
+}
+
+impl Editor {
+ pub fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ if !self.mode().is_full()
+ || !EditorSettings::get_global(cx).gutter.runnables
+ || !self.enable_runnables
+ {
+ self.clear_runnables(None);
+ return;
+ }
+ if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+ if self
+ .runnables
+ .has_cached(buffer.read(cx).remote_id(), &buffer.read(cx).version())
+ {
+ return;
+ }
+ }
+
+ let project = self.project().map(Entity::downgrade);
+ let lsp_task_sources = self.lsp_task_sources(true, true, cx);
+ let multi_buffer = self.buffer.downgrade();
+ self.runnables.runnables_update_task = cx.spawn_in(window, async move |editor, cx| {
+ cx.background_executor().timer(UPDATE_DEBOUNCE).await;
+ let Some(project) = project.and_then(|p| p.upgrade()) else {
+ return;
+ };
+
+ let hide_runnables = project.update(cx, |project, _| project.is_via_collab());
+ if hide_runnables {
+ return;
+ }
+ let lsp_tasks = if lsp_task_sources.is_empty() {
+ Vec::new()
+ } else {
+ let Ok(lsp_tasks) = cx
+ .update(|_, cx| crate::lsp_tasks(project.clone(), &lsp_task_sources, None, cx))
+ else {
+ return;
+ };
+ lsp_tasks.await
+ };
+ let new_rows = {
+ let Some((multi_buffer_snapshot, multi_buffer_query_range)) = editor
+ .update(cx, |editor, cx| {
+ let multi_buffer = editor.buffer().read(cx);
+ if multi_buffer.is_singleton() {
+ Some((multi_buffer.snapshot(cx), Anchor::min()..Anchor::max()))
+ } else {
+ let display_snapshot =
+ editor.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let multi_buffer_query_range =
+ editor.multi_buffer_visible_range(&display_snapshot, cx);
+ let multi_buffer_snapshot = display_snapshot.buffer();
+ Some((
+ multi_buffer_snapshot.clone(),
+ multi_buffer_query_range.to_anchors(&multi_buffer_snapshot),
+ ))
+ }
+ })
+ .ok()
+ .flatten()
+ else {
+ return;
+ };
+ cx.background_spawn({
+ async move {
+ multi_buffer_snapshot
+ .runnable_ranges(multi_buffer_query_range)
+ .collect()
+ }
+ })
+ .await
+ };
+
+ let Ok(multi_buffer_snapshot) =
+ editor.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
+ else {
+ return;
+ };
+ let Ok(mut lsp_tasks_by_rows) = cx.update(|_, cx| {
+ lsp_tasks
+ .into_iter()
+ .flat_map(|(kind, tasks)| {
+ tasks.into_iter().filter_map(move |(location, task)| {
+ Some((kind.clone(), location?, task))
+ })
+ })
+ .fold(HashMap::default(), |mut acc, (kind, location, task)| {
+ let buffer = location.target.buffer;
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ let offset = multi_buffer_snapshot.excerpts().find_map(
+ |(excerpt_id, snapshot, _)| {
+ if snapshot.remote_id() == buffer_snapshot.remote_id() {
+ multi_buffer_snapshot
+ .anchor_in_excerpt(excerpt_id, location.target.range.start)
+ } else {
+ None
+ }
+ },
+ );
+ if let Some(offset) = offset {
+ let task_buffer_range =
+ location.target.range.to_point(&buffer_snapshot);
+ let context_buffer_range =
+ task_buffer_range.to_offset(&buffer_snapshot);
+ let context_range = BufferOffset(context_buffer_range.start)
+ ..BufferOffset(context_buffer_range.end);
+
+ acc.entry((buffer_snapshot.remote_id(), task_buffer_range.start.row))
+ .or_insert_with(|| RunnableTasks {
+ templates: Vec::new(),
+ offset,
+ column: task_buffer_range.start.column,
+ extra_variables: HashMap::default(),
+ context_range,
+ })
+ .templates
+ .push((kind, task.original_task().clone()));
+ }
+
+ acc
+ })
+ }) else {
+ return;
+ };
+
+ let Ok(prefer_lsp) = multi_buffer.update(cx, |buffer, cx| {
+ buffer.language_settings(cx).tasks.prefer_lsp
+ }) else {
+ return;
+ };
+
+ let rows = Self::runnable_rows(
+ project,
+ multi_buffer_snapshot,
+ prefer_lsp && !lsp_tasks_by_rows.is_empty(),
+ new_rows,
+ cx.clone(),
+ )
+ .await;
+ editor
+ .update(cx, |editor, cx| {
+ for ((buffer_id, row), mut new_tasks) in rows {
+ let Some(buffer) = editor.buffer().read(cx).buffer(buffer_id) else {
+ continue;
+ };
+
+ if let Some(lsp_tasks) = lsp_tasks_by_rows.remove(&(buffer_id, row)) {
+ new_tasks.templates.extend(lsp_tasks.templates);
+ }
+ editor.insert_runnables(
+ buffer_id,
+ buffer.read(cx).version(),
+ row,
+ new_tasks,
+ );
+ }
+ for ((buffer_id, row), new_tasks) in lsp_tasks_by_rows {
+ let Some(buffer) = editor.buffer().read(cx).buffer(buffer_id) else {
+ continue;
+ };
+ editor.insert_runnables(
+ buffer_id,
+ buffer.read(cx).version(),
+ row,
+ new_tasks,
+ );
+ }
+ })
+ .ok();
+ });
+ }
+
+ pub fn spawn_nearest_task(
+ &mut self,
+ action: &SpawnNearestTask,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some((workspace, _)) = self.workspace.clone() else {
+ return;
+ };
+ let Some(project) = self.project.clone() else {
+ return;
+ };
+
+ // Try to find a closest, enclosing node using tree-sitter that has a task
+ let Some((buffer, buffer_row, tasks)) = self
+ .find_enclosing_node_task(cx)
+ // Or find the task that's closest in row-distance.
+ .or_else(|| self.find_closest_task(cx))
+ else {
+ return;
+ };
+
+ let reveal_strategy = action.reveal;
+ let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
+ cx.spawn_in(window, async move |_, cx| {
+ let context = task_context.await?;
+ let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?;
+
+ let resolved = &mut resolved_task.resolved;
+ resolved.reveal = reveal_strategy;
+
+ workspace
+ .update_in(cx, |workspace, window, cx| {
+ workspace.schedule_resolved_task(
+ task_source_kind,
+ resolved_task,
+ false,
+ window,
+ cx,
+ );
+ })
+ .ok()
+ })
+ .detach();
+ }
+
+ pub fn clear_runnables(&mut self, for_buffer: Option<BufferId>) {
+ if let Some(buffer_id) = for_buffer {
+ self.runnables.runnables.remove(&buffer_id);
+ } else {
+ self.runnables.runnables.clear();
+ }
+ self.runnables.runnables_update_task = Task::ready(());
+ }
+
+ pub fn task_context(&self, window: &mut Window, cx: &mut App) -> Task<Option<TaskContext>> {
+ let Some(project) = self.project.clone() else {
+ return Task::ready(None);
+ };
+ let (selection, buffer, editor_snapshot) = {
+ let selection = self.selections.newest_adjusted(&self.display_snapshot(cx));
+ let Some((buffer, _)) = self
+ .buffer()
+ .read(cx)
+ .point_to_buffer_offset(selection.start, cx)
+ else {
+ return Task::ready(None);
+ };
+ let snapshot = self.snapshot(window, cx);
+ (selection, buffer, snapshot)
+ };
+ let selection_range = selection.range();
+ let start = editor_snapshot
+ .display_snapshot
+ .buffer_snapshot()
+ .anchor_after(selection_range.start)
+ .text_anchor;
+ let end = editor_snapshot
+ .display_snapshot
+ .buffer_snapshot()
+ .anchor_after(selection_range.end)
+ .text_anchor;
+ let location = Location {
+ buffer,
+ range: start..end,
+ };
+ let captured_variables = {
+ let mut variables = TaskVariables::default();
+ let buffer = location.buffer.read(cx);
+ let buffer_id = buffer.remote_id();
+ let snapshot = buffer.snapshot();
+ let starting_point = location.range.start.to_point(&snapshot);
+ let starting_offset = starting_point.to_offset(&snapshot);
+ for (_, tasks) in self
+ .runnables
+ .runnables
+ .get(&buffer_id)
+ .into_iter()
+ .flat_map(|(_, tasks)| tasks.range(0..starting_point.row + 1))
+ {
+ if !tasks
+ .context_range
+ .contains(&crate::BufferOffset(starting_offset))
+ {
+ continue;
+ }
+ for (capture_name, value) in tasks.extra_variables.iter() {
+ variables.insert(
+ VariableName::Custom(capture_name.to_owned().into()),
+ value.clone(),
+ );
+ }
+ }
+ variables
+ };
+
+ project.update(cx, |project, cx| {
+ project.task_store().update(cx, |task_store, cx| {
+ task_store.task_context_for_location(captured_variables, location, cx)
+ })
+ })
+ }
+
+ pub fn lsp_task_sources(
+ &self,
+ visible_only: bool,
+ skip_cached: bool,
+ cx: &mut Context<Self>,
+ ) -> HashMap<LanguageServerName, Vec<BufferId>> {
+ if !self.lsp_data_enabled() {
+ return HashMap::default();
+ }
+ let buffers = if visible_only {
+ self.visible_excerpts(true, cx)
+ .into_values()
+ .map(|(buffer, _, _)| buffer)
+ .collect()
+ } else {
+ self.buffer().read(cx).all_buffers()
+ };
+
+ let lsp_settings = &ProjectSettings::get_global(cx).lsp;
+
+ buffers
+ .into_iter()
+ .filter_map(|buffer| {
+ let lsp_tasks_source = buffer
+ .read(cx)
+ .language()?
+ .context_provider()?
+ .lsp_task_source()?;
+ if lsp_settings
+ .get(&lsp_tasks_source)
+ .is_none_or(|s| s.enable_lsp_tasks)
+ {
+ let buffer_id = buffer.read(cx).remote_id();
+ if skip_cached
+ && self
+ .runnables
+ .has_cached(buffer_id, &buffer.read(cx).version())
+ {
+ None
+ } else {
+ Some((lsp_tasks_source, buffer_id))
+ }
+ } else {
+ None
+ }
+ })
+ .fold(
+ HashMap::default(),
+ |mut acc, (lsp_task_source, buffer_id)| {
+ acc.entry(lsp_task_source)
+ .or_insert_with(Vec::new)
+ .push(buffer_id);
+ acc
+ },
+ )
+ }
+
+ pub fn find_enclosing_node_task(
+ &mut self,
+ cx: &mut Context<Self>,
+ ) -> Option<(Entity<Buffer>, u32, Arc<RunnableTasks>)> {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let offset = self
+ .selections
+ .newest::<MultiBufferOffset>(&self.display_snapshot(cx))
+ .head();
+ let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
+ let offset = excerpt.map_offset_to_buffer(offset);
+ let buffer_id = excerpt.buffer().remote_id();
+
+ let layer = excerpt.buffer().syntax_layer_at(offset)?;
+ let mut cursor = layer.node().walk();
+
+ while cursor.goto_first_child_for_byte(offset.0).is_some() {
+ if cursor.node().end_byte() == offset.0 {
+ cursor.goto_next_sibling();
+ }
+ }
+
+ // Ascend to the smallest ancestor that contains the range and has a task.
+ loop {
+ let node = cursor.node();
+ let node_range = node.byte_range();
+ let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row;
+
+ // Check if this node contains our offset
+ if node_range.start <= offset.0 && node_range.end >= offset.0 {
+ // If it contains offset, check for task
+ if let Some(tasks) = self
+ .runnables
+ .runnables
+ .get(&buffer_id)
+ .and_then(|(_, tasks)| tasks.get(&symbol_start_row))
+ {
+ let buffer = self.buffer.read(cx).buffer(buffer_id)?;
+ return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned())));
+ }
+ }
+
+ if !cursor.goto_parent() {
+ break;
+ }
+ }
+ None
+ }
+
+ pub fn render_run_indicator(
+ &self,
+ _style: &EditorStyle,
+ is_active: bool,
+ row: DisplayRow,
+ breakpoint: Option<(Anchor, Breakpoint, Option<BreakpointSessionState>)>,
+ cx: &mut Context<Self>,
+ ) -> IconButton {
+ let color = Color::Muted;
+ let position = breakpoint.as_ref().map(|(anchor, _, _)| *anchor);
+
+ IconButton::new(
+ ("run_indicator", row.0 as usize),
+ ui::IconName::PlayOutlined,
+ )
+ .shape(ui::IconButtonShape::Square)
+ .icon_size(IconSize::XSmall)
+ .icon_color(color)
+ .toggle_state(is_active)
+ .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| {
+ let quick_launch = match e {
+ ClickEvent::Keyboard(_) => true,
+ ClickEvent::Mouse(e) => e.down.button == MouseButton::Left,
+ };
+
+ window.focus(&editor.focus_handle(cx), cx);
+ editor.toggle_code_actions(
+ &ToggleCodeActions {
+ deployed_from: Some(CodeActionSource::RunMenu(row)),
+ quick_launch,
+ },
+ window,
+ cx,
+ );
+ }))
+ .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
+ editor.set_breakpoint_context_menu(row, position, event.position(), window, cx);
+ }))
+ }
+
+ fn insert_runnables(
+ &mut self,
+ buffer: BufferId,
+ version: Global,
+ row: BufferRow,
+ new_tasks: RunnableTasks,
+ ) {
+ let (old_version, tasks) = self.runnables.runnables.entry(buffer).or_default();
+ if !old_version.changed_since(&version) {
+ *old_version = version;
+ tasks.insert(row, new_tasks);
+ }
+ }
+
+ fn runnable_rows(
+ project: Entity<Project>,
+ snapshot: MultiBufferSnapshot,
+ prefer_lsp: bool,
+ runnable_ranges: Vec<(Range<MultiBufferOffset>, language::RunnableRange)>,
+ cx: AsyncWindowContext,
+ ) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> {
+ cx.spawn(async move |cx| {
+ let mut runnable_rows = Vec::with_capacity(runnable_ranges.len());
+ for (run_range, mut runnable) in runnable_ranges {
+ let Some(tasks) = cx
+ .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
+ .ok()
+ else {
+ continue;
+ };
+ let mut tasks = tasks.await;
+
+ if prefer_lsp {
+ tasks.retain(|(task_kind, _)| {
+ !matches!(task_kind, TaskSourceKind::Language { .. })
+ });
+ }
+ if tasks.is_empty() {
+ continue;
+ }
+
+ let point = run_range.start.to_point(&snapshot);
+ let Some(row) = snapshot
+ .buffer_line_for_row(MultiBufferRow(point.row))
+ .map(|(_, range)| range.start.row)
+ else {
+ continue;
+ };
+
+ let context_range =
+ BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
+ runnable_rows.push((
+ (runnable.buffer_id, row),
+ RunnableTasks {
+ templates: tasks,
+ offset: snapshot.anchor_before(run_range.start),
+ context_range,
+ column: point.column,
+ extra_variables: runnable.extra_captures,
+ },
+ ));
+ }
+ runnable_rows
+ })
+ }
+
+ fn templates_with_tags(
+ project: &Entity<Project>,
+ runnable: &mut Runnable,
+ cx: &mut App,
+ ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
+ let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
+ let (worktree_id, file) = project
+ .buffer_for_id(runnable.buffer, cx)
+ .and_then(|buffer| buffer.read(cx).file())
+ .map(|file| (file.worktree_id(cx), file.clone()))
+ .unzip();
+
+ (
+ project.task_store().read(cx).task_inventory().cloned(),
+ worktree_id,
+ file,
+ )
+ });
+
+ let tags = mem::take(&mut runnable.tags);
+ let language = runnable.language.clone();
+ cx.spawn(async move |cx| {
+ let mut templates_with_tags = Vec::new();
+ if let Some(inventory) = inventory {
+ for RunnableTag(tag) in tags {
+ let new_tasks = inventory.update(cx, |inventory, cx| {
+ inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx)
+ });
+ templates_with_tags.extend(new_tasks.await.into_iter().filter(
+ move |(_, template)| {
+ template.tags.iter().any(|source_tag| source_tag == &tag)
+ },
+ ));
+ }
+ }
+ templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned());
+
+ if let Some((leading_tag_source, _)) = templates_with_tags.first() {
+ // Strongest source wins; if we have worktree tag binding, prefer that to
+ // global and language bindings;
+ // if we have a global binding, prefer that to language binding.
+ let first_mismatch = templates_with_tags
+ .iter()
+ .position(|(tag_source, _)| tag_source != leading_tag_source);
+ if let Some(index) = first_mismatch {
+ templates_with_tags.truncate(index);
+ }
+ }
+
+ templates_with_tags
+ })
+ }
+
+ fn find_closest_task(
+ &mut self,
+ cx: &mut Context<Self>,
+ ) -> Option<(Entity<Buffer>, u32, Arc<RunnableTasks>)> {
+ let cursor_row = self
+ .selections
+ .newest_adjusted(&self.display_snapshot(cx))
+ .head()
+ .row;
+
+ let ((buffer_id, row), tasks) = self
+ .runnables
+ .runnables
+ .iter()
+ .flat_map(|(buffer_id, (_, tasks))| {
+ tasks.iter().map(|(row, tasks)| ((*buffer_id, *row), tasks))
+ })
+ .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?;
+
+ let buffer = self.buffer.read(cx).buffer(buffer_id)?;
+ let tasks = Arc::new(tasks.to_owned());
+ Some((buffer, row, tasks))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{sync::Arc, time::Duration};
+
+ use gpui::{AppContext as _, Task, TestAppContext};
+ use indoc::indoc;
+ use language::ContextProvider;
+ use languages::rust_lang;
+ use multi_buffer::{MultiBuffer, PathKey};
+ use project::{FakeFs, Project};
+ use serde_json::json;
+ use task::{TaskTemplate, TaskTemplates};
+ use text::Point;
+ use util::path;
+
+ use crate::{
+ Editor, UPDATE_DEBOUNCE, editor_tests::init_test, scroll::scroll_amount::ScrollAmount,
+ };
+
+ struct TestRustContextProvider;
+
+ impl ContextProvider for TestRustContextProvider {
+ fn associated_tasks(
+ &self,
+ _: Option<Arc<dyn language::File>>,
+ _: &gpui::App,
+ ) -> Task<Option<TaskTemplates>> {
+ Task::ready(Some(TaskTemplates(vec![
+ TaskTemplate {
+ label: "Run main".into(),
+ command: "cargo".into(),
+ args: vec!["run".into()],
+ tags: vec!["rust-main".into()],
+ ..TaskTemplate::default()
+ },
+ TaskTemplate {
+ label: "Run test".into(),
+ command: "cargo".into(),
+ args: vec!["test".into()],
+ tags: vec!["rust-test".into()],
+ ..TaskTemplate::default()
+ },
+ ])))
+ }
+ }
+
+ fn rust_lang_with_task_context() -> Arc<language::Language> {
+ Arc::new(
+ Arc::try_unwrap(rust_lang())
+ .unwrap()
+ .with_context_provider(Some(Arc::new(TestRustContextProvider))),
+ )
+ }
+
+ fn collect_runnable_labels(
+ editor: &Editor,
+ ) -> Vec<(text::BufferId, language::BufferRow, Vec<String>)> {
+ let mut result = editor
+ .runnables
+ .runnables
+ .iter()
+ .flat_map(|(buffer_id, (_, tasks))| {
+ tasks.iter().map(move |(row, runnable_tasks)| {
+ let mut labels: Vec<String> = runnable_tasks
+ .templates
+ .iter()
+ .map(|(_, template)| template.label.clone())
+ .collect();
+ labels.sort();
+ (*buffer_id, *row, labels)
+ })
+ })
+ .collect::<Vec<_>>();
+ result.sort_by_key(|(id, row, _)| (*id, *row));
+ result
+ }
+
+ #[gpui::test]
+ async fn test_multi_buffer_runnables_on_scroll(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let padding_lines = 50;
+ let mut first_rs = String::from("fn main() {\n println!(\"hello\");\n}\n");
+ for _ in 0..padding_lines {
+ first_rs.push_str("//\n");
+ }
+ let test_one_row = 3 + padding_lines as u32 + 1;
+ first_rs.push_str("#[test]\nfn test_one() {\n assert!(true);\n}\n");
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/project"),
+ json!({
+ "first.rs": first_rs,
+ "second.rs": indoc! {"
+ #[test]
+ fn test_two() {
+ assert!(true);
+ }
+
+ #[test]
+ fn test_three() {
+ assert!(true);
+ }
+ "},
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang_with_task_context());
+
+ let buffer_1 = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer(path!("/project/first.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_2 = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer(path!("/project/second.rs"), cx)
+ })
+ .await
+ .unwrap();
+
+ let buffer_1_id = buffer_1.read_with(cx, |buffer, _| buffer.remote_id());
+ let buffer_2_id = buffer_2.read_with(cx, |buffer, _| buffer.remote_id());
+
+ let multi_buffer = cx.new(|cx| {
+ let mut multi_buffer = MultiBuffer::new(language::Capability::ReadWrite);
+ let end = buffer_1.read(cx).max_point();
+ multi_buffer.set_excerpts_for_path(
+ PathKey::sorted(0),
+ buffer_1.clone(),
+ [Point::new(0, 0)..end],
+ 0,
+ cx,
+ );
+ multi_buffer.set_excerpts_for_path(
+ PathKey::sorted(1),
+ buffer_2.clone(),
+ [Point::new(0, 0)..Point::new(8, 1)],
+ 0,
+ cx,
+ );
+ multi_buffer
+ });
+
+ let editor = cx.add_window(|window, cx| {
+ Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
+ });
+ cx.executor().advance_clock(Duration::from_millis(500));
+ cx.executor().run_until_parked();
+
+ // Clear stale data from startup events, then refresh.
+ // first.rs is long enough that second.rs is below the ~47-line viewport.
+ editor
+ .update(cx, |editor, window, cx| {
+ editor.clear_runnables(None);
+ editor.refresh_runnables(window, cx);
+ })
+ .unwrap();
+ cx.executor().advance_clock(UPDATE_DEBOUNCE);
+ cx.executor().run_until_parked();
+ assert_eq!(
+ editor
+ .update(cx, |editor, _, _| collect_runnable_labels(editor))
+ .unwrap(),
+ vec![(buffer_1_id, 0, vec!["Run main".to_string()])],
+ "Only fn main from first.rs should be visible before scrolling"
+ );
+
+ // Scroll down to bring second.rs excerpts into view.
+ editor
+ .update(cx, |editor, window, cx| {
+ editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx);
+ })
+ .unwrap();
+ cx.executor().advance_clock(Duration::from_millis(200));
+ cx.executor().run_until_parked();
+
+ let after_scroll = editor
+ .update(cx, |editor, _, _| collect_runnable_labels(editor))
+ .unwrap();
+ assert_eq!(
+ after_scroll,
+ vec![
+ (buffer_1_id, 0, vec!["Run main".to_string()]),
+ (buffer_1_id, test_one_row, vec!["Run test".to_string()]),
+ (buffer_2_id, 1, vec!["Run test".to_string()]),
+ (buffer_2_id, 6, vec!["Run test".to_string()]),
+ ],
+ "Tree-sitter should detect both #[test] fns in second.rs after scroll"
+ );
+
+ // Edit second.rs to invalidate its cache; first.rs data should persist.
+ buffer_2.update(cx, |buffer, cx| {
+ buffer.edit([(0..0, "// added comment\n")], None, cx);
+ });
+ editor
+ .update(cx, |editor, window, cx| {
+ editor.scroll_screen(&ScrollAmount::Page(-1.0), window, cx);
+ })
+ .unwrap();
+ cx.executor().advance_clock(Duration::from_millis(200));
+ cx.executor().run_until_parked();
+
+ assert_eq!(
+ editor
+ .update(cx, |editor, _, _| collect_runnable_labels(editor))
+ .unwrap(),
+ vec![
+ (buffer_1_id, 0, vec!["Run main".to_string()]),
+ (buffer_1_id, test_one_row, vec!["Run test".to_string()]),
+ ],
+ "first.rs runnables should survive an edit to second.rs"
+ );
+ }
+}
@@ -1,110 +0,0 @@
-use crate::Editor;
-
-use collections::HashMap;
-use gpui::{App, Task, Window};
-use lsp::LanguageServerName;
-use project::{Location, project_settings::ProjectSettings};
-use settings::Settings as _;
-use task::{TaskContext, TaskVariables, VariableName};
-use text::{BufferId, ToOffset, ToPoint};
-
-impl Editor {
- pub fn task_context(&self, window: &mut Window, cx: &mut App) -> Task<Option<TaskContext>> {
- let Some(project) = self.project.clone() else {
- return Task::ready(None);
- };
- let (selection, buffer, editor_snapshot) = {
- let selection = self.selections.newest_adjusted(&self.display_snapshot(cx));
- let Some((buffer, _)) = self
- .buffer()
- .read(cx)
- .point_to_buffer_offset(selection.start, cx)
- else {
- return Task::ready(None);
- };
- let snapshot = self.snapshot(window, cx);
- (selection, buffer, snapshot)
- };
- let selection_range = selection.range();
- let start = editor_snapshot
- .display_snapshot
- .buffer_snapshot()
- .anchor_after(selection_range.start)
- .text_anchor;
- let end = editor_snapshot
- .display_snapshot
- .buffer_snapshot()
- .anchor_after(selection_range.end)
- .text_anchor;
- let location = Location {
- buffer,
- range: start..end,
- };
- let captured_variables = {
- let mut variables = TaskVariables::default();
- let buffer = location.buffer.read(cx);
- let buffer_id = buffer.remote_id();
- let snapshot = buffer.snapshot();
- let starting_point = location.range.start.to_point(&snapshot);
- let starting_offset = starting_point.to_offset(&snapshot);
- for (_, tasks) in self
- .tasks
- .range((buffer_id, 0)..(buffer_id, starting_point.row + 1))
- {
- if !tasks
- .context_range
- .contains(&crate::BufferOffset(starting_offset))
- {
- continue;
- }
- for (capture_name, value) in tasks.extra_variables.iter() {
- variables.insert(
- VariableName::Custom(capture_name.to_owned().into()),
- value.clone(),
- );
- }
- }
- variables
- };
-
- project.update(cx, |project, cx| {
- project.task_store().update(cx, |task_store, cx| {
- task_store.task_context_for_location(captured_variables, location, cx)
- })
- })
- }
-
- pub fn lsp_task_sources(&self, cx: &App) -> HashMap<LanguageServerName, Vec<BufferId>> {
- let lsp_settings = &ProjectSettings::get_global(cx).lsp;
-
- self.buffer()
- .read(cx)
- .all_buffers()
- .into_iter()
- .filter_map(|buffer| {
- let lsp_tasks_source = buffer
- .read(cx)
- .language()?
- .context_provider()?
- .lsp_task_source()?;
- if lsp_settings
- .get(&lsp_tasks_source)
- .is_none_or(|s| s.enable_lsp_tasks)
- {
- let buffer_id = buffer.read(cx).remote_id();
- Some((lsp_tasks_source, buffer_id))
- } else {
- None
- }
- })
- .fold(
- HashMap::default(),
- |mut acc, (lsp_task_source, buffer_id)| {
- acc.entry(lsp_task_source)
- .or_insert_with(Vec::new)
- .push(buffer_id);
- acc
- },
- )
- }
-}
@@ -316,7 +316,9 @@ pub fn task_contexts(
let lsp_task_sources = active_editor
.as_ref()
- .map(|active_editor| active_editor.update(cx, |editor, cx| editor.lsp_task_sources(cx)))
+ .map(|active_editor| {
+ active_editor.update(cx, |editor, cx| editor.lsp_task_sources(false, false, cx))
+ })
.unwrap_or_default();
let latest_selection = active_editor.as_ref().map(|active_editor| {