Compare commits

...

1 Commits

Author SHA1 Message Date
Piotr Osiewicz
d895f53337 tasks: Provide environment variable autocomplete for tasks modal
Fixes #12099
2024-05-23 16:00:33 +02:00
7 changed files with 192 additions and 9 deletions

2
Cargo.lock generated
View File

@@ -10153,6 +10153,7 @@ dependencies = [
"gpui",
"language",
"menu",
"parking_lot",
"picker",
"project",
"schemars",
@@ -10160,6 +10161,7 @@ dependencies = [
"serde_json",
"settings",
"task",
"text",
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use editor::{scroll::Autoscroll, Editor};
use editor::{scroll::Autoscroll, CompletionProvider, Editor};
use gpui::{
actions, div, impl_actions, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent,
DismissEvent, EventEmitter, FocusHandle, FocusableView, Length, ListState, MouseButton,
@@ -166,6 +166,17 @@ impl<D: PickerDelegate> Picker<D> {
Self::new(delegate, ContainerKind::List, head, cx)
}
/// Adds a completion provider for this pickers query editor, if it has one.
pub fn with_completions_provider(
self,
provider: Box<dyn CompletionProvider>,
cx: &mut WindowContext<'_>,
) -> Self {
if let Head::Editor(editor) = &self.head {
editor.update(cx, |this, _| this.set_completion_provider(provider))
}
self
}
fn new(delegate: D, container: ContainerKind, head: Head, cx: &mut ViewContext<Self>) -> Self {
let mut this = Self {
delegate,

View File

@@ -5,10 +5,11 @@ pub mod static_source;
mod task_template;
mod vscode_format;
use collections::{HashMap, HashSet};
use collections::{BTreeMap, HashMap, HashSet};
use gpui::SharedString;
use serde::Serialize;
use std::path::PathBuf;
use std::str::FromStr;
use std::{borrow::Cow, path::Path};
pub use task_template::{RevealStrategy, TaskTemplate, TaskTemplates};
@@ -120,7 +121,7 @@ impl ResolvedTask {
}
/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub enum VariableName {
/// An absolute path of the currently opened file.
File,
@@ -134,8 +135,6 @@ pub enum VariableName {
Column,
/// Text from the latest selection.
SelectedText,
/// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm
RunnableSymbol,
/// Custom variable, provided by the plugin or other external source.
/// Will be printed with `ZED_` prefix to avoid potential conflicts with other variables.
Custom(Cow<'static, str>),
@@ -165,7 +164,6 @@ impl std::fmt::Display for VariableName {
Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"),
Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
Self::Custom(s) => write!(f, "{ZED_VARIABLE_NAME_PREFIX}CUSTOM_{s}"),
}
}
@@ -173,7 +171,7 @@ impl std::fmt::Display for VariableName {
/// Container for predefined environment variables that describe state of Zed at the time the task was spawned.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct TaskVariables(HashMap<VariableName, String>);
pub struct TaskVariables(BTreeMap<VariableName, String>);
impl TaskVariables {
/// Inserts another variable into the container, overwriting the existing one if it already exists — in this case, the old value is returned.
@@ -199,14 +197,42 @@ impl TaskVariables {
}
})
}
/// Returns iterator over names of all set task variables.
pub fn keys(&self) -> impl Iterator<Item = &VariableName> {
self.0.keys()
}
}
impl FromIterator<(VariableName, String)> for TaskVariables {
fn from_iter<T: IntoIterator<Item = (VariableName, String)>>(iter: T) -> Self {
Self(HashMap::from_iter(iter))
Self(BTreeMap::from_iter(iter))
}
}
impl FromStr for VariableName {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let without_prefix = s.strip_prefix(ZED_VARIABLE_NAME_PREFIX).ok_or(())?;
let value = match without_prefix {
"FILE" => Self::File,
"WORKTREE_ROOT" => Self::WorktreeRoot,
"SYMBOL" => Self::Symbol,
"SELECTED_TEXT" => Self::SelectedText,
"ROW" => Self::Row,
"COLUMN" => Self::Column,
_ => {
if let Some(custom_name) = without_prefix.strip_prefix("CUSTOM_") {
Self::Custom(Cow::Owned(custom_name.to_owned()))
} else {
return Err(());
}
}
};
Ok(value)
}
}
/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function).
/// Keeps all Zed-related state inside, used to produce a resolved task out of its template.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]

View File

@@ -14,9 +14,11 @@ file_icons.workspace = true
fuzzy.workspace = true
gpui.workspace = true
menu.workspace = true
parking_lot.workspace = true
picker.workspace = true
project.workspace = true
task.workspace = true
text.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true

View File

@@ -10,6 +10,7 @@ use workspace::tasks::schedule_task;
use workspace::{tasks::schedule_resolved_task, Workspace};
mod modal;
mod modal_completions;
mod settings;
pub use modal::Spawn;

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::active_item_selection_properties;
use crate::{active_item_selection_properties, modal_completions::TaskVariablesCompletionProvider};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView,
@@ -139,11 +139,14 @@ impl TasksModal {
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let provider = TaskVariablesCompletionProvider::new(task_context.task_variables.clone());
let picker = cx.new_view(|cx| {
Picker::uniform_list(
TasksModalDelegate::new(inventory, task_context, workspace),
cx,
)
.with_completions_provider(Box::new(provider), cx)
});
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
cx.emit(DismissEvent);

View File

@@ -0,0 +1,138 @@
use std::{str::FromStr, sync::Arc};
use editor::CompletionProvider;
use fuzzy::{CharBag, StringMatchCandidate};
use gpui::{AppContext, Model, Task};
use language::{CodeLabel, Documentation, LanguageServerId};
use parking_lot::RwLock;
use task::{TaskVariables, VariableName};
use text::{Anchor, ToOffset};
use ui::ViewContext;
pub(crate) struct TaskVariablesCompletionProvider {
task_variables: Arc<TaskVariables>,
pub(crate) names: Arc<[StringMatchCandidate]>,
}
impl TaskVariablesCompletionProvider {
pub(crate) fn new(variables: TaskVariables) -> Self {
let names = variables
.keys()
.enumerate()
.map(|(index, name)| {
let name = name.to_string();
StringMatchCandidate {
id: index,
char_bag: CharBag::from(name.as_str()),
string: name,
}
})
.collect::<Arc<[_]>>();
Self {
names,
task_variables: Arc::new(variables),
}
}
fn current_query(
buffer: &Model<language::Buffer>,
position: language::Anchor,
cx: &AppContext,
) -> Option<String> {
let mut has_trigger_character = false;
let reversed_query = buffer
.read(cx)
.reversed_chars_for_range(Anchor::MIN..position)
.take_while(|c| {
let is_trigger = *c == '$';
if is_trigger {
has_trigger_character = true;
}
!is_trigger && (*c == '_' || c.is_ascii_alphanumeric())
})
.collect::<String>();
has_trigger_character.then(|| reversed_query.chars().rev().collect())
}
}
impl CompletionProvider for TaskVariablesCompletionProvider {
fn completions(
&self,
buffer: &Model<language::Buffer>,
buffer_position: text::Anchor,
cx: &mut ViewContext<editor::Editor>,
) -> gpui::Task<gpui::Result<Vec<project::Completion>>> {
let Some(current_query) = Self::current_query(buffer, buffer_position, cx) else {
return Task::ready(Ok(vec![]));
};
let buffer = buffer.read(cx);
let buffer_snapshot = buffer.snapshot();
let offset = buffer_position.to_offset(&buffer_snapshot);
let starting_offset = offset - current_query.len();
let starting_anchor = buffer.anchor_before(starting_offset);
let executor = cx.background_executor().clone();
let names = self.names.clone();
let variables = self.task_variables.clone();
cx.background_executor().spawn(async move {
let matches = fuzzy::match_strings(
&names,
&current_query,
true,
100,
&Default::default(),
executor,
)
.await;
// Find all variables starting with this
Ok(matches
.into_iter()
.filter_map(|hit| {
let variable_key = VariableName::from_str(&hit.string).ok()?;
let value_of_var = variables.get(&variable_key)?.to_owned();
Some(project::Completion {
old_range: starting_anchor..buffer_position,
new_text: hit.string.clone(),
label: CodeLabel::plain(hit.string, None),
documentation: Some(Documentation::SingleLine(value_of_var)),
server_id: LanguageServerId(0), // TODO: Make this optional or something?
lsp_completion: Default::default(), // TODO: Make this optional or something?
})
})
.collect())
})
}
fn resolve_completions(
&self,
_buffer: Model<language::Buffer>,
_completion_indices: Vec<usize>,
_completions: Arc<RwLock<Box<[project::Completion]>>>,
_cx: &mut ViewContext<editor::Editor>,
) -> gpui::Task<gpui::Result<bool>> {
Task::ready(Ok(true))
}
fn apply_additional_edits_for_completion(
&self,
_buffer: Model<language::Buffer>,
_completion: project::Completion,
_push_to_history: bool,
_cx: &mut ViewContext<editor::Editor>,
) -> gpui::Task<gpui::Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
buffer: &Model<language::Buffer>,
position: language::Anchor,
text: &str,
_trigger_in_words: bool,
cx: &mut ViewContext<editor::Editor>,
) -> bool {
if text == "$" {
return true;
}
Self::current_query(buffer, position, cx).is_some()
}
}