logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
41
crates/task/Cargo.toml
Normal file
41
crates/task/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "task"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"gpui/test-support",
|
||||
"util/test-support"
|
||||
]
|
||||
|
||||
[lib]
|
||||
path = "src/task.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
hex.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
proto.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
sha2.workspace = true
|
||||
shellexpand.workspace = true
|
||||
util.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
1
crates/task/LICENSE-GPL
Symbolic link
1
crates/task/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
16
crates/task/src/adapter_schema.rs
Normal file
16
crates/task/src/adapter_schema.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use gpui::SharedString;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// JSON schema for a specific adapter
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
pub struct AdapterSchema {
|
||||
/// The adapter name identifier
|
||||
pub adapter: SharedString,
|
||||
/// The JSON schema for this adapter's configuration
|
||||
pub schema: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
pub struct AdapterSchemas(pub Vec<AdapterSchema>);
|
||||
534
crates/task/src/debug_format.rs
Normal file
534
crates/task/src/debug_format.rs
Normal file
@@ -0,0 +1,534 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::FxHashMap;
|
||||
use gpui::SharedString;
|
||||
use log as _;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::IpAddr;
|
||||
use std::path::PathBuf;
|
||||
use util::{debug_panic, schemars::add_new_subschema};
|
||||
|
||||
use crate::{TaskTemplate, adapter_schema::AdapterSchemas};
|
||||
|
||||
/// Represents the host information of the debug adapter
|
||||
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
pub struct TcpArgumentsTemplate {
|
||||
/// The port that the debug adapter is listening on
|
||||
///
|
||||
/// Default: We will try to find an open port
|
||||
pub port: Option<u16>,
|
||||
/// The host that the debug adapter is listening too
|
||||
///
|
||||
/// Default: 127.0.0.1
|
||||
pub host: Option<IpAddr>,
|
||||
/// The max amount of time in milliseconds to connect to a tcp DAP before returning an error
|
||||
///
|
||||
/// Default: 2000ms
|
||||
pub timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl TcpArgumentsTemplate {
|
||||
/// Get the host or fallback to the default host
|
||||
pub fn host(&self) -> IpAddr {
|
||||
self.host
|
||||
.unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))
|
||||
}
|
||||
|
||||
pub fn from_proto(proto: proto::TcpHost) -> Result<Self> {
|
||||
Ok(Self {
|
||||
port: proto.port.map(|p| p.try_into()).transpose()?,
|
||||
host: proto.host.map(|h| h.parse()).transpose()?,
|
||||
timeout: proto.timeout,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::TcpHost {
|
||||
proto::TcpHost {
|
||||
port: self.port.map(|p| p.into()),
|
||||
host: self.host.map(|h| h.to_string()),
|
||||
timeout: self.timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the attach request information of the debug adapter
|
||||
#[derive(Default, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
pub struct AttachRequest {
|
||||
/// The processId to attach to, if left empty we will show a process picker
|
||||
pub process_id: Option<u32>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AttachRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Helper {
|
||||
process_id: Option<u32>,
|
||||
}
|
||||
|
||||
let helper = Helper::deserialize(deserializer)?;
|
||||
|
||||
// Skip creating an AttachRequest if process_id is None
|
||||
if helper.process_id.is_none() {
|
||||
return Err(serde::de::Error::custom("process_id is required"));
|
||||
}
|
||||
|
||||
Ok(AttachRequest {
|
||||
process_id: helper.process_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the launch request information of the debug adapter
|
||||
#[derive(Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
pub struct LaunchRequest {
|
||||
/// The program that you trying to debug
|
||||
pub program: String,
|
||||
/// The current working directory of your project
|
||||
#[serde(default)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
/// Arguments to pass to a debuggee
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub env: FxHashMap<String, String>,
|
||||
}
|
||||
|
||||
impl LaunchRequest {
|
||||
pub fn env_json(&self) -> serde_json::Value {
|
||||
serde_json::Value::Object(
|
||||
self.env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.to_owned().into()))
|
||||
.collect::<serde_json::Map<String, serde_json::Value>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the type that will determine which request to call on the debug adapter
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
#[serde(rename_all = "lowercase", tag = "request")]
|
||||
pub enum DebugRequest {
|
||||
/// Call the `launch` request on the debug adapter
|
||||
Launch(LaunchRequest),
|
||||
/// Call the `attach` request on the debug adapter
|
||||
Attach(AttachRequest),
|
||||
}
|
||||
|
||||
impl DebugRequest {
|
||||
pub fn to_proto(&self) -> proto::DebugRequest {
|
||||
match self {
|
||||
DebugRequest::Launch(launch_request) => proto::DebugRequest {
|
||||
request: Some(proto::debug_request::Request::DebugLaunchRequest(
|
||||
proto::DebugLaunchRequest {
|
||||
program: launch_request.program.clone(),
|
||||
cwd: launch_request
|
||||
.cwd
|
||||
.as_ref()
|
||||
.map(|cwd| cwd.to_string_lossy().into_owned()),
|
||||
args: launch_request.args.clone(),
|
||||
env: launch_request
|
||||
.env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
},
|
||||
)),
|
||||
},
|
||||
DebugRequest::Attach(attach_request) => proto::DebugRequest {
|
||||
request: Some(proto::debug_request::Request::DebugAttachRequest(
|
||||
proto::DebugAttachRequest {
|
||||
process_id: attach_request
|
||||
.process_id
|
||||
.expect("The process ID to be already filled out."),
|
||||
},
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_proto(val: proto::DebugRequest) -> Result<DebugRequest> {
|
||||
let request = val.request.context("Missing debug request")?;
|
||||
match request {
|
||||
proto::debug_request::Request::DebugLaunchRequest(proto::DebugLaunchRequest {
|
||||
program,
|
||||
cwd,
|
||||
args,
|
||||
env,
|
||||
}) => Ok(DebugRequest::Launch(LaunchRequest {
|
||||
program,
|
||||
cwd: cwd.map(From::from),
|
||||
args,
|
||||
env: env.into_iter().collect(),
|
||||
})),
|
||||
|
||||
proto::debug_request::Request::DebugAttachRequest(proto::DebugAttachRequest {
|
||||
process_id,
|
||||
}) => Ok(DebugRequest::Attach(AttachRequest {
|
||||
process_id: Some(process_id),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LaunchRequest> for DebugRequest {
|
||||
fn from(launch_config: LaunchRequest) -> Self {
|
||||
DebugRequest::Launch(launch_config)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AttachRequest> for DebugRequest {
|
||||
fn from(attach_config: AttachRequest) -> Self {
|
||||
DebugRequest::Attach(attach_config)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum BuildTaskDefinition {
|
||||
ByName(SharedString),
|
||||
Template {
|
||||
#[serde(flatten)]
|
||||
task_template: TaskTemplate,
|
||||
#[serde(skip)]
|
||||
locator_name: Option<SharedString>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BuildTaskDefinition {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct TemplateHelper {
|
||||
#[serde(default)]
|
||||
label: Option<String>,
|
||||
#[serde(flatten)]
|
||||
rest: serde_json::Value,
|
||||
}
|
||||
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
|
||||
if let Ok(name) = serde_json::from_value::<SharedString>(value.clone()) {
|
||||
return Ok(BuildTaskDefinition::ByName(name));
|
||||
}
|
||||
|
||||
let helper: TemplateHelper =
|
||||
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
|
||||
|
||||
let mut template_value = helper.rest;
|
||||
if let serde_json::Value::Object(ref mut map) = template_value {
|
||||
map.insert(
|
||||
"label".to_string(),
|
||||
serde_json::to_value(helper.label.unwrap_or_else(|| "debug-build".to_owned()))
|
||||
.map_err(serde::de::Error::custom)?,
|
||||
);
|
||||
}
|
||||
|
||||
let task_template: TaskTemplate =
|
||||
serde_json::from_value(template_value).map_err(serde::de::Error::custom)?;
|
||||
|
||||
Ok(BuildTaskDefinition::Template {
|
||||
task_template,
|
||||
locator_name: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
|
||||
pub enum Request {
|
||||
Launch,
|
||||
Attach,
|
||||
}
|
||||
|
||||
/// This struct represent a user created debug task from the new process modal
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ZedDebugConfig {
|
||||
/// Name of the debug task
|
||||
pub label: SharedString,
|
||||
/// The debug adapter to use
|
||||
pub adapter: SharedString,
|
||||
#[serde(flatten)]
|
||||
pub request: DebugRequest,
|
||||
/// Whether to tell the debug adapter to stop on entry
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stop_on_entry: Option<bool>,
|
||||
}
|
||||
|
||||
/// This struct represent a user created debug task
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct DebugScenario {
|
||||
pub adapter: SharedString,
|
||||
/// Name of the debug task
|
||||
pub label: SharedString,
|
||||
/// A task to run prior to spawning the debuggee.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub build: Option<BuildTaskDefinition>,
|
||||
/// The main arguments to be sent to the debug adapter
|
||||
#[serde(default, flatten)]
|
||||
pub config: serde_json::Value,
|
||||
/// Optional TCP connection information
|
||||
///
|
||||
/// If provided, this will be used to connect to the debug adapter instead of
|
||||
/// spawning a new process. This is useful for connecting to a debug adapter
|
||||
/// that is already running or is started by another process.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tcp_connection: Option<TcpArgumentsTemplate>,
|
||||
}
|
||||
|
||||
/// A group of Debug Tasks defined in a JSON file.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
pub struct DebugTaskFile(pub Vec<DebugScenario>);
|
||||
|
||||
impl DebugTaskFile {
|
||||
pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json::Value {
|
||||
let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
|
||||
|
||||
let mut build_task_value = BuildTaskDefinition::json_schema(&mut generator).to_value();
|
||||
|
||||
if let Some(template_object) = build_task_value
|
||||
.get_mut("anyOf")
|
||||
.and_then(|array| array.as_array_mut())
|
||||
.and_then(|array| array.get_mut(1))
|
||||
{
|
||||
if let Some(properties) = template_object
|
||||
.get_mut("properties")
|
||||
.and_then(|value| value.as_object_mut())
|
||||
&& properties.remove("label").is_none()
|
||||
{
|
||||
debug_panic!(
|
||||
"Generated TaskTemplate json schema did not have expected 'label' field. \
|
||||
Schema of 2nd alternative is: {template_object:?}"
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(arr) = template_object
|
||||
.get_mut("required")
|
||||
.and_then(|array| array.as_array_mut())
|
||||
{
|
||||
arr.retain(|v| v.as_str() != Some("label"));
|
||||
}
|
||||
} else {
|
||||
debug_panic!(
|
||||
"Generated TaskTemplate json schema did not match expectations. \
|
||||
Schema is: {build_task_value:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let adapter_conditions = schemas
|
||||
.0
|
||||
.iter()
|
||||
.map(|adapter_schema| {
|
||||
let adapter_name = adapter_schema.adapter.to_string();
|
||||
add_new_subschema(
|
||||
&mut generator,
|
||||
&format!("{adapter_name}DebugSettings"),
|
||||
serde_json::json!({
|
||||
"if": {
|
||||
"properties": {
|
||||
"adapter": { "const": adapter_name }
|
||||
}
|
||||
},
|
||||
"then": adapter_schema.schema
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let build_task_definition_ref = add_new_subschema(
|
||||
&mut generator,
|
||||
BuildTaskDefinition::schema_name().as_ref(),
|
||||
build_task_value,
|
||||
);
|
||||
|
||||
let meta_schema = generator
|
||||
.settings()
|
||||
.meta_schema
|
||||
.as_ref()
|
||||
.expect("meta_schema should be present in schemars settings")
|
||||
.to_string();
|
||||
|
||||
serde_json::json!({
|
||||
"$schema": meta_schema,
|
||||
"title": "Debug Configurations",
|
||||
"description": "Configuration for debug scenarios",
|
||||
"allowTrailingCommas": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["adapter", "label"],
|
||||
// TODO: Uncommenting this will cause json-language-server to provide warnings for
|
||||
// unrecognized properties. It should be enabled if/when there's an adapter JSON
|
||||
// schema that's comprehensive. In order to not get warnings for the other schemas,
|
||||
// `additionalProperties` or `unevaluatedProperties` (to handle "allOf" etc style
|
||||
// schema combinations) could be set to `true` for that schema.
|
||||
//
|
||||
// "unevaluatedProperties": false,
|
||||
"properties": {
|
||||
"adapter": {
|
||||
"type": "string",
|
||||
"description": "The name of the debug adapter"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "The name of the debug configuration"
|
||||
},
|
||||
"build": build_task_definition_ref,
|
||||
"tcp_connection": {
|
||||
"type": "object",
|
||||
"description": "Optional TCP connection information for connecting to an already running debug adapter",
|
||||
"properties": {
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "The port that the debug adapter is listening on (default: auto-find open port)"
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "The host that the debug adapter is listening to, as an IPv4 or IPv6 address (default: 127.0.0.1)"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "The max amount of time in milliseconds to connect to a tcp DAP before returning an error (default: 2000ms)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": adapter_conditions
|
||||
},
|
||||
"$defs": generator.take_definitions(true),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::DebugScenario;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_just_build_args() {
|
||||
let json = r#"{
|
||||
"label": "Build & debug rust",
|
||||
"adapter": "CodeLLDB",
|
||||
"build": {
|
||||
"command": "rust",
|
||||
"args": ["build"]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
|
||||
assert!(deserialized.build.is_some());
|
||||
match deserialized.build.as_ref().unwrap() {
|
||||
crate::BuildTaskDefinition::Template { task_template, .. } => {
|
||||
assert_eq!("debug-build", task_template.label);
|
||||
assert_eq!("rust", task_template.command);
|
||||
assert_eq!(vec!["build"], task_template.args);
|
||||
}
|
||||
_ => panic!("Expected Template variant"),
|
||||
}
|
||||
assert_eq!(json!({}), deserialized.config);
|
||||
assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
|
||||
assert_eq!("Build & debug rust", deserialized.label.as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_scenario_has_none_request() {
|
||||
let json = r#"{
|
||||
"label": "Build & debug rust",
|
||||
"build": "rust",
|
||||
"adapter": "CodeLLDB"
|
||||
}"#;
|
||||
|
||||
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert_eq!(json!({}), deserialized.config);
|
||||
assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
|
||||
assert_eq!("Build & debug rust", deserialized.label.as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_launch_scenario_deserialization() {
|
||||
let json = r#"{
|
||||
"label": "Launch program",
|
||||
"adapter": "CodeLLDB",
|
||||
"request": "launch",
|
||||
"program": "target/debug/myapp",
|
||||
"args": ["--test"]
|
||||
}"#;
|
||||
|
||||
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
json!({ "request": "launch", "program": "target/debug/myapp", "args": ["--test"] }),
|
||||
deserialized.config
|
||||
);
|
||||
assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
|
||||
assert_eq!("Launch program", deserialized.label.as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attach_scenario_deserialization() {
|
||||
let json = r#"{
|
||||
"label": "Attach to process",
|
||||
"adapter": "CodeLLDB",
|
||||
"process_id": 1234,
|
||||
"request": "attach"
|
||||
}"#;
|
||||
|
||||
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
json!({ "request": "attach", "process_id": 1234 }),
|
||||
deserialized.config
|
||||
);
|
||||
assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
|
||||
assert_eq!("Attach to process", deserialized.label.as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_task_definition_without_label() {
|
||||
use crate::BuildTaskDefinition;
|
||||
|
||||
let json = r#""my_build_task""#;
|
||||
let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
|
||||
match deserialized {
|
||||
BuildTaskDefinition::ByName(name) => assert_eq!("my_build_task", name.as_ref()),
|
||||
_ => panic!("Expected ByName variant"),
|
||||
}
|
||||
|
||||
let json = r#"{
|
||||
"command": "cargo",
|
||||
"args": ["build", "--release"]
|
||||
}"#;
|
||||
let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
|
||||
match deserialized {
|
||||
BuildTaskDefinition::Template { task_template, .. } => {
|
||||
assert_eq!("debug-build", task_template.label);
|
||||
assert_eq!("cargo", task_template.command);
|
||||
assert_eq!(vec!["build", "--release"], task_template.args);
|
||||
}
|
||||
_ => panic!("Expected Template variant"),
|
||||
}
|
||||
|
||||
let json = r#"{
|
||||
"label": "Build Release",
|
||||
"command": "cargo",
|
||||
"args": ["build", "--release"]
|
||||
}"#;
|
||||
let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
|
||||
match deserialized {
|
||||
BuildTaskDefinition::Template { task_template, .. } => {
|
||||
assert_eq!("Build Release", task_template.label);
|
||||
assert_eq!("cargo", task_template.command);
|
||||
assert_eq!(vec!["build", "--release"], task_template.args);
|
||||
}
|
||||
_ => panic!("Expected Template variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
37
crates/task/src/serde_helpers.rs
Normal file
37
crates/task/src/serde_helpers.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use serde::de::{self, Deserializer, Visitor};
|
||||
use std::fmt;
|
||||
|
||||
/// Deserializes a non-empty string array.
|
||||
pub fn non_empty_string_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct NonEmptyStringVecVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for NonEmptyStringVecVisitor {
|
||||
type Value = Vec<String>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a list of non-empty strings")
|
||||
}
|
||||
|
||||
fn visit_seq<V>(self, mut seq: V) -> Result<Vec<String>, V::Error>
|
||||
where
|
||||
V: de::SeqAccess<'de>,
|
||||
{
|
||||
let mut vec = Vec::new();
|
||||
while let Some(value) = seq.next_element::<String>()? {
|
||||
if value.is_empty() {
|
||||
return Err(de::Error::invalid_value(
|
||||
de::Unexpected::Str(&value),
|
||||
&"a non-empty string",
|
||||
));
|
||||
}
|
||||
vec.push(value);
|
||||
}
|
||||
Ok(vec)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_seq(NonEmptyStringVecVisitor)
|
||||
}
|
||||
123
crates/task/src/static_source.rs
Normal file
123
crates/task/src/static_source.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
//! 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, channel::mpsc::UnboundedSender};
|
||||
use gpui::{App, AppContext, TaskExt};
|
||||
use parking_lot::RwLock;
|
||||
use serde::Deserialize;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::TaskTemplates;
|
||||
use futures::channel::mpsc::UnboundedReceiver;
|
||||
|
||||
/// The source of tasks defined in a tasks config file.
|
||||
pub struct StaticSource {
|
||||
tasks: TrackedFile<TaskTemplates>,
|
||||
}
|
||||
|
||||
/// A Wrapper around deserializable T that keeps track of its contents
|
||||
/// via a provided channel.
|
||||
pub struct TrackedFile<T> {
|
||||
parsed_contents: Arc<RwLock<T>>,
|
||||
}
|
||||
|
||||
impl<T: PartialEq + 'static + Sync> TrackedFile<T> {
|
||||
/// Initializes new [`TrackedFile`] with a type that's deserializable.
|
||||
pub fn new(
|
||||
mut tracker: UnboundedReceiver<String>,
|
||||
notification_outlet: UnboundedSender<()>,
|
||||
cx: &App,
|
||||
) -> Self
|
||||
where
|
||||
T: for<'a> Deserialize<'a> + Default + Send,
|
||||
{
|
||||
let parsed_contents: Arc<RwLock<T>> = Arc::default();
|
||||
cx.background_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();
|
||||
if *contents != new_contents {
|
||||
*contents = new_contents;
|
||||
if notification_outlet.unbounded_send(()).is_err() {
|
||||
// Whoever cared about contents is not around anymore.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
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>,
|
||||
notification_outlet: UnboundedSender<()>,
|
||||
cx: &App,
|
||||
) -> Self
|
||||
where
|
||||
T: Default + Send,
|
||||
{
|
||||
let parsed_contents: Arc<RwLock<T>> = Arc::default();
|
||||
cx.background_spawn({
|
||||
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();
|
||||
if *contents != new_contents {
|
||||
*contents = new_contents;
|
||||
if notification_outlet.unbounded_send(()).is_err() {
|
||||
// Whoever cared about contents is not around anymore.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Self {
|
||||
parsed_contents: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticSource {
|
||||
/// Initializes the static source, reacting on tasks config changes.
|
||||
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()
|
||||
}
|
||||
}
|
||||
478
crates/task/src/task.rs
Normal file
478
crates/task/src/task.rs
Normal file
@@ -0,0 +1,478 @@
|
||||
//! Baseline interface of Tasks in Zed: all tasks in Zed are intended to use those for implementing their own logic.
|
||||
|
||||
mod adapter_schema;
|
||||
mod debug_format;
|
||||
mod serde_helpers;
|
||||
pub mod static_source;
|
||||
mod task_template;
|
||||
mod vscode_debug_format;
|
||||
mod vscode_format;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use collections::{HashMap, HashSet, hash_map};
|
||||
use gpui::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use adapter_schema::{AdapterSchema, AdapterSchemas};
|
||||
pub use debug_format::{
|
||||
AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest,
|
||||
Request, TcpArgumentsTemplate, ZedDebugConfig,
|
||||
};
|
||||
pub use task_template::{
|
||||
DebugArgsRequest, HideStrategy, RevealStrategy, SaveStrategy, TaskHook, TaskTemplate,
|
||||
TaskTemplates, substitute_variables_in_map, substitute_variables_in_str,
|
||||
};
|
||||
pub use util::shell::{Shell, ShellKind};
|
||||
pub use util::shell_builder::ShellBuilder;
|
||||
pub use vscode_debug_format::VsCodeDebugTaskFile;
|
||||
pub use vscode_format::VsCodeTaskFile;
|
||||
pub use zed_actions::RevealTarget;
|
||||
|
||||
/// Task identifier, unique within the application.
|
||||
/// Based on it, task reruns and terminal tabs are managed.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)]
|
||||
pub struct TaskId(pub String);
|
||||
|
||||
/// Contains all information needed by Zed to spawn a new terminal tab for the given task.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SpawnInTerminal {
|
||||
/// Id of the task to use when determining task tab affinity.
|
||||
pub id: TaskId,
|
||||
/// Full unshortened form of `label` field.
|
||||
pub full_label: String,
|
||||
/// Human readable name of the terminal tab.
|
||||
pub label: String,
|
||||
/// Executable command to spawn.
|
||||
pub command: Option<String>,
|
||||
/// Arguments to the command, potentially unsubstituted,
|
||||
/// to let the shell that spawns the command to do the substitution, if needed.
|
||||
pub args: Vec<String>,
|
||||
/// A human-readable label, containing command and all of its arguments, joined and substituted.
|
||||
pub command_label: String,
|
||||
/// Current working directory to spawn the command into.
|
||||
pub cwd: Option<PathBuf>,
|
||||
/// Env overrides for the command, will be appended to the terminal's environment from the settings.
|
||||
pub env: HashMap<String, String>,
|
||||
/// Whether to use a new terminal tab or reuse the existing one to spawn the process.
|
||||
pub use_new_terminal: bool,
|
||||
/// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish.
|
||||
pub allow_concurrent_runs: bool,
|
||||
/// What to do with the terminal pane and tab, after the command was started.
|
||||
pub reveal: RevealStrategy,
|
||||
/// Where to show tasks' terminal output.
|
||||
pub reveal_target: RevealTarget,
|
||||
/// What to do with the terminal pane and tab, after the command had finished.
|
||||
pub hide: HideStrategy,
|
||||
/// Which shell to use when spawning the task.
|
||||
pub shell: Shell,
|
||||
/// Whether to show the task summary line in the task output (sucess/failure).
|
||||
pub show_summary: bool,
|
||||
/// Whether to show the command line in the task output.
|
||||
pub show_command: bool,
|
||||
/// Whether to show the rerun button in the terminal tab.
|
||||
pub show_rerun: bool,
|
||||
/// Which edited buffers to save before running the task.
|
||||
pub save: SaveStrategy,
|
||||
}
|
||||
|
||||
impl SpawnInTerminal {
|
||||
pub fn to_proto(&self) -> proto::SpawnInTerminal {
|
||||
proto::SpawnInTerminal {
|
||||
label: self.label.clone(),
|
||||
command: self.command.clone(),
|
||||
args: self.args.clone(),
|
||||
env: self
|
||||
.env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
cwd: self
|
||||
.cwd
|
||||
.clone()
|
||||
.map(|cwd| cwd.to_string_lossy().into_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_proto(proto: proto::SpawnInTerminal) -> Self {
|
||||
Self {
|
||||
label: proto.label.clone(),
|
||||
command: proto.command.clone(),
|
||||
args: proto.args.clone(),
|
||||
env: proto.env.into_iter().collect(),
|
||||
cwd: proto.cwd.map(PathBuf::from),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A final form of the [`TaskTemplate`], that got resolved with a particular [`TaskContext`] and now is ready to spawn the actual task.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResolvedTask {
|
||||
/// A way to distinguish tasks produced by the same template, but different contexts.
|
||||
/// NOTE: Resolved tasks may have the same labels, commands and do the same things,
|
||||
/// but still may have different ids if the context was different during the resolution.
|
||||
/// Since the template has `env` field, for a generic task that may be a bash command,
|
||||
/// so it's impossible to determine the id equality without more context in a generic case.
|
||||
pub id: TaskId,
|
||||
/// A template the task got resolved from.
|
||||
original_task: TaskTemplate,
|
||||
/// Full, unshortened label of the task after all resolutions are made.
|
||||
pub resolved_label: String,
|
||||
/// Variables that were substituted during the task template resolution.
|
||||
substituted_variables: HashSet<VariableName>,
|
||||
/// Further actions that need to take place after the resolved task is spawned,
|
||||
/// with all task variables resolved.
|
||||
pub resolved: SpawnInTerminal,
|
||||
}
|
||||
|
||||
impl ResolvedTask {
|
||||
/// A task template before the resolution.
|
||||
pub fn original_task(&self) -> &TaskTemplate {
|
||||
&self.original_task
|
||||
}
|
||||
|
||||
/// Variables that were substituted during the task template resolution.
|
||||
pub fn substituted_variables(&self) -> &HashSet<VariableName> {
|
||||
&self.substituted_variables
|
||||
}
|
||||
|
||||
/// A human-readable label to display in the UI.
|
||||
pub fn display_label(&self) -> &str {
|
||||
self.resolved.label.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].
|
||||
/// Name of the variable must be a valid shell variable identifier, which generally means that it is
|
||||
/// a word consisting only of alphanumeric characters and underscores,
|
||||
/// and beginning with an alphabetic character or an underscore.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
||||
pub enum VariableName {
|
||||
/// An absolute path of the currently opened file.
|
||||
File,
|
||||
/// A path of the currently opened file (relative to worktree root).
|
||||
RelativeFile,
|
||||
/// A path of the currently opened file's directory (relative to worktree root).
|
||||
RelativeDir,
|
||||
/// The currently opened filename.
|
||||
Filename,
|
||||
/// The path to a parent directory of a currently opened file.
|
||||
Dirname,
|
||||
/// Stem (filename without extension) of the currently opened file.
|
||||
Stem,
|
||||
/// An absolute path of the currently opened worktree, that contains the file.
|
||||
WorktreeRoot,
|
||||
/// A symbol text, that contains latest cursor/selection position.
|
||||
Symbol,
|
||||
/// A row with the latest cursor/selection position.
|
||||
Row,
|
||||
/// A column with the latest cursor/selection position.
|
||||
Column,
|
||||
/// Text from the latest selection.
|
||||
SelectedText,
|
||||
/// The language of the currently opened buffer (e.g., "Rust", "Python").
|
||||
Language,
|
||||
/// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm
|
||||
RunnableSymbol,
|
||||
/// Open a Picker to select a process ID to use in place
|
||||
/// Can only be used to debug configurations
|
||||
PickProcessId,
|
||||
/// An absolute path of the main (original) git worktree for the current repository.
|
||||
/// For normal checkouts, this equals the worktree root. For linked worktrees,
|
||||
/// this is the original repo's working directory.
|
||||
MainGitWorktree,
|
||||
/// Full SHA for the Git commit associated with the task context.
|
||||
GitSha,
|
||||
/// Short SHA for the Git commit associated with the task context.
|
||||
GitShaShort,
|
||||
/// Name of the Git repository associated with the task context.
|
||||
GitRepositoryName,
|
||||
/// Absolute path of the Git repository associated with the task context.
|
||||
GitRepositoryPath,
|
||||
/// Custom variable, provided by the plugin or other external source.
|
||||
/// Will be printed with `CUSTOM_` prefix to avoid potential conflicts with other variables.
|
||||
Custom(Cow<'static, str>),
|
||||
}
|
||||
|
||||
impl VariableName {
|
||||
/// Generates a `$VARIABLE`-like string value to be used in templates.
|
||||
pub fn template_value(&self) -> String {
|
||||
format!("${self}")
|
||||
}
|
||||
/// Generates a `"$VARIABLE"`-like string, to be used instead of `Self::template_value` when expanded value could contain spaces or special characters.
|
||||
pub fn template_value_with_whitespace(&self) -> String {
|
||||
format!("\"${self}\"")
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
"FILENAME" => Self::Filename,
|
||||
"RELATIVE_FILE" => Self::RelativeFile,
|
||||
"RELATIVE_DIR" => Self::RelativeDir,
|
||||
"DIRNAME" => Self::Dirname,
|
||||
"STEM" => Self::Stem,
|
||||
"WORKTREE_ROOT" => Self::WorktreeRoot,
|
||||
"SYMBOL" => Self::Symbol,
|
||||
"RUNNABLE_SYMBOL" => Self::RunnableSymbol,
|
||||
"SELECTED_TEXT" => Self::SelectedText,
|
||||
"LANGUAGE" => Self::Language,
|
||||
"ROW" => Self::Row,
|
||||
"COLUMN" => Self::Column,
|
||||
"MAIN_GIT_WORKTREE" => Self::MainGitWorktree,
|
||||
"GIT_SHA" => Self::GitSha,
|
||||
"GIT_SHA_SHORT" => Self::GitShaShort,
|
||||
"GIT_REPOSITORY_NAME" => Self::GitRepositoryName,
|
||||
"GIT_REPOSITORY_PATH" => Self::GitRepositoryPath,
|
||||
_ => {
|
||||
if let Some(custom_name) =
|
||||
without_prefix.strip_prefix(ZED_CUSTOM_VARIABLE_NAME_PREFIX)
|
||||
{
|
||||
Self::Custom(Cow::Owned(custom_name.to_owned()))
|
||||
} else {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A prefix that all [`VariableName`] variants are prefixed with when used in environment variables and similar template contexts.
|
||||
pub const ZED_VARIABLE_NAME_PREFIX: &str = "ZED_";
|
||||
const ZED_CUSTOM_VARIABLE_NAME_PREFIX: &str = "CUSTOM_";
|
||||
|
||||
impl std::fmt::Display for VariableName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::File => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILE"),
|
||||
Self::Filename => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILENAME"),
|
||||
Self::RelativeFile => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RELATIVE_FILE"),
|
||||
Self::RelativeDir => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RELATIVE_DIR"),
|
||||
Self::Dirname => write!(f, "{ZED_VARIABLE_NAME_PREFIX}DIRNAME"),
|
||||
Self::Stem => write!(f, "{ZED_VARIABLE_NAME_PREFIX}STEM"),
|
||||
Self::WorktreeRoot => write!(f, "{ZED_VARIABLE_NAME_PREFIX}WORKTREE_ROOT"),
|
||||
Self::Symbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SYMBOL"),
|
||||
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::Language => write!(f, "{ZED_VARIABLE_NAME_PREFIX}LANGUAGE"),
|
||||
Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
|
||||
Self::PickProcessId => write!(f, "{ZED_VARIABLE_NAME_PREFIX}PICK_PID"),
|
||||
Self::MainGitWorktree => write!(f, "{ZED_VARIABLE_NAME_PREFIX}MAIN_GIT_WORKTREE"),
|
||||
Self::GitSha => write!(f, "{ZED_VARIABLE_NAME_PREFIX}GIT_SHA"),
|
||||
Self::GitShaShort => write!(f, "{ZED_VARIABLE_NAME_PREFIX}GIT_SHA_SHORT"),
|
||||
Self::GitRepositoryName => write!(f, "{ZED_VARIABLE_NAME_PREFIX}GIT_REPOSITORY_NAME"),
|
||||
Self::GitRepositoryPath => write!(f, "{ZED_VARIABLE_NAME_PREFIX}GIT_REPOSITORY_PATH"),
|
||||
Self::Custom(s) => write!(
|
||||
f,
|
||||
"{ZED_VARIABLE_NAME_PREFIX}{ZED_CUSTOM_VARIABLE_NAME_PREFIX}{s}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>);
|
||||
|
||||
impl TaskVariables {
|
||||
/// Inserts another variable into the container, overwriting the existing one if it already exists — in this case, the old value is returned.
|
||||
pub fn insert(&mut self, variable: VariableName, value: String) -> Option<String> {
|
||||
self.0.insert(variable, value)
|
||||
}
|
||||
|
||||
/// Extends the container with another one, overwriting the existing variables on collision.
|
||||
pub fn extend(&mut self, other: Self) {
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
/// Get the value associated with given variable name, if there is one.
|
||||
pub fn get(&self, key: &VariableName) -> Option<&str> {
|
||||
self.0.get(key).map(|s| s.as_str())
|
||||
}
|
||||
/// Clear out variables obtained from tree-sitter queries, which are prefixed with '_' character
|
||||
pub fn sweep(&mut self) {
|
||||
self.0.retain(|name, _| {
|
||||
if let VariableName::Custom(name) = name {
|
||||
!name.starts_with('_')
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&VariableName, &String)> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(VariableName, String)> for TaskVariables {
|
||||
fn from_iter<T: IntoIterator<Item = (VariableName, String)>>(iter: T) -> Self {
|
||||
Self(HashMap::from_iter(iter))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for TaskVariables {
|
||||
type Item = (VariableName, String);
|
||||
|
||||
type IntoIter = hash_map::IntoIter<VariableName, String>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
pub struct TaskContext {
|
||||
/// A path to a directory in which the task should be executed.
|
||||
pub cwd: Option<PathBuf>,
|
||||
/// Additional environment variables associated with a given task.
|
||||
pub task_variables: TaskVariables,
|
||||
/// Environment variables obtained when loading the project into Zed.
|
||||
/// This is the environment one would get when `cd`ing in a terminal
|
||||
/// into the project's root directory.
|
||||
pub project_env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// A shared reference to a [`TaskContext`], used to avoid cloning the context multiple times.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SharedTaskContext(Arc<TaskContext>);
|
||||
|
||||
impl std::ops::Deref for SharedTaskContext {
|
||||
type Target = TaskContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TaskContext> for SharedTaskContext {
|
||||
fn from(context: TaskContext) -> Self {
|
||||
Self(Arc::new(context))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
pub fn shell_from_proto(proto: proto::Shell) -> anyhow::Result<Shell> {
|
||||
let shell_type = proto.shell_type.context("invalid shell type")?;
|
||||
let shell = match shell_type {
|
||||
proto::shell::ShellType::System(_) => Shell::System,
|
||||
proto::shell::ShellType::Program(program) => Shell::Program(program),
|
||||
proto::shell::ShellType::WithArguments(program) => Shell::WithArguments {
|
||||
program: program.program,
|
||||
args: program.args,
|
||||
title_override: None,
|
||||
},
|
||||
};
|
||||
Ok(shell)
|
||||
}
|
||||
|
||||
pub fn shell_to_proto(shell: Shell) -> proto::Shell {
|
||||
let shell_type = match shell {
|
||||
Shell::System => proto::shell::ShellType::System(proto::System {}),
|
||||
Shell::Program(program) => proto::shell::ShellType::Program(program),
|
||||
Shell::WithArguments {
|
||||
program,
|
||||
args,
|
||||
title_override: _,
|
||||
} => proto::shell::ShellType::WithArguments(proto::shell::WithArguments { program, args }),
|
||||
};
|
||||
proto::Shell {
|
||||
shell_type: Some(shell_type),
|
||||
}
|
||||
}
|
||||
|
||||
type VsCodeEnvVariable = String;
|
||||
type VsCodeCommand = String;
|
||||
type ZedEnvVariable = String;
|
||||
|
||||
struct EnvVariableReplacer {
|
||||
variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>,
|
||||
commands: HashMap<VsCodeCommand, ZedEnvVariable>,
|
||||
}
|
||||
|
||||
impl EnvVariableReplacer {
|
||||
fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
|
||||
Self {
|
||||
variables,
|
||||
commands: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_commands(
|
||||
mut self,
|
||||
commands: impl IntoIterator<Item = (VsCodeCommand, ZedEnvVariable)>,
|
||||
) -> Self {
|
||||
self.commands = commands.into_iter().collect();
|
||||
self
|
||||
}
|
||||
|
||||
fn replace_value(&self, input: serde_json::Value) -> serde_json::Value {
|
||||
match input {
|
||||
serde_json::Value::String(s) => serde_json::Value::String(self.replace(&s)),
|
||||
serde_json::Value::Array(arr) => {
|
||||
serde_json::Value::Array(arr.into_iter().map(|v| self.replace_value(v)).collect())
|
||||
}
|
||||
serde_json::Value::Object(obj) => serde_json::Value::Object(
|
||||
obj.into_iter()
|
||||
.map(|(k, v)| (self.replace(&k), self.replace_value(v)))
|
||||
.collect(),
|
||||
),
|
||||
_ => input,
|
||||
}
|
||||
}
|
||||
// Replaces occurrences of VsCode-specific environment variables with Zed equivalents.
|
||||
fn replace(&self, input: &str) -> String {
|
||||
shellexpand::env_with_context_no_errors(&input, |var: &str| {
|
||||
// Colons denote a default value in case the variable is not set. We want to preserve that default, as otherwise shellexpand will substitute it for us.
|
||||
let colon_position = var.find(':').unwrap_or(var.len());
|
||||
let (left, right) = var.split_at(colon_position);
|
||||
if left == "env" && !right.is_empty() {
|
||||
let variable_name = &right[1..];
|
||||
return Some(format!("${{{variable_name}}}"));
|
||||
} else if left == "command" && !right.is_empty() {
|
||||
let command_name = &right[1..];
|
||||
if let Some(replacement_command) = self.commands.get(command_name) {
|
||||
return Some(format!("${{{replacement_command}}}"));
|
||||
}
|
||||
}
|
||||
|
||||
let (variable_name, default) = (left, right);
|
||||
let append_previous_default = |ret: &mut String| {
|
||||
if !default.is_empty() {
|
||||
ret.push_str(default);
|
||||
}
|
||||
};
|
||||
if let Some(substitution) = self.variables.get(variable_name) {
|
||||
// Got a VSCode->Zed hit, perform a substitution
|
||||
let mut name = format!("${{{substitution}");
|
||||
append_previous_default(&mut name);
|
||||
name.push('}');
|
||||
return Some(name);
|
||||
}
|
||||
// This is an unknown variable.
|
||||
// We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect.
|
||||
// If there's a default, we need to return the string verbatim as otherwise shellexpand will apply that default for us.
|
||||
if !default.is_empty() {
|
||||
return Some(format!("${{{var}}}"));
|
||||
}
|
||||
// Else we can just return None and that variable will be left as is.
|
||||
None
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
1154
crates/task/src/task_template.rs
Normal file
1154
crates/task/src/task_template.rs
Normal file
File diff suppressed because it is too large
Load Diff
194
crates/task/src/vscode_debug_format.rs
Normal file
194
crates/task/src/vscode_debug_format.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use collections::HashMap;
|
||||
use serde::Deserialize;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{
|
||||
DebugScenario, DebugTaskFile, EnvVariableReplacer, TcpArgumentsTemplate, VariableName,
|
||||
};
|
||||
|
||||
// TODO support preLaunchTask linkage with other tasks
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VsCodeDebugTaskDefinition {
|
||||
r#type: String,
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
port: Option<u16>,
|
||||
#[serde(flatten)]
|
||||
other_attributes: serde_json::Value,
|
||||
}
|
||||
|
||||
impl VsCodeDebugTaskDefinition {
|
||||
fn try_to_zed(mut self, replacer: &EnvVariableReplacer) -> anyhow::Result<DebugScenario> {
|
||||
let label = replacer.replace(&self.name);
|
||||
let mut config = replacer.replace_value(self.other_attributes);
|
||||
let adapter = task_type_to_adapter_name(&self.r#type);
|
||||
if let Some(config) = config.as_object_mut()
|
||||
&& adapter == "JavaScript"
|
||||
{
|
||||
config.insert("type".to_owned(), self.r#type.clone().into());
|
||||
if let Some(port) = self.port.take() {
|
||||
config.insert("port".to_owned(), port.into());
|
||||
}
|
||||
}
|
||||
let definition = DebugScenario {
|
||||
label: label.into(),
|
||||
build: None,
|
||||
adapter: adapter.into(),
|
||||
tcp_connection: self.port.map(|port| TcpArgumentsTemplate {
|
||||
port: Some(port),
|
||||
host: None,
|
||||
timeout: None,
|
||||
}),
|
||||
config,
|
||||
};
|
||||
Ok(definition)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VsCodeDebugTaskFile {
|
||||
#[serde(default)]
|
||||
version: Option<String>,
|
||||
configurations: Vec<VsCodeDebugTaskDefinition>,
|
||||
}
|
||||
|
||||
impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(file: VsCodeDebugTaskFile) -> Result<Self, Self::Error> {
|
||||
let replacer = EnvVariableReplacer::new(HashMap::from_iter([
|
||||
(
|
||||
"workspaceFolder".to_owned(),
|
||||
VariableName::WorktreeRoot.to_string(),
|
||||
),
|
||||
(
|
||||
"relativeFile".to_owned(),
|
||||
VariableName::RelativeFile.to_string(),
|
||||
),
|
||||
("file".to_owned(), VariableName::File.to_string()),
|
||||
]))
|
||||
.with_commands([(
|
||||
"pickMyProcess".to_owned(),
|
||||
VariableName::PickProcessId.to_string(),
|
||||
)]);
|
||||
let templates = file
|
||||
.configurations
|
||||
.into_iter()
|
||||
.filter_map(|config| config.try_to_zed(&replacer).log_err())
|
||||
.collect::<Vec<_>>();
|
||||
Ok(DebugTaskFile(templates))
|
||||
}
|
||||
}
|
||||
|
||||
fn task_type_to_adapter_name(task_type: &str) -> String {
|
||||
match task_type {
|
||||
"pwa-node" | "node" | "node-terminal" | "chrome" | "pwa-chrome" | "edge" | "pwa-edge"
|
||||
| "msedge" | "pwa-msedge" => "JavaScript",
|
||||
"go" => "Delve",
|
||||
"php" => "Xdebug",
|
||||
"cppdbg" | "lldb" => "CodeLLDB",
|
||||
"debugpy" => "Debugpy",
|
||||
"rdbg" => "rdbg",
|
||||
_ => task_type,
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{DebugScenario, DebugTaskFile, VariableName};
|
||||
|
||||
use super::VsCodeDebugTaskFile;
|
||||
|
||||
#[test]
|
||||
fn test_parsing_vscode_launch_json() {
|
||||
let raw = r#"
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug my JS app",
|
||||
"request": "launch",
|
||||
"type": "node",
|
||||
"program": "${workspaceFolder}/xyz.js",
|
||||
"showDevDebugOutput": false,
|
||||
"stopOnEntry": true,
|
||||
"args": ["--foo", "${workspaceFolder}/thing"],
|
||||
"cwd": "${workspaceFolder}/${env:FOO}/sub",
|
||||
"env": {
|
||||
"X": "Y"
|
||||
},
|
||||
"port": 17
|
||||
},
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let parsed: VsCodeDebugTaskFile =
|
||||
serde_json_lenient::from_str(raw).expect("deserializing launch.json");
|
||||
let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
|
||||
pretty_assertions::assert_eq!(
|
||||
zed,
|
||||
DebugTaskFile(vec![DebugScenario {
|
||||
label: "Debug my JS app".into(),
|
||||
adapter: "JavaScript".into(),
|
||||
config: json!({
|
||||
"request": "launch",
|
||||
"program": "${ZED_WORKTREE_ROOT}/xyz.js",
|
||||
"showDevDebugOutput": false,
|
||||
"stopOnEntry": true,
|
||||
"args": [
|
||||
"--foo",
|
||||
"${ZED_WORKTREE_ROOT}/thing",
|
||||
],
|
||||
"cwd": "${ZED_WORKTREE_ROOT}/${FOO}/sub",
|
||||
"env": {
|
||||
"X": "Y",
|
||||
},
|
||||
"type": "node",
|
||||
"port": 17,
|
||||
}),
|
||||
tcp_connection: None,
|
||||
build: None
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_pickmyprocess_replacement() {
|
||||
let raw = r#"
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach to Process",
|
||||
"request": "attach",
|
||||
"type": "cppdbg",
|
||||
"processId": "${command:pickMyProcess}"
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let parsed: VsCodeDebugTaskFile =
|
||||
serde_json_lenient::from_str(raw).expect("deserializing launch.json");
|
||||
let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
|
||||
|
||||
let expected_placeholder = format!("${{{}}}", VariableName::PickProcessId);
|
||||
pretty_assertions::assert_eq!(
|
||||
zed,
|
||||
DebugTaskFile(vec![DebugScenario {
|
||||
label: "Attach to Process".into(),
|
||||
adapter: "CodeLLDB".into(),
|
||||
config: json!({
|
||||
"request": "attach",
|
||||
"processId": expected_placeholder,
|
||||
}),
|
||||
tcp_connection: None,
|
||||
build: None
|
||||
}])
|
||||
);
|
||||
}
|
||||
}
|
||||
463
crates/task/src/vscode_format.rs
Normal file
463
crates/task/src/vscode_format.rs
Normal file
@@ -0,0 +1,463 @@
|
||||
use anyhow::bail;
|
||||
use collections::HashMap;
|
||||
use serde::Deserialize;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{EnvVariableReplacer, TaskTemplate, TaskTemplates, VariableName};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TaskOptions {
|
||||
cwd: Option<String>,
|
||||
#[serde(default)]
|
||||
env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct VsCodeTaskDefinition {
|
||||
label: String,
|
||||
command: Option<Command>,
|
||||
other_attributes: HashMap<String, serde_json_lenient::Value>,
|
||||
options: Option<TaskOptions>,
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for VsCodeTaskDefinition {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TaskHelper {
|
||||
#[serde(default)]
|
||||
label: Option<String>,
|
||||
#[serde(flatten)]
|
||||
command: Option<Command>,
|
||||
#[serde(flatten)]
|
||||
other_attributes: HashMap<String, serde_json_lenient::Value>,
|
||||
options: Option<TaskOptions>,
|
||||
}
|
||||
|
||||
let helper = TaskHelper::deserialize(deserializer)?;
|
||||
|
||||
let label = helper
|
||||
.label
|
||||
.unwrap_or_else(|| generate_label(&helper.command));
|
||||
|
||||
Ok(VsCodeTaskDefinition {
|
||||
label,
|
||||
command: helper.command,
|
||||
other_attributes: helper.other_attributes,
|
||||
options: helper.options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum Command {
|
||||
Npm {
|
||||
script: String,
|
||||
},
|
||||
Shell {
|
||||
command: String,
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
Gulp {
|
||||
task: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn generate_label(command: &Option<Command>) -> String {
|
||||
match command {
|
||||
Some(Command::Npm { script }) => format!("npm: {}", script),
|
||||
Some(Command::Gulp { task }) => format!("gulp: {}", task),
|
||||
Some(Command::Shell { command, .. }) => {
|
||||
if command.trim().is_empty() {
|
||||
"shell".to_string()
|
||||
} else {
|
||||
command.clone()
|
||||
}
|
||||
}
|
||||
None => "Untitled Task".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
impl VsCodeTaskDefinition {
|
||||
fn into_zed_format(
|
||||
self,
|
||||
replacer: &EnvVariableReplacer,
|
||||
) -> anyhow::Result<Option<TaskTemplate>> {
|
||||
if self.other_attributes.contains_key("dependsOn") {
|
||||
log::warn!(
|
||||
"Skipping deserializing of a task `{}` with the unsupported `dependsOn` key",
|
||||
self.label
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
// `type` might not be set in e.g. tasks that use `dependsOn`; we still want to deserialize the whole object though (hence command is an Option),
|
||||
// as that way we can provide more specific description of why deserialization failed.
|
||||
// E.g. if the command is missing due to `dependsOn` presence, we can check other_attributes first before doing this (and provide nice error message)
|
||||
// catch-all if on value.command presence.
|
||||
let Some(command) = self.command else {
|
||||
bail!("Missing `type` field in task");
|
||||
};
|
||||
|
||||
let (command, args) = match command {
|
||||
Command::Npm { script } => ("npm".to_owned(), vec!["run".to_string(), script]),
|
||||
Command::Shell { command, args } => (command, args),
|
||||
Command::Gulp { task } => ("gulp".to_owned(), vec![task]),
|
||||
};
|
||||
// Per VSC docs, only `command`, `args` and `options` support variable substitution.
|
||||
let command = replacer.replace(&command);
|
||||
let args = args.into_iter().map(|arg| replacer.replace(&arg)).collect();
|
||||
let mut template = TaskTemplate {
|
||||
label: self.label,
|
||||
command,
|
||||
args,
|
||||
..TaskTemplate::default()
|
||||
};
|
||||
if let Some(options) = self.options {
|
||||
template.cwd = options.cwd.map(|cwd| replacer.replace(&cwd));
|
||||
template.env = options.env;
|
||||
}
|
||||
Ok(Some(template))
|
||||
}
|
||||
}
|
||||
|
||||
/// [`VsCodeTaskFile`] is a superset of Code's task definition format.
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct VsCodeTaskFile {
|
||||
tasks: Vec<VsCodeTaskDefinition>,
|
||||
}
|
||||
|
||||
impl TryFrom<VsCodeTaskFile> for TaskTemplates {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: VsCodeTaskFile) -> Result<Self, Self::Error> {
|
||||
let replacer = EnvVariableReplacer::new(HashMap::from_iter([
|
||||
(
|
||||
"workspaceFolder".to_owned(),
|
||||
VariableName::WorktreeRoot.to_string(),
|
||||
),
|
||||
("file".to_owned(), VariableName::File.to_string()),
|
||||
("lineNumber".to_owned(), VariableName::Row.to_string()),
|
||||
(
|
||||
"selectedText".to_owned(),
|
||||
VariableName::SelectedText.to_string(),
|
||||
),
|
||||
]));
|
||||
let templates = value
|
||||
.tasks
|
||||
.into_iter()
|
||||
.filter_map(|vscode_definition| {
|
||||
vscode_definition
|
||||
.into_zed_format(&replacer)
|
||||
.log_err()
|
||||
.flatten()
|
||||
})
|
||||
.collect();
|
||||
Ok(Self(templates))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
TaskTemplate, TaskTemplates, VsCodeTaskFile,
|
||||
vscode_format::{Command, VsCodeTaskDefinition},
|
||||
};
|
||||
|
||||
use super::{EnvVariableReplacer, generate_label};
|
||||
|
||||
fn compare_without_other_attributes(lhs: VsCodeTaskDefinition, rhs: VsCodeTaskDefinition) {
|
||||
assert_eq!(
|
||||
VsCodeTaskDefinition {
|
||||
other_attributes: Default::default(),
|
||||
..lhs
|
||||
},
|
||||
VsCodeTaskDefinition {
|
||||
other_attributes: Default::default(),
|
||||
..rhs
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_variable_substitution() {
|
||||
let replacer = EnvVariableReplacer::new(Default::default());
|
||||
assert_eq!(replacer.replace("Food"), "Food");
|
||||
// Unknown variables are left in tact.
|
||||
assert_eq!(
|
||||
replacer.replace("$PATH is an environment variable"),
|
||||
"$PATH is an environment variable"
|
||||
);
|
||||
assert_eq!(replacer.replace("${PATH}"), "${PATH}");
|
||||
assert_eq!(replacer.replace("${PATH:food}"), "${PATH:food}");
|
||||
// And now, the actual replacing
|
||||
let replacer = EnvVariableReplacer::new(HashMap::from_iter([(
|
||||
"PATH".to_owned(),
|
||||
"ZED_PATH".to_owned(),
|
||||
)]));
|
||||
assert_eq!(replacer.replace("Food"), "Food");
|
||||
assert_eq!(
|
||||
replacer.replace("$PATH is an environment variable"),
|
||||
"${ZED_PATH} is an environment variable"
|
||||
);
|
||||
assert_eq!(replacer.replace("${PATH}"), "${ZED_PATH}");
|
||||
assert_eq!(replacer.replace("${PATH:food}"), "${ZED_PATH:food}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialize_ts_tasks() {
|
||||
const TYPESCRIPT_TASKS: &str = include_str!("../test_data/typescript.json");
|
||||
let vscode_definitions: VsCodeTaskFile =
|
||||
serde_json_lenient::from_str(TYPESCRIPT_TASKS).unwrap();
|
||||
|
||||
let expected = vec![
|
||||
VsCodeTaskDefinition {
|
||||
label: "gulp: tests".to_string(),
|
||||
command: Some(Command::Npm {
|
||||
script: "build:tests:notypecheck".to_string(),
|
||||
}),
|
||||
other_attributes: Default::default(),
|
||||
options: None,
|
||||
},
|
||||
VsCodeTaskDefinition {
|
||||
label: "tsc: watch ./src".to_string(),
|
||||
command: Some(Command::Shell {
|
||||
command: "node".to_string(),
|
||||
args: vec![
|
||||
"${workspaceFolder}/node_modules/typescript/lib/tsc.js".to_string(),
|
||||
"--build".to_string(),
|
||||
"${workspaceFolder}/src".to_string(),
|
||||
"--watch".to_string(),
|
||||
],
|
||||
}),
|
||||
other_attributes: Default::default(),
|
||||
options: None,
|
||||
},
|
||||
VsCodeTaskDefinition {
|
||||
label: "npm: build:compiler".to_string(),
|
||||
command: Some(Command::Npm {
|
||||
script: "build:compiler".to_string(),
|
||||
}),
|
||||
other_attributes: Default::default(),
|
||||
options: None,
|
||||
},
|
||||
VsCodeTaskDefinition {
|
||||
label: "npm: build:tests".to_string(),
|
||||
command: Some(Command::Npm {
|
||||
script: "build:tests:notypecheck".to_string(),
|
||||
}),
|
||||
other_attributes: Default::default(),
|
||||
options: None,
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(vscode_definitions.tasks.len(), expected.len());
|
||||
vscode_definitions
|
||||
.tasks
|
||||
.iter()
|
||||
.zip(expected)
|
||||
.for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs));
|
||||
|
||||
let expected = vec![
|
||||
TaskTemplate {
|
||||
label: "gulp: tests".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "build:tests:notypecheck".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "tsc: watch ./src".to_string(),
|
||||
command: "node".to_string(),
|
||||
args: vec![
|
||||
"${ZED_WORKTREE_ROOT}/node_modules/typescript/lib/tsc.js".to_string(),
|
||||
"--build".to_string(),
|
||||
"${ZED_WORKTREE_ROOT}/src".to_string(),
|
||||
"--watch".to_string(),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "npm: build:compiler".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "build:compiler".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "npm: build:tests".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "build:tests:notypecheck".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
let tasks: TaskTemplates = vscode_definitions.try_into().unwrap();
|
||||
assert_eq!(tasks.0, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialize_rust_analyzer_tasks() {
|
||||
const RUST_ANALYZER_TASKS: &str = include_str!("../test_data/rust-analyzer.json");
|
||||
let vscode_definitions: VsCodeTaskFile =
|
||||
serde_json_lenient::from_str(RUST_ANALYZER_TASKS).unwrap();
|
||||
let expected = vec![
|
||||
VsCodeTaskDefinition {
|
||||
label: "Build Extension in Background".to_string(),
|
||||
command: Some(Command::Npm {
|
||||
script: "watch".to_string(),
|
||||
}),
|
||||
options: None,
|
||||
other_attributes: Default::default(),
|
||||
},
|
||||
VsCodeTaskDefinition {
|
||||
label: "Build Extension".to_string(),
|
||||
command: Some(Command::Npm {
|
||||
script: "build".to_string(),
|
||||
}),
|
||||
options: None,
|
||||
other_attributes: Default::default(),
|
||||
},
|
||||
VsCodeTaskDefinition {
|
||||
label: "Build Server".to_string(),
|
||||
command: Some(Command::Shell {
|
||||
command: "cargo build --package rust-analyzer".to_string(),
|
||||
args: Default::default(),
|
||||
}),
|
||||
options: None,
|
||||
other_attributes: Default::default(),
|
||||
},
|
||||
VsCodeTaskDefinition {
|
||||
label: "Build Server (Release)".to_string(),
|
||||
command: Some(Command::Shell {
|
||||
command: "cargo build --release --package rust-analyzer".to_string(),
|
||||
args: Default::default(),
|
||||
}),
|
||||
options: None,
|
||||
other_attributes: Default::default(),
|
||||
},
|
||||
VsCodeTaskDefinition {
|
||||
label: "Pretest".to_string(),
|
||||
command: Some(Command::Npm {
|
||||
script: "pretest".to_string(),
|
||||
}),
|
||||
options: None,
|
||||
other_attributes: Default::default(),
|
||||
},
|
||||
VsCodeTaskDefinition {
|
||||
label: "Build Server and Extension".to_string(),
|
||||
command: None,
|
||||
options: None,
|
||||
other_attributes: Default::default(),
|
||||
},
|
||||
VsCodeTaskDefinition {
|
||||
label: "Build Server (Release) and Extension".to_string(),
|
||||
command: None,
|
||||
options: None,
|
||||
other_attributes: Default::default(),
|
||||
},
|
||||
];
|
||||
assert_eq!(vscode_definitions.tasks.len(), expected.len());
|
||||
vscode_definitions
|
||||
.tasks
|
||||
.iter()
|
||||
.zip(expected)
|
||||
.for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs));
|
||||
let expected = vec![
|
||||
TaskTemplate {
|
||||
label: "Build Extension in Background".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "watch".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "Build Extension".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "build".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "Build Server".to_string(),
|
||||
command: "cargo build --package rust-analyzer".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "Build Server (Release)".to_string(),
|
||||
command: "cargo build --release --package rust-analyzer".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "Pretest".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "pretest".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let tasks: TaskTemplates = vscode_definitions.try_into().unwrap();
|
||||
assert_eq!(tasks.0, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialize_tasks_without_labels() {
|
||||
const TASKS_WITHOUT_LABELS: &str = include_str!("../test_data/tasks-without-labels.json");
|
||||
let vscode_definitions: VsCodeTaskFile =
|
||||
serde_json_lenient::from_str(TASKS_WITHOUT_LABELS).unwrap();
|
||||
|
||||
assert_eq!(vscode_definitions.tasks.len(), 4);
|
||||
assert_eq!(vscode_definitions.tasks[0].label, "npm: start");
|
||||
assert_eq!(vscode_definitions.tasks[1].label, "Explicit Label");
|
||||
assert_eq!(vscode_definitions.tasks[2].label, "gulp: build");
|
||||
assert_eq!(vscode_definitions.tasks[3].label, "echo hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_label() {
|
||||
assert_eq!(
|
||||
generate_label(&Some(Command::Npm {
|
||||
script: "start".to_string()
|
||||
})),
|
||||
"npm: start"
|
||||
);
|
||||
assert_eq!(
|
||||
generate_label(&Some(Command::Gulp {
|
||||
task: "build".to_string()
|
||||
})),
|
||||
"gulp: build"
|
||||
);
|
||||
assert_eq!(
|
||||
generate_label(&Some(Command::Shell {
|
||||
command: "echo hello".to_string(),
|
||||
args: vec![]
|
||||
})),
|
||||
"echo hello"
|
||||
);
|
||||
assert_eq!(
|
||||
generate_label(&Some(Command::Shell {
|
||||
command: "cargo build --release".to_string(),
|
||||
args: vec![]
|
||||
})),
|
||||
"cargo build --release"
|
||||
);
|
||||
assert_eq!(
|
||||
generate_label(&Some(Command::Shell {
|
||||
command: " ".to_string(),
|
||||
args: vec![]
|
||||
})),
|
||||
"shell"
|
||||
);
|
||||
assert_eq!(
|
||||
generate_label(&Some(Command::Shell {
|
||||
command: "".to_string(),
|
||||
args: vec![]
|
||||
})),
|
||||
"shell"
|
||||
);
|
||||
assert_eq!(generate_label(&None), "Untitled Task");
|
||||
}
|
||||
}
|
||||
67
crates/task/test_data/rust-analyzer.json
Normal file
67
crates/task/test_data/rust-analyzer.json
Normal file
@@ -0,0 +1,67 @@
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build Extension in Background",
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"path": "editors/code/",
|
||||
"problemMatcher": {
|
||||
"base": "$tsc-watch",
|
||||
"fileLocation": ["relative", "${workspaceFolder}/editors/code/"]
|
||||
},
|
||||
"isBackground": true
|
||||
},
|
||||
{
|
||||
"label": "Build Extension",
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "build",
|
||||
"path": "editors/code/",
|
||||
"problemMatcher": {
|
||||
"base": "$tsc",
|
||||
"fileLocation": ["relative", "${workspaceFolder}/editors/code/"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build Server",
|
||||
"group": "build",
|
||||
"type": "shell",
|
||||
"command": "cargo build --package rust-analyzer",
|
||||
"problemMatcher": "$rustc"
|
||||
},
|
||||
{
|
||||
"label": "Build Server (Release)",
|
||||
"group": "build",
|
||||
"type": "shell",
|
||||
"command": "cargo build --release --package rust-analyzer",
|
||||
"problemMatcher": "$rustc"
|
||||
},
|
||||
{
|
||||
"label": "Pretest",
|
||||
"group": "build",
|
||||
"isBackground": false,
|
||||
"type": "npm",
|
||||
"script": "pretest",
|
||||
"path": "editors/code/",
|
||||
"problemMatcher": {
|
||||
"base": "$tsc",
|
||||
"fileLocation": ["relative", "${workspaceFolder}/editors/code/"]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"label": "Build Server and Extension",
|
||||
"dependsOn": ["Build Server", "Build Extension"],
|
||||
"problemMatcher": "$rustc"
|
||||
},
|
||||
{
|
||||
"label": "Build Server (Release) and Extension",
|
||||
"dependsOn": ["Build Server (Release)", "Build Extension"],
|
||||
"problemMatcher": "$rustc"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
crates/task/test_data/tasks-without-labels.json
Normal file
22
crates/task/test_data/tasks-without-labels.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start"
|
||||
},
|
||||
{
|
||||
"label": "Explicit Label",
|
||||
"type": "npm",
|
||||
"script": "test"
|
||||
},
|
||||
{
|
||||
"type": "gulp",
|
||||
"task": "build"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "echo hello"
|
||||
}
|
||||
]
|
||||
}
|
||||
51
crates/task/test_data/typescript.json
Normal file
51
crates/task/test_data/typescript.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
// Kept for backwards compat for old launch.json files so it's
|
||||
// less annoying if moving up to the new build or going back to
|
||||
// the old build.
|
||||
//
|
||||
// This is first because the actual "npm: build:tests" task
|
||||
// below has the same script value, and VS Code ignores labels
|
||||
// and deduplicates them.
|
||||
// https://github.com/microsoft/vscode/issues/93001
|
||||
"label": "gulp: tests",
|
||||
"type": "npm",
|
||||
"script": "build:tests:notypecheck",
|
||||
"group": "build",
|
||||
"hide": true,
|
||||
"problemMatcher": ["$tsc"]
|
||||
},
|
||||
{
|
||||
"label": "tsc: watch ./src",
|
||||
"type": "shell",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"${workspaceFolder}/node_modules/typescript/lib/tsc.js",
|
||||
"--build",
|
||||
"${workspaceFolder}/src",
|
||||
"--watch"
|
||||
],
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": ["$tsc-watch"]
|
||||
},
|
||||
{
|
||||
"label": "npm: build:compiler",
|
||||
"type": "npm",
|
||||
"script": "build:compiler",
|
||||
"group": "build",
|
||||
"problemMatcher": ["$tsc"]
|
||||
},
|
||||
{
|
||||
"label": "npm: build:tests",
|
||||
"type": "npm",
|
||||
"script": "build:tests:notypecheck",
|
||||
"group": "build",
|
||||
"problemMatcher": ["$tsc"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user