Add support for detecting tests in source files, and implement it for Rust (#11195)
Continuing work from #10873 Release Notes: - N/A --------- Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
14c7782ce6
commit
5a71d8c7f1
29 changed files with 1148 additions and 606 deletions
|
@ -14,6 +14,7 @@ collections.workspace = true
|
|||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
hex.workspace = true
|
||||
parking_lot.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
|
|
|
@ -6,9 +6,8 @@ mod task_template;
|
|||
mod vscode_format;
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::ModelContext;
|
||||
use gpui::SharedString;
|
||||
use serde::Serialize;
|
||||
use std::any::Any;
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
@ -103,6 +102,8 @@ 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>),
|
||||
|
@ -132,6 +133,7 @@ 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}"),
|
||||
}
|
||||
}
|
||||
|
@ -169,13 +171,6 @@ pub struct TaskContext {
|
|||
pub task_variables: TaskVariables,
|
||||
}
|
||||
|
||||
/// [`Source`] produces tasks that can be scheduled.
|
||||
///
|
||||
/// Implementations of this trait could be e.g. [`StaticSource`] that parses tasks from a .json files and provides process templates to be spawned;
|
||||
/// another one could be a language server providing lenses with tests or build server listing all targets for a given project.
|
||||
pub trait TaskSource: Any {
|
||||
/// A way to erase the type of the source, processing and storing them generically.
|
||||
fn as_any(&mut self) -> &mut dyn Any;
|
||||
/// Collects all tasks available for scheduling.
|
||||
fn tasks_to_schedule(&mut self, cx: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates;
|
||||
}
|
||||
/// This is a new type representing a 'tag' on a 'runnable symbol', typically a test of main() function, found via treesitter.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RunnableTag(pub SharedString);
|
||||
|
|
|
@ -1,134 +1,110 @@
|
|||
//! A source of tasks, based on a static configuration, deserialized from the tasks config file, and related infrastructure for tracking changes to the file.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
||||
use gpui::AppContext;
|
||||
use parking_lot::RwLock;
|
||||
use serde::Deserialize;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{TaskSource, TaskTemplates};
|
||||
use crate::TaskTemplates;
|
||||
use futures::channel::mpsc::UnboundedReceiver;
|
||||
|
||||
/// The source of tasks defined in a tasks config file.
|
||||
pub struct StaticSource {
|
||||
tasks: TaskTemplates,
|
||||
_templates: Model<TrackedFile<TaskTemplates>>,
|
||||
_subscription: Subscription,
|
||||
tasks: TrackedFile<TaskTemplates>,
|
||||
}
|
||||
|
||||
/// A Wrapper around deserializable T that keeps track of its contents
|
||||
/// via a provided channel. Once T value changes, the observers of [`TrackedFile`] are
|
||||
/// notified.
|
||||
pub struct TrackedFile<T> {
|
||||
parsed_contents: T,
|
||||
parsed_contents: Arc<RwLock<T>>,
|
||||
}
|
||||
|
||||
impl<T: PartialEq + 'static> TrackedFile<T> {
|
||||
impl<T: PartialEq + 'static + Sync> TrackedFile<T> {
|
||||
/// Initializes new [`TrackedFile`] with a type that's deserializable.
|
||||
pub fn new(mut tracker: UnboundedReceiver<String>, cx: &mut AppContext) -> Model<Self>
|
||||
pub fn new(mut tracker: UnboundedReceiver<String>, cx: &mut AppContext) -> Self
|
||||
where
|
||||
T: for<'a> Deserialize<'a> + Default,
|
||||
T: for<'a> Deserialize<'a> + Default + Send,
|
||||
{
|
||||
cx.new_model(move |cx| {
|
||||
cx.spawn(|tracked_file, mut cx| async move {
|
||||
while let Some(new_contents) = tracker.next().await {
|
||||
if !new_contents.trim().is_empty() {
|
||||
// String -> T (ZedTaskFormat)
|
||||
// String -> U (VsCodeFormat) -> Into::into T
|
||||
let Some(new_contents) =
|
||||
serde_json_lenient::from_str(&new_contents).log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile<T>, cx| {
|
||||
if tracked_file.parsed_contents != new_contents {
|
||||
tracked_file.parsed_contents = new_contents;
|
||||
cx.notify();
|
||||
let parsed_contents: Arc<RwLock<T>> = Arc::default();
|
||||
cx.background_executor()
|
||||
.spawn({
|
||||
let parsed_contents = parsed_contents.clone();
|
||||
async move {
|
||||
while let Some(new_contents) = tracker.next().await {
|
||||
if Arc::strong_count(&parsed_contents) == 1 {
|
||||
// We're no longer being observed. Stop polling.
|
||||
break;
|
||||
}
|
||||
if !new_contents.trim().is_empty() {
|
||||
let Some(new_contents) =
|
||||
serde_json_lenient::from_str::<T>(&new_contents).log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
})?;
|
||||
let mut contents = parsed_contents.write();
|
||||
*contents = new_contents;
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Self {
|
||||
parsed_contents: Default::default(),
|
||||
}
|
||||
})
|
||||
Self { parsed_contents }
|
||||
}
|
||||
|
||||
/// Initializes new [`TrackedFile`] with a type that's convertible from another deserializable type.
|
||||
pub fn new_convertible<U: for<'a> Deserialize<'a> + TryInto<T, Error = anyhow::Error>>(
|
||||
mut tracker: UnboundedReceiver<String>,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<Self>
|
||||
) -> Self
|
||||
where
|
||||
T: Default,
|
||||
T: Default + Send,
|
||||
{
|
||||
cx.new_model(move |cx| {
|
||||
cx.spawn(|tracked_file, mut cx| async move {
|
||||
while let Some(new_contents) = tracker.next().await {
|
||||
if !new_contents.trim().is_empty() {
|
||||
let Some(new_contents) =
|
||||
serde_json_lenient::from_str::<U>(&new_contents).log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(new_contents) = new_contents.try_into().log_err() else {
|
||||
continue;
|
||||
};
|
||||
tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile<T>, cx| {
|
||||
if tracked_file.parsed_contents != new_contents {
|
||||
tracked_file.parsed_contents = new_contents;
|
||||
cx.notify();
|
||||
let parsed_contents: Arc<RwLock<T>> = Arc::default();
|
||||
cx.background_executor()
|
||||
.spawn({
|
||||
let parsed_contents = parsed_contents.clone();
|
||||
async move {
|
||||
while let Some(new_contents) = tracker.next().await {
|
||||
if Arc::strong_count(&parsed_contents) == 1 {
|
||||
// We're no longer being observed. Stop polling.
|
||||
break;
|
||||
}
|
||||
|
||||
if !new_contents.trim().is_empty() {
|
||||
let Some(new_contents) =
|
||||
serde_json_lenient::from_str::<U>(&new_contents).log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
})?;
|
||||
let Some(new_contents) = new_contents.try_into().log_err() else {
|
||||
continue;
|
||||
};
|
||||
let mut contents = parsed_contents.write();
|
||||
*contents = new_contents;
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Self {
|
||||
parsed_contents: Default::default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn get(&self) -> &T {
|
||||
&self.parsed_contents
|
||||
Self {
|
||||
parsed_contents: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticSource {
|
||||
/// Initializes the static source, reacting on tasks config changes.
|
||||
pub fn new(
|
||||
templates: Model<TrackedFile<TaskTemplates>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<Box<dyn TaskSource>> {
|
||||
cx.new_model(|cx| {
|
||||
let _subscription = cx.observe(
|
||||
&templates,
|
||||
move |source: &mut Box<(dyn TaskSource + 'static)>, new_templates, cx| {
|
||||
if let Some(static_source) = source.as_any().downcast_mut::<Self>() {
|
||||
static_source.tasks = new_templates.read(cx).get().clone();
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
);
|
||||
Box::new(Self {
|
||||
tasks: TaskTemplates::default(),
|
||||
_templates: templates,
|
||||
_subscription,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskSource for StaticSource {
|
||||
fn tasks_to_schedule(&mut self, _: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates {
|
||||
self.tasks.clone()
|
||||
}
|
||||
|
||||
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
pub fn new(tasks: TrackedFile<TaskTemplates>) -> Self {
|
||||
Self { tasks }
|
||||
}
|
||||
/// Returns current list of tasks
|
||||
pub fn tasks_to_schedule(&self) -> TaskTemplates {
|
||||
self.tasks.parsed_contents.read().clone()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,10 @@ pub struct TaskTemplate {
|
|||
/// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
#[serde(default)]
|
||||
pub reveal: RevealStrategy,
|
||||
|
||||
/// Represents the tags which this template attaches to. Adding this removes this task from other UI.
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// What to do with the terminal pane and tab, after the command was started.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue