Merge Zed task context providing logic (#10544)

Before, `tasks_ui` set most of the context with `SymbolContextProvider`
providing the symbol data part of the context. Now, there's a
`BasicContextProvider` that forms all standard Zed context and it
automatically serves as a base, with no need for other providers like
`RustContextProvider` to call it as before.

Also, stop adding `SelectedText` task variable into the context for
blank text selection.

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2024-04-15 10:52:15 +02:00 committed by GitHub
parent 97c5cffbe3
commit 573ba83034
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 173 additions and 124 deletions

View file

@ -56,7 +56,7 @@ use std::{
}, },
}; };
use syntax_map::SyntaxSnapshot; use syntax_map::SyntaxSnapshot;
pub use task_context::{ContextProvider, ContextProviderWithTasks, SymbolContextProvider}; pub use task_context::{BasicContextProvider, ContextProvider, ContextProviderWithTasks};
use theme::SyntaxTheme; use theme::SyntaxTheme;
use tree_sitter::{self, wasmtime, Query, WasmStore}; use tree_sitter::{self, wasmtime, Query, WasmStore};
use util::http::HttpClient; use util::http::HttpClient;

View file

@ -1,34 +1,56 @@
use std::path::Path;
use crate::Location; use crate::Location;
use anyhow::Result; use anyhow::Result;
use gpui::AppContext; use gpui::AppContext;
use task::{TaskTemplates, TaskVariables, VariableName}; use task::{TaskTemplates, TaskVariables, VariableName};
use text::{Point, ToPoint};
/// Language Contexts are used by Zed tasks to extract information about source file. /// Language Contexts are used by Zed tasks to extract information about the source file where the tasks are supposed to be scheduled from.
/// Multiple context providers may be used together: by default, Zed provides a base [`BasicContextProvider`] context that fills all non-custom [`VariableName`] variants.
///
/// The context will be used to fill data for the tasks, and filter out the ones that do not have the variables required.
pub trait ContextProvider: Send + Sync { pub trait ContextProvider: Send + Sync {
fn build_context(&self, _: Location, _: &mut AppContext) -> Result<TaskVariables> { /// Builds a specific context to be placed on top of the basic one (replacing all conflicting entries) and to be used for task resolving later.
fn build_context(
&self,
_: Option<&Path>,
_: &Location,
_: &mut AppContext,
) -> Result<TaskVariables> {
Ok(TaskVariables::default()) Ok(TaskVariables::default())
} }
/// Provides all tasks, associated with the current language.
fn associated_tasks(&self) -> Option<TaskTemplates> { fn associated_tasks(&self) -> Option<TaskTemplates> {
None None
} }
// Determines whether the [`BasicContextProvider`] variables should be filled too (if `false`), or omitted (if `true`).
fn is_basic(&self) -> bool {
false
}
} }
/// A context provider that finds out what symbol is currently focused in the buffer. /// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
pub struct SymbolContextProvider; /// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
pub struct BasicContextProvider;
impl ContextProvider for BasicContextProvider {
fn is_basic(&self) -> bool {
true
}
impl ContextProvider for SymbolContextProvider {
fn build_context( fn build_context(
&self, &self,
location: Location, worktree_abs_path: Option<&Path>,
location: &Location,
cx: &mut AppContext, cx: &mut AppContext,
) -> gpui::Result<TaskVariables> { ) -> Result<TaskVariables> {
let symbols = location let buffer = location.buffer.read(cx);
.buffer let buffer_snapshot = buffer.snapshot();
.read(cx) let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
.snapshot()
.symbols_containing(location.range.start, None);
let symbol = symbols.unwrap_or_default().last().map(|symbol| { let symbol = symbols.unwrap_or_default().last().map(|symbol| {
let range = symbol let range = symbol
.name_ranges .name_ranges
@ -37,9 +59,40 @@ impl ContextProvider for SymbolContextProvider {
.unwrap_or(0..symbol.text.len()); .unwrap_or(0..symbol.text.len());
symbol.text[range].to_string() symbol.text[range].to_string()
}); });
Ok(TaskVariables::from_iter(
Some(VariableName::Symbol).zip(symbol), let current_file = buffer
)) .file()
.and_then(|file| file.as_local())
.map(|file| file.abs_path(cx).to_string_lossy().to_string());
let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
let row = row + 1;
let column = column + 1;
let selected_text = buffer
.chars_for_range(location.range.clone())
.collect::<String>();
let mut task_variables = TaskVariables::from_iter([
(VariableName::Row, row.to_string()),
(VariableName::Column, column.to_string()),
]);
if let Some(symbol) = symbol {
task_variables.insert(VariableName::Symbol, symbol);
}
if !selected_text.trim().is_empty() {
task_variables.insert(VariableName::SelectedText, selected_text);
}
if let Some(path) = current_file {
task_variables.insert(VariableName::File, path);
}
if let Some(worktree_path) = worktree_abs_path {
task_variables.insert(
VariableName::WorktreeRoot,
worktree_path.to_string_lossy().to_string(),
);
}
Ok(task_variables)
} }
} }
@ -61,7 +114,12 @@ impl ContextProvider for ContextProviderWithTasks {
Some(self.templates.clone()) Some(self.templates.clone())
} }
fn build_context(&self, location: Location, cx: &mut AppContext) -> Result<TaskVariables> { fn build_context(
SymbolContextProvider.build_context(location, cx) &self,
worktree_abs_path: Option<&Path>,
location: &Location,
cx: &mut AppContext,
) -> Result<TaskVariables> {
BasicContextProvider.build_context(worktree_abs_path, location, cx)
} }
} }

