tasks: Provide task variables from matching runnable ranges in task modal (#12237)

In #12003 we found ourselves in need for precise region tracking in
which a given runnable has an effect in order to grab variables from it.
This PR makes it so that in task modal all task variables from queries
overlapping current cursor position.
However, in the process of working on that I've found that we cannot
always use a top-level capture to represent the full match range of
runnable (which has been my assumption up to this point). Tree-sitter
captures cannot capture sibling groups; we did just that in Rust
queries.

Thankfully, none of the extensions are affected as in them, a capture is
always attached to single node. This PR adds annotations to them
nonetheless; we'll be able to get rid of top-level captures in extension
runnables.scm once this PR is in stable version of Zed.


Release Notes:

- N/A
This commit is contained in:
Piotr Osiewicz 2024-05-24 21:00:23 +02:00 committed by GitHub
parent 08a3d3a0c2
commit 27229bba6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 295 additions and 183 deletions

View File

@ -406,10 +406,13 @@ impl Default for ScrollbarMarkerState {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct RunnableTasks { struct RunnableTasks {
templates: Vec<(TaskSourceKind, TaskTemplate)>, templates: Vec<(TaskSourceKind, TaskTemplate)>,
// We need the column at which the task context evaluation should take place. offset: MultiBufferOffset,
// We need the column at which the task context evaluation should take place (when we're spawning it via gutter).
column: u32, column: u32,
// Values of all named captures, including those starting with '_' // Values of all named captures, including those starting with '_'
extra_variables: HashMap<String, String>, 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>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -417,7 +420,10 @@ struct ResolvedTasks {
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>, templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
position: Anchor, position: Anchor,
} }
#[derive(Copy, Clone, Debug)]
struct MultiBufferOffset(usize);
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
struct BufferOffset(usize);
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`] /// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
/// ///
/// See the [module level documentation](self) for more information. /// See the [module level documentation](self) for more information.
@ -516,7 +522,7 @@ pub struct Editor {
>, >,
last_bounds: Option<Bounds<Pixels>>, last_bounds: Option<Bounds<Pixels>>,
expect_bounds_change: Option<Bounds<Pixels>>, expect_bounds_change: Option<Bounds<Pixels>>,
tasks: HashMap<(BufferId, BufferRow), (usize, RunnableTasks)>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
tasks_update_task: Option<Task<()>>, tasks_update_task: Option<Task<()>>,
} }
@ -4053,7 +4059,7 @@ impl Editor {
this.discard_inline_completion(false, cx); this.discard_inline_completion(false, cx);
let tasks = tasks.as_ref().zip(this.workspace.clone()).and_then( let tasks = tasks.as_ref().zip(this.workspace.clone()).and_then(
|(tasks, (workspace, _))| { |(tasks, (workspace, _))| {
let position = Point::new(buffer_row, tasks.1.column); let position = Point::new(buffer_row, tasks.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right); let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location { let location = Location {
buffer: buffer.clone(), buffer: buffer.clone(),
@ -4061,7 +4067,7 @@ impl Editor {
}; };
// Fill in the environmental variables from the tree-sitter captures // Fill in the environmental variables from the tree-sitter captures
let mut captured_task_variables = TaskVariables::default(); let mut captured_task_variables = TaskVariables::default();
for (capture_name, value) in tasks.1.extra_variables.clone() { for (capture_name, value) in tasks.extra_variables.clone() {
captured_task_variables.insert( captured_task_variables.insert(
task::VariableName::Custom(capture_name.into()), task::VariableName::Custom(capture_name.into()),
value.clone(), value.clone(),
@ -4082,7 +4088,6 @@ impl Editor {
.map(|task_context| { .map(|task_context| {
Arc::new(ResolvedTasks { Arc::new(ResolvedTasks {
templates: tasks templates: tasks
.1
.templates .templates
.iter() .iter()
.filter_map(|(kind, template)| { .filter_map(|(kind, template)| {
@ -4092,7 +4097,7 @@ impl Editor {
}) })
.collect(), .collect(),
position: snapshot.buffer_snapshot.anchor_before( position: snapshot.buffer_snapshot.anchor_before(
Point::new(multibuffer_point.row, tasks.1.column), Point::new(multibuffer_point.row, tasks.column),
), ),
}) })
}) })
@ -4693,7 +4698,7 @@ impl Editor {
self.tasks.clear() self.tasks.clear()
} }
fn insert_tasks(&mut self, key: (BufferId, BufferRow), value: (usize, RunnableTasks)) { fn insert_tasks(&mut self, key: (BufferId, BufferRow), value: RunnableTasks) {
if let Some(_) = self.tasks.insert(key, value) { if let Some(_) = self.tasks.insert(key, value) {
// This case should hopefully be rare, but just in case... // 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") log::error!("multiple different run targets found on a single line, only the last target will be rendered")
@ -7931,7 +7936,7 @@ impl Editor {
snapshot: DisplaySnapshot, snapshot: DisplaySnapshot,
runnable_ranges: Vec<RunnableRange>, runnable_ranges: Vec<RunnableRange>,
mut cx: AsyncWindowContext, mut cx: AsyncWindowContext,
) -> Vec<((BufferId, u32), (usize, RunnableTasks))> { ) -> Vec<((BufferId, u32), RunnableTasks)> {
runnable_ranges runnable_ranges
.into_iter() .into_iter()
.filter_map(|mut runnable| { .filter_map(|mut runnable| {
@ -7953,16 +7958,17 @@ impl Editor {
.start .start
.row; .row;
let context_range =
BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
Some(( Some((
(runnable.buffer_id, row), (runnable.buffer_id, row),
( RunnableTasks {
runnable.run_range.start, templates: tasks,
RunnableTasks { offset: MultiBufferOffset(runnable.run_range.start),
templates: tasks, context_range,
column: point.column, column: point.column,
extra_variables: runnable.extra_captures, extra_variables: runnable.extra_captures,
}, },
),
)) ))
}) })
.collect() .collect()

View File

@ -1535,8 +1535,8 @@ impl EditorElement {
editor editor
.tasks .tasks
.iter() .iter()
.filter_map(|(_, (multibuffer_offset, _))| { .filter_map(|(_, tasks)| {
let multibuffer_point = multibuffer_offset.to_point(&snapshot.buffer_snapshot); let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row); let multibuffer_row = MultiBufferRow(multibuffer_point.row);
if snapshot.is_line_folded(multibuffer_row) { if snapshot.is_line_folded(multibuffer_row) {
return None; return None;

View File

@ -5,7 +5,7 @@ use gpui::{Model, WindowContext};
use language::ContextProvider; use language::ContextProvider;
use project::{BasicContextProvider, Location, Project}; use project::{BasicContextProvider, Location, Project};
use task::{TaskContext, TaskVariables, VariableName}; use task::{TaskContext, TaskVariables, VariableName};
use text::Point; use text::{Point, ToOffset, ToPoint};
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
@ -70,14 +70,26 @@ fn task_context_with_editor(
}; };
let captured_variables = { let captured_variables = {
let mut variables = TaskVariables::default(); let mut variables = TaskVariables::default();
for range in location let buffer = location.buffer.read(cx);
.buffer let buffer_id = buffer.remote_id();
.read(cx) let snapshot = buffer.snapshot();
.snapshot() let starting_point = location.range.start.to_point(&snapshot);
.runnable_ranges(location.range.clone()) let starting_offset = starting_point.to_offset(&snapshot);
for (_, tasks) in editor
.tasks
.range((buffer_id, 0)..(buffer_id, starting_point.row + 1))
{ {
for (capture_name, value) in range.extra_captures { if !tasks
variables.insert(VariableName::Custom(capture_name.into()), value); .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 variables

View File

@ -14,7 +14,7 @@ use crate::{
SyntaxSnapshot, ToTreeSitterPoint, SyntaxSnapshot, ToTreeSitterPoint,
}, },
task_context::RunnableRange, task_context::RunnableRange,
LanguageScope, Outline, RunnableTag, LanguageScope, Outline, RunnableCapture, RunnableTag,
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
pub use clock::ReplicaId; pub use clock::ReplicaId;
@ -3061,41 +3061,76 @@ impl BufferSnapshot {
iter::from_fn(move || loop { iter::from_fn(move || loop {
let mat = syntax_matches.peek()?; let mat = syntax_matches.peek()?;
let test_range = test_configs[mat.grammar_index].and_then(|test_configs| { let test_range = test_configs[mat.grammar_index].and_then(|test_configs| {
let mut tags: SmallVec<[(Range<usize>, RunnableTag); 1]> = let mut run_range = None;
let full_range = mat.captures.iter().fold(
Range {
start: usize::MAX,
end: 0,
},
|mut acc, next| {
let byte_range = next.node.byte_range();
if acc.start > byte_range.start {
acc.start = byte_range.start;
}
if acc.end < byte_range.end {
acc.end = byte_range.end;
}
acc
},
);
if full_range.start > full_range.end {
// We did not find a full spanning range of this match.
return None;
}
let extra_captures: SmallVec<[_; 1]> =
SmallVec::from_iter(mat.captures.iter().filter_map(|capture| { SmallVec::from_iter(mat.captures.iter().filter_map(|capture| {
test_configs test_configs
.runnable_tags .extra_captures
.get(&capture.index) .get(capture.index as usize)
.cloned() .cloned()
.map(|tag_name| (capture.node.byte_range(), tag_name)) .and_then(|tag_name| match tag_name {
RunnableCapture::Named(name) => {
Some((capture.node.byte_range(), name))
}
RunnableCapture::Run => {
let _ = run_range.insert(capture.node.byte_range());
None
}
})
})); }));
let maximum_range = tags let run_range = run_range?;
let tags = test_configs
.query
.property_settings(mat.pattern_index)
.iter() .iter()
.max_by_key(|(byte_range, _)| byte_range.len()) .filter_map(|property| {
.map(|(range, _)| range)? if *property.key == *"tag" {
.clone(); property
tags.sort_by_key(|(range, _)| range == &maximum_range); .value
let split_point = tags.partition_point(|(range, _)| range != &maximum_range); .as_ref()
let (extra_captures, tags) = tags.split_at(split_point); .map(|value| RunnableTag(value.to_string().into()))
} else {
None
}
})
.collect();
let extra_captures = extra_captures let extra_captures = extra_captures
.into_iter() .into_iter()
.map(|(range, name)| { .map(|(range, name)| {
( (
name.0.to_string(), name.to_string(),
self.text_for_range(range.clone()).collect::<String>(), self.text_for_range(range.clone()).collect::<String>(),
) )
}) })
.collect(); .collect();
// All tags should have the same range.
Some(RunnableRange { Some(RunnableRange {
run_range: mat run_range,
.captures full_range,
.iter()
.find(|capture| capture.index == test_configs.run_capture_ix)
.map(|mat| mat.node.byte_range())?,
runnable: Runnable { runnable: Runnable {
tags: tags.into_iter().cloned().map(|(_, tag)| tag).collect(), tags,
language: mat.language, language: mat.language,
buffer: self.remote_id(), buffer: self.remote_id(),
}, },

View File

@ -25,7 +25,7 @@ use anyhow::{anyhow, Context, Result};
use async_trait::async_trait; use async_trait::async_trait;
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use futures::Future; use futures::Future;
use gpui::{AppContext, AsyncAppContext, Model, Task}; use gpui::{AppContext, AsyncAppContext, Model, SharedString, Task};
pub use highlight_map::HighlightMap; pub use highlight_map::HighlightMap;
use http::HttpClient; use http::HttpClient;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -882,12 +882,16 @@ struct RedactionConfig {
pub redaction_capture_ix: u32, pub redaction_capture_ix: u32,
} }
#[derive(Clone, Debug, PartialEq)]
enum RunnableCapture {
Named(SharedString),
Run,
}
struct RunnableConfig { struct RunnableConfig {
pub query: Query, pub query: Query,
/// A mapping from captures indices to known test tags /// A mapping from capture indice to capture kind
pub runnable_tags: HashMap<u32, RunnableTag>, pub extra_captures: Vec<RunnableCapture>,
/// index of the capture that corresponds to @run
pub run_capture_ix: u32,
} }
struct OverrideConfig { struct OverrideConfig {
@ -1009,23 +1013,21 @@ impl Language {
.ok_or_else(|| anyhow!("cannot mutate grammar"))?; .ok_or_else(|| anyhow!("cannot mutate grammar"))?;
let query = Query::new(&grammar.ts_language, source)?; let query = Query::new(&grammar.ts_language, source)?;
let mut run_capture_index = None; let mut extra_captures = Vec::with_capacity(query.capture_names().len());
let mut runnable_tags = HashMap::default();
for (ix, name) in query.capture_names().iter().enumerate() { for name in query.capture_names().iter() {
if *name == "run" { let kind = if *name == "run" {
run_capture_index = Some(ix as u32); RunnableCapture::Run
} else { } else {
runnable_tags.insert(ix as u32, RunnableTag(name.to_string().into())); RunnableCapture::Named(name.to_string().into())
} };
extra_captures.push(kind);
} }
if let Some(run_capture_ix) = run_capture_index { grammar.runnable_config = Some(RunnableConfig {
grammar.runnable_config = Some(RunnableConfig { extra_captures,
query, query,
run_capture_ix, });
runnable_tags,
});
}
Ok(self) Ok(self)
} }

View File

@ -11,6 +11,7 @@ use text::BufferId;
pub struct RunnableRange { pub struct RunnableRange {
pub buffer_id: BufferId, pub buffer_id: BufferId,
pub run_range: Range<usize>, pub run_range: Range<usize>,
pub full_range: Range<usize>,
pub runnable: Runnable, pub runnable: Runnable,
pub extra_captures: HashMap<String, String>, pub extra_captures: HashMap<String, String>,
} }

View File

@ -1,9 +1,15 @@
( (
(function_declaration name: (_) @run (
(#match? @run "^Test.*")) (function_declaration name: (_) @run
) @go-test (#match? @run "^Test.*"))
) @_
(#set! tag go-test)
)
( (
(function_declaration name: (_) @run (
(#eq? @run "main")) (function_declaration name: (_) @run
) @go-main (#eq? @run "main"))
) @_
(#set! tag go-main)
)

View File

@ -1,17 +1,27 @@
(mod_item
name: (_) @run
(#eq? @run "tests")
) @rust-mod-test
( (
(attribute_item (attribute (mod_item
[((identifier) @_attribute) name: (_) @run
(scoped_identifier (identifier) @_attribute) (#eq? @run "tests")
]) ) @rust-mod-test
(#eq? @_attribute "test")) (#set! tag rust-mod-test)
. )
(attribute_item) *
. (
(function_item (
name: (_) @run) (attribute_item (attribute
) @rust-test [((identifier) @_attribute)
(scoped_identifier (identifier) @_attribute)
])
(#eq? @_attribute "test")
) @start
.
(attribute_item) *
.
(function_item
name: (_) @run
body: _
) @end
)
(#set! tag rust-test)
)

View File

@ -1,9 +1,13 @@
; Functions with names ending in `_test`. ; Functions with names ending in `_test`.
; This matches the standalone test style used by Startest and Gleeunit. ; This matches the standalone test style used by Startest and Gleeunit.
( (
(function name: (_) @run (
(#match? @run ".*_test$")) (function name: (_) @run
) @gleam-test (#match? @run ".*_test$"))
) @gleam-test
(#set! tag gleam-test)
)
; `describe` API for Startest. ; `describe` API for Startest.
( (
@ -17,4 +21,5 @@
) )
) )
) )
(#set! tag gleam-test)
) @gleam-test ) @gleam-test

View File

@ -2,87 +2,101 @@
; and that doesn't have the abstract modifier ; and that doesn't have the abstract modifier
; and have a method that follow the naming convention of PHPUnit test methods ; and have a method that follow the naming convention of PHPUnit test methods
; and the method is public ; and the method is public
(class_declaration (
modifier: (_)? @_modifier (class_declaration
(#not-eq? @_modifier "abstract") modifier: (_)? @_modifier
name: (_) @_name (#not-eq? @_modifier "abstract")
(#match? @_name ".*Test$") name: (_) @_name
body: (declaration_list (#match? @_name ".*Test$")
(method_declaration body: (declaration_list
(visibility_modifier)? @_visibility (method_declaration
(#eq? @_visibility "public") (visibility_modifier)? @_visibility
name: (_) @run (#eq? @_visibility "public")
(#match? @run "^test.*") name: (_) @run
(#match? @run "^test.*")
)
) )
) ) @phpunit-test
) @phpunit-test (#set! tag phpunit-test)
)
; Class that follow the naming convention of PHPUnit test classes ; Class that follow the naming convention of PHPUnit test classes
; and that doesn't have the abstract modifier ; and that doesn't have the abstract modifier
; and have a method that has the @test annotation ; and have a method that has the @test annotation
; and the method is public ; and the method is public
(class_declaration (
modifier: (_)? @_modifier (class_declaration
(#not-eq? @_modifier "abstract") modifier: (_)? @_modifier
name: (_) @_name (#not-eq? @_modifier "abstract")
(#match? @_name ".*Test$") name: (_) @_name
body: (declaration_list (#match? @_name ".*Test$")
((comment) @_comment body: (declaration_list
(#match? @_comment ".*@test\\b.*") ((comment) @_comment
. (#match? @_comment ".*@test\\b.*")
(method_declaration .
(visibility_modifier)? @_visibility (method_declaration
(#eq? @_visibility "public") (visibility_modifier)? @_visibility
name: (_) @run (#eq? @_visibility "public")
(#not-match? @run "^test.*") name: (_) @run
)) (#not-match? @run "^test.*")
) ))
) @phpunit-test )
) @phpunit-test
(#set! tag phpunit-test)
)
; Class that follow the naming convention of PHPUnit test classes ; Class that follow the naming convention of PHPUnit test classes
; and that doesn't have the abstract modifier ; and that doesn't have the abstract modifier
; and have a method that has the #[Test] attribute ; and have a method that has the #[Test] attribute
; and the method is public ; and the method is public
(class_declaration (
modifier: (_)? @_modifier (class_declaration
(#not-eq? @_modifier "abstract") modifier: (_)? @_modifier
name: (_) @_name (#not-eq? @_modifier "abstract")
(#match? @_name ".*Test$") name: (_) @_name
body: (declaration_list (#match? @_name ".*Test$")
(method_declaration body: (declaration_list
(attribute_list (method_declaration
(attribute_group (attribute_list
(attribute (name) @_attribute) (attribute_group
(attribute (name) @_attribute)
)
) )
(#eq? @_attribute "Test")
(visibility_modifier)? @_visibility
(#eq? @_visibility "public")
name: (_) @run
(#not-match? @run "^test.*")
) )
(#eq? @_attribute "Test")
(visibility_modifier)? @_visibility
(#eq? @_visibility "public")
name: (_) @run
(#not-match? @run "^test.*")
) )
) ) @phpunit-test
) @phpunit-test (#set! tag phpunit-test)
)
; Class that follow the naming convention of PHPUnit test classes ; Class that follow the naming convention of PHPUnit test classes
; and that doesn't have the abstract modifier ; and that doesn't have the abstract modifier
(class_declaration (
modifier: (_)? @_modifier (class_declaration
(#not-eq? @_modifier "abstract") modifier: (_)? @_modifier
name: (_) @run (#not-eq? @_modifier "abstract")
(#match? @run ".*Test$") name: (_) @run
) @phpunit-test (#match? @run ".*Test$")
) @phpunit-test
(#set! tag phpunit-test)
)
; Add support for Pest runnable ; Add support for Pest runnable
; Function expression that has `it`, `test` or `describe` as the function name ; Function expression that has `it`, `test` or `describe` as the function name
(function_call_expression (
function: (_) @_name (function_call_expression
(#any-of? @_name "it" "test" "describe") function: (_) @_name
arguments: (arguments (#any-of? @_name "it" "test" "describe")
. arguments: (arguments
(argument .
(encapsed_string (string_value) @run) (argument
(encapsed_string (string_value) @run)
)
) )
) ) @pest-test
) @pest-test (#set! tag pest-test)
)

View File

@ -4,46 +4,67 @@
; Minitest ; Minitest
;; Rails unit tests ;; Rails unit tests
(class (
name: [ (class
(constant) @run name: [
(scope_resolution scope: (constant) name: (constant) @run) (constant) @run
] (scope_resolution scope: (constant) name: (constant) @run)
(superclass (scope_resolution) @superclass (#match? @superclass "(::IntegrationTest|::TestCase|::SystemTestCase)$")) ]
) @minitest-test (superclass (scope_resolution) @superclass (#match? @superclass "(::IntegrationTest|::TestCase|::SystemTestCase)$"))
) @minitest-test
(#set! tag minitest-test)
)
(call (
method: (identifier) @run (#eq? @run "test") (call
arguments: (argument_list (string (string_content) @name)) method: (identifier) @run (#eq? @run "test")
) @minitest-test arguments: (argument_list (string (string_content) @name))
) @minitest-test
(#set! tag minitest-test)
)
; Methods that begin with test_ ; Methods that begin with test_
(method (
name: (identifier) @run (#match? @run "^test_") (method
) @minitest-test name: (identifier) @run (#match? @run "^test_")
) @minitest-test
(#set! tag minitest-test)
)
; System tests that inherit from ApplicationSystemTestCase ; System tests that inherit from ApplicationSystemTestCase
(class (
name: (constant) @run (superclass) @superclass (#match? @superclass "(ApplicationSystemTestCase)$") (class
) @minitest-test name: (constant) @run (superclass) @superclass (#match? @superclass "(ApplicationSystemTestCase)$")
) @minitest-test
(#set! tag minitest-test)
)
; RSpec ; RSpec
; Example groups with literals ; Example groups with literals
(call (
method: (identifier) @run (#any-of? @run "describe" "context") (call
arguments: (argument_list . (_) @name) method: (identifier) @run (#any-of? @run "describe" "context")
) @rspec-test arguments: (argument_list . (_) @name)
) @rspec-test
(#set! tag rspec-test)
)
; Examples ; Examples
(call (
method: (identifier) @run (#any-of? @run "it" "its" "specify") (call
arguments: (argument_list (string (string_content) @name)) method: (identifier) @run (#any-of? @run "it" "its" "specify")
) @rspec-test arguments: (argument_list (string (string_content) @name))
) @rspec-test
(#set! tag rspec-test)
)
; Examples (one-liner syntax) ; Examples (one-liner syntax)
(call (
method: (identifier) @run (#any-of? @run "it" "its" "specify") (call
block: (_) @name method: (identifier) @run (#any-of? @run "it" "its" "specify")
!arguments block: (_) @name
) @rspec-test !arguments
) @rspec-test
(#set! tag rspec-test)
)