View file

@ -87,7 +87,7 @@ pub fn init(
Ok(( Ok((
config.clone(), config.clone(),
load_queries($name), load_queries($name),
Some(Arc::new(language::SymbolContextProvider)), Some(Arc::new(language::BasicContextProvider)),
)) ))
}, },
); );
@ -107,7 +107,7 @@ pub fn init(
Ok(( Ok((
config.clone(), config.clone(),
load_queries($name), load_queries($name),
Some(Arc::new(language::SymbolContextProvider)), Some(Arc::new(language::BasicContextProvider)),
)) ))
}, },
); );

View file

@ -331,25 +331,26 @@ const RUST_PACKAGE_TASK_VARIABLE: VariableName =
impl ContextProvider for RustContextProvider { impl ContextProvider for RustContextProvider {
fn build_context( fn build_context(
&self, &self,
location: Location, _: Option<&Path>,
location: &Location,
cx: &mut gpui::AppContext, cx: &mut gpui::AppContext,
) -> Result<TaskVariables> { ) -> Result<TaskVariables> {
let mut context = SymbolContextProvider.build_context(location.clone(), cx)?;
let local_abs_path = location let local_abs_path = location
.buffer .buffer
.read(cx) .read(cx)
.file() .file()
.and_then(|file| Some(file.as_local()?.abs_path(cx))); .and_then(|file| Some(file.as_local()?.abs_path(cx)));
if let Some(package_name) = local_abs_path Ok(
.as_deref() if let Some(package_name) = local_abs_path
.and_then(|local_abs_path| local_abs_path.parent()) .as_deref()
.and_then(human_readable_package_name) .and_then(|local_abs_path| local_abs_path.parent())
{ .and_then(human_readable_package_name)
context.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name); {
} TaskVariables::from_iter(Some((RUST_PACKAGE_TASK_VARIABLE.clone(), package_name)))
} else {
Ok(context) TaskVariables::default()
},
)
} }
fn associated_tasks(&self) -> Option<TaskTemplates> { fn associated_tasks(&self) -> Option<TaskTemplates> {

View file

@ -1,12 +1,16 @@
use std::{path::PathBuf, sync::Arc}; use std::{
path::{Path, PathBuf},
sync::Arc,
};
use ::settings::Settings; use ::settings::Settings;
use anyhow::Context;
use editor::Editor; use editor::Editor;
use gpui::{AppContext, ViewContext, WindowContext}; use gpui::{AppContext, ViewContext, WindowContext};
use language::{Language, Point}; use language::{BasicContextProvider, ContextProvider, Language};
use modal::{Spawn, TasksModal}; use modal::{Spawn, TasksModal};
use project::{Location, TaskSourceKind, WorktreeId}; use project::{Location, TaskSourceKind, WorktreeId};
use task::{ResolvedTask, TaskContext, TaskTemplate, TaskVariables, VariableName}; use task::{ResolvedTask, TaskContext, TaskTemplate, TaskVariables};
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
@ -146,100 +150,88 @@ fn active_item_selection_properties(
} }
fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext { fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
let cwd = task_cwd(workspace, cx).log_err().flatten(); fn task_context_impl(workspace: &Workspace, cx: &mut WindowContext<'_>) -> Option<TaskContext> {
let current_editor = workspace let cwd = task_cwd(workspace, cx).log_err().flatten();
.active_item(cx) let editor = workspace
.and_then(|item| item.act_as::<Editor>(cx)); .active_item(cx)
if let Some(current_editor) = current_editor { .and_then(|item| item.act_as::<Editor>(cx))?;
(|| {
let editor = current_editor.read(cx); let (selection, buffer, editor_snapshot) = editor.update(cx, |editor, cx| {
let selection = editor.selections.newest::<usize>(cx); let selection = editor.selections.newest::<usize>(cx);
let (buffer, _, _) = editor let (buffer, _, _) = editor
.buffer() .buffer()
.read(cx) .read(cx)
.point_to_buffer_offset(selection.start, cx)?; .point_to_buffer_offset(selection.start, cx)?;
let snapshot = editor.snapshot(cx);
Some((selection, buffer, snapshot))
})?;
let language_context_provider = buffer
.read(cx)
.language()
.and_then(|language| language.context_provider())?;
current_editor.update(cx, |editor, cx| { let selection_range = selection.range();
let snapshot = editor.snapshot(cx); let start = editor_snapshot
let selection_range = selection.range(); .display_snapshot
let start = snapshot .buffer_snapshot
.display_snapshot .anchor_after(selection_range.start)
.buffer_snapshot .text_anchor;
.anchor_after(selection_range.start) let end = editor_snapshot
.text_anchor; .display_snapshot
let end = snapshot .buffer_snapshot
.display_snapshot .anchor_after(selection_range.end)
.buffer_snapshot .text_anchor;
.anchor_after(selection_range.end) let worktree_abs_path = buffer
.text_anchor; .read(cx)
let Point { row, column } = snapshot .file()
.display_snapshot .map(|file| WorktreeId::from_usize(file.worktree_id()))
.buffer_snapshot .and_then(|worktree_id| {
.offset_to_point(selection_range.start); workspace
let row = row + 1; .project()
let column = column + 1;
let location = Location {
buffer: buffer.clone(),
range: start..end,
};
let current_file = location
.buffer
.read(cx) .read(cx)
.file() .worktree_for_id(worktree_id, cx)
.and_then(|file| file.as_local()) .map(|worktree| worktree.read(cx).abs_path())
.map(|file| file.abs_path(cx).to_string_lossy().to_string()); });
let worktree_id = location let location = Location {
.buffer buffer,
.read(cx) range: start..end,
.file() };
.map(|file| WorktreeId::from_usize(file.worktree_id())); let task_variables = combine_task_variables(
let context = buffer worktree_abs_path.as_deref(),
.read(cx) location,
.language() language_context_provider.as_ref(),
.and_then(|language| language.context_provider()) cx,
.and_then(|provider| provider.build_context(location, cx).ok()); )
.log_err()?;
let worktree_path = worktree_id.and_then(|worktree_id| { Some(TaskContext {
workspace
.project()
.read(cx)
.worktree_for_id(worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string())
});
let selected_text = buffer.read(cx).chars_for_range(selection_range).collect();
let mut task_variables = TaskVariables::from_iter([
(VariableName::Row, row.to_string()),
(VariableName::Column, column.to_string()),
(VariableName::SelectedText, selected_text),
]);
if let Some(path) = current_file {
task_variables.insert(VariableName::File, path);
}
if let Some(worktree_path) = worktree_path {
task_variables.insert(VariableName::WorktreeRoot, worktree_path);
}
if let Some(language_context) = context {
task_variables.extend(language_context);
}
Some(TaskContext {
cwd: cwd.clone(),
task_variables,
})
})
})()
.unwrap_or_else(|| TaskContext {
cwd, cwd,
task_variables: Default::default(), task_variables,
}) })
}
task_context_impl(workspace, cx).unwrap_or_default()
}
fn combine_task_variables(
worktree_abs_path: Option<&Path>,
location: Location,
context_provider: &dyn ContextProvider,
cx: &mut WindowContext<'_>,
) -> anyhow::Result<TaskVariables> {
if context_provider.is_basic() {
context_provider
.build_context(worktree_abs_path, &location, cx)
.context("building basic provider context")
} else { } else {
TaskContext { let mut basic_context = BasicContextProvider
cwd, .build_context(worktree_abs_path, &location, cx)
task_variables: Default::default(), .context("building basic default context")?;
} basic_context.extend(
context_provider
.build_context(worktree_abs_path, &location, cx)
.context("building provider context ")?,
);
Ok(basic_context)
} }
} }
@ -325,7 +317,7 @@ mod tests {
use editor::Editor; use editor::Editor;
use gpui::{Entity, TestAppContext}; use gpui::{Entity, TestAppContext};
use language::{Language, LanguageConfig, SymbolContextProvider}; use language::{BasicContextProvider, Language, LanguageConfig};
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use serde_json::json; use serde_json::json;
use task::{TaskContext, TaskVariables, VariableName}; use task::{TaskContext, TaskVariables, VariableName};
@ -375,7 +367,7 @@ mod tests {
name: (_) @name) @item"#, name: (_) @name) @item"#,
) )
.unwrap() .unwrap()
.with_context_provider(Some(Arc::new(SymbolContextProvider))), .with_context_provider(Some(Arc::new(BasicContextProvider))),
); );
let typescript_language = Arc::new( let typescript_language = Arc::new(
@ -393,7 +385,7 @@ mod tests {
")" @context)) @item"#, ")" @context)) @item"#,
) )
.unwrap() .unwrap()
.with_context_provider(Some(Arc::new(SymbolContextProvider))), .with_context_provider(Some(Arc::new(BasicContextProvider))),
); );
let project = Project::test(fs, ["/dir".as_ref()], cx).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let worktree_id = project.update(cx, |project, cx| { let worktree_id = project.update(cx, |project, cx| {
@ -435,7 +427,6 @@ mod tests {
(VariableName::WorktreeRoot, "/dir".into()), (VariableName::WorktreeRoot, "/dir".into()),
(VariableName::Row, "1".into()), (VariableName::Row, "1".into()),
(VariableName::Column, "1".into()), (VariableName::Column, "1".into()),
(VariableName::SelectedText, "".into())
]) ])
} }
); );
@ -469,7 +460,6 @@ mod tests {
(VariableName::WorktreeRoot, "/dir".into()), (VariableName::WorktreeRoot, "/dir".into()),
(VariableName::Row, "1".into()), (VariableName::Row, "1".into()),
(VariableName::Column, "1".into()), (VariableName::Column, "1".into()),
(VariableName::SelectedText, "".into()),
(VariableName::Symbol, "this_is_a_test".into()), (VariableName::Symbol, "this_is_a_test".into()),
]) ])
} }