diff --git a/Cargo.lock b/Cargo.lock index 49dcb57ef133612ab807ab7c84a25b070c9e1241..b81a1a8d317a21646b8c456943f0d01aa56904dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5532,6 +5532,7 @@ dependencies = [ "ctor", "dap", "extension", + "extension_cli", "fs", "futures 0.3.31", "gpui", @@ -5566,6 +5567,7 @@ dependencies = [ "wasmparser 0.221.3", "wasmtime", "wasmtime-wasi", + "workspace", "workspace-hack", "zlog", ] @@ -20405,6 +20407,7 @@ dependencies = [ "editor", "env_logger 0.11.8", "extension", + "extension_cli", "extension_host", "extensions_ui", "feature_flags", @@ -20493,6 +20496,7 @@ dependencies = [ "theme_selector", "time", "title_bar", + "tokio", "toolchain_selector", "tree-sitter-md", "tree-sitter-rust", diff --git a/Cargo.toml b/Cargo.toml index c8d555a24278a77a9dbd0f649ee75d6d04efcba0..afda980a7dd6510a1f473f71e5cd60cb0b9bfc87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -286,6 +286,7 @@ deepseek = { path = "crates/deepseek" } diagnostics = { path = "crates/diagnostics" } editor = { path = "crates/editor" } extension = { path = "crates/extension" } +extension_cli = { path = "crates/extension_cli" } extension_host = { path = "crates/extension_host" } extensions_ui = { path = "crates/extensions_ui" } feature_flags = { path = "crates/feature_flags" } diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 3a3026f19c1961a6f4ac4c7fe5ac217ef6855cea..ac902742574b89edc895eb7fb71fc29ec8a46e69 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -151,7 +151,7 @@ impl ExtensionBuilder { "compiling Rust crate for extension {}", extension_dir.display() ); - let output = util::command::new_std_command("cargo") + let status = util::command::new_std_command("cargo") .args(["build", "--target", RUST_TARGET]) .args(options.release.then_some("--release")) .arg("--target-dir") @@ -159,13 +159,10 @@ impl ExtensionBuilder { // WASI builds do not work with sccache and just stuck, so disable it. .env("RUSTC_WRAPPER", "") .current_dir(extension_dir) - .output() + .status() .context("failed to run `cargo`")?; - if !output.status.success() { - bail!( - "failed to build extension {}", - String::from_utf8_lossy(&output.stderr) - ); + if !status.success() { + bail!("failed to build extension",); } log::info!( @@ -248,7 +245,7 @@ impl ExtensionBuilder { let scanner_path = src_path.join("scanner.c"); log::info!("compiling {grammar_name} parser"); - let clang_output = util::command::new_std_command(&clang_path) + let clang_status = util::command::new_std_command(&clang_path) .args(["-fPIC", "-shared", "-Os"]) .arg(format!("-Wl,--export=tree_sitter_{grammar_name}")) .arg("-o") @@ -257,15 +254,11 @@ impl ExtensionBuilder { .arg(&src_path) .arg(&parser_path) .args(scanner_path.exists().then_some(scanner_path)) - .output() + .status() .context("failed to run clang")?; - if !clang_output.status.success() { - bail!( - "failed to compile {} parser with clang: {}", - grammar_name, - String::from_utf8_lossy(&clang_output.stderr), - ); + if !clang_status.success() { + bail!("failed to compile {} parser with clang", grammar_name,); } Ok(()) diff --git a/crates/extension_cli/Cargo.toml b/crates/extension_cli/Cargo.toml index b2909ec6c9c281012f7814a39d5571baadce1bab..68053e539030f296a80731d04dc3e2b77a7d5c82 100644 --- a/crates/extension_cli/Cargo.toml +++ b/crates/extension_cli/Cargo.toml @@ -12,6 +12,10 @@ workspace = true name = "zed-extension" path = "src/main.rs" +[lib] +name = "extension_cli" +path = "src/extension_cli.rs" + [dependencies] anyhow.workspace = true clap = { workspace = true, features = ["derive"] } diff --git a/crates/extension_cli/src/extension_cli.rs b/crates/extension_cli/src/extension_cli.rs new file mode 100644 index 0000000000000000000000000000000000000000..0e3941d74ef6e67b6d7cb6807b30853f8f2e7307 --- /dev/null +++ b/crates/extension_cli/src/extension_cli.rs @@ -0,0 +1,359 @@ +use std::collections::{BTreeSet, HashMap}; +use std::env; +use std::ffi::OsString; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Arc; + +use ::fs::{CopyOptions, Fs, RealFs, copy_recursive}; +use anyhow::{Context as _, Result, anyhow, bail}; +use clap::Parser; +use extension::ExtensionManifest; +use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; +use language::LanguageConfig; +use reqwest_client::ReqwestClient; +use rpc::ExtensionProvides; +use tree_sitter::{Language, Query, WasmStore}; + +pub struct WrappedExtensionBuilder {} + +impl WrappedExtensionBuilder { + pub fn new(build_dir: PathBuf) -> Self { + Self {} + } +} + +#[derive(Parser, Debug)] +#[command(name = "zed-extension")] +struct Args { + /// The path to the extension directory + #[arg(long)] + source_dir: PathBuf, + /// The output directory to place the packaged extension. + #[arg(long)] + output_dir: PathBuf, + /// The path to a directory where build dependencies are downloaded + #[arg(long)] + scratch_dir: PathBuf, +} + +pub async fn run(extension_path: PathBuf, scratch_dir: PathBuf) -> Result<()> { + env_logger::init(); + + let fs = Arc::new(RealFs::new(None, gpui::background_executor())); + let engine = wasmtime::Engine::default(); + let mut wasm_store = WasmStore::new(&engine)?; + + log::info!("loading extension manifest"); + let mut manifest = ExtensionManifest::load(fs.clone(), &extension_path).await?; + + log::info!("compiling extension"); + + let user_agent = format!( + "Zed Extension CLI/{} ({}; {})", + env!("CARGO_PKG_VERSION"), + std::env::consts::OS, + std::env::consts::ARCH + ); + let http_client = Arc::new(ReqwestClient::user_agent(&user_agent)?); + + let builder = ExtensionBuilder::new(http_client, scratch_dir); + builder + .compile_extension( + &extension_path, + &mut manifest, + CompileExtensionOptions { release: true }, + ) + .await + .context("failed to compile extension")?; + + let grammars = test_grammars(&manifest, &extension_path, &mut wasm_store)?; + test_languages(&manifest, &extension_path, &grammars)?; + test_themes(&manifest, &extension_path, fs.clone()).await?; + + Ok(()) +} + +/// Returns the set of features provided by the extension. +fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet { + let mut provides = BTreeSet::default(); + if !manifest.themes.is_empty() { + provides.insert(ExtensionProvides::Themes); + } + + if !manifest.icon_themes.is_empty() { + provides.insert(ExtensionProvides::IconThemes); + } + + if !manifest.languages.is_empty() { + provides.insert(ExtensionProvides::Languages); + } + + if !manifest.grammars.is_empty() { + provides.insert(ExtensionProvides::Grammars); + } + + if !manifest.language_servers.is_empty() { + provides.insert(ExtensionProvides::LanguageServers); + } + + if !manifest.context_servers.is_empty() { + provides.insert(ExtensionProvides::ContextServers); + } + + if manifest.snippets.is_some() { + provides.insert(ExtensionProvides::Snippets); + } + + if !manifest.debug_adapters.is_empty() { + provides.insert(ExtensionProvides::DebugAdapters); + } + + provides +} + +async fn copy_extension_resources( + manifest: &ExtensionManifest, + extension_path: &Path, + output_dir: &Path, + fs: Arc, +) -> Result<()> { + fs::create_dir_all(output_dir).context("failed to create output dir")?; + + let manifest_toml = toml::to_string(&manifest).context("failed to serialize manifest")?; + fs::write(output_dir.join("extension.toml"), &manifest_toml) + .context("failed to write extension.toml")?; + + if manifest.lib.kind.is_some() { + fs::copy( + extension_path.join("extension.wasm"), + output_dir.join("extension.wasm"), + ) + .context("failed to copy extension.wasm")?; + } + + if !manifest.grammars.is_empty() { + let source_grammars_dir = extension_path.join("grammars"); + let output_grammars_dir = output_dir.join("grammars"); + fs::create_dir_all(&output_grammars_dir)?; + for grammar_name in manifest.grammars.keys() { + let mut grammar_filename = PathBuf::from(grammar_name.as_ref()); + grammar_filename.set_extension("wasm"); + fs::copy( + source_grammars_dir.join(&grammar_filename), + output_grammars_dir.join(&grammar_filename), + ) + .with_context(|| format!("failed to copy grammar '{}'", grammar_filename.display()))?; + } + } + + if !manifest.themes.is_empty() { + let output_themes_dir = output_dir.join("themes"); + fs::create_dir_all(&output_themes_dir)?; + for theme_path in &manifest.themes { + fs::copy( + extension_path.join(theme_path), + output_themes_dir.join(theme_path.file_name().context("invalid theme path")?), + ) + .with_context(|| format!("failed to copy theme '{}'", theme_path.display()))?; + } + } + + if !manifest.icon_themes.is_empty() { + let output_icon_themes_dir = output_dir.join("icon_themes"); + fs::create_dir_all(&output_icon_themes_dir)?; + for icon_theme_path in &manifest.icon_themes { + fs::copy( + extension_path.join(icon_theme_path), + output_icon_themes_dir.join( + icon_theme_path + .file_name() + .context("invalid icon theme path")?, + ), + ) + .with_context(|| { + format!("failed to copy icon theme '{}'", icon_theme_path.display()) + })?; + } + + let output_icons_dir = output_dir.join("icons"); + fs::create_dir_all(&output_icons_dir)?; + copy_recursive( + fs.as_ref(), + &extension_path.join("icons"), + &output_icons_dir, + CopyOptions { + overwrite: true, + ignore_if_exists: false, + }, + ) + .await + .with_context(|| "failed to copy icons")?; + } + + if !manifest.languages.is_empty() { + let output_languages_dir = output_dir.join("languages"); + fs::create_dir_all(&output_languages_dir)?; + for language_path in &manifest.languages { + copy_recursive( + fs.as_ref(), + &extension_path.join(language_path), + &output_languages_dir + .join(language_path.file_name().context("invalid language path")?), + CopyOptions { + overwrite: true, + ignore_if_exists: false, + }, + ) + .await + .with_context(|| { + format!("failed to copy language dir '{}'", language_path.display()) + })?; + } + } + + if !manifest.debug_adapters.is_empty() { + for (debug_adapter, entry) in &manifest.debug_adapters { + let schema_path = entry.schema_path.clone().unwrap_or_else(|| { + PathBuf::from("debug_adapter_schemas".to_owned()) + .join(debug_adapter.as_ref()) + .with_extension("json") + }); + let parent = schema_path + .parent() + .with_context(|| format!("invalid empty schema path for {debug_adapter}"))?; + fs::create_dir_all(output_dir.join(parent))?; + copy_recursive( + fs.as_ref(), + &extension_path.join(&schema_path), + &output_dir.join(&schema_path), + CopyOptions { + overwrite: true, + ignore_if_exists: false, + }, + ) + .await + .with_context(|| { + format!( + "failed to copy debug adapter schema '{}'", + schema_path.display() + ) + })?; + } + } + + if let Some(snippets_path) = manifest.snippets.as_ref() { + let parent = snippets_path.parent(); + if let Some(parent) = parent.filter(|p| p.components().next().is_some()) { + fs::create_dir_all(output_dir.join(parent))?; + } + copy_recursive( + fs.as_ref(), + &extension_path.join(&snippets_path), + &output_dir.join(&snippets_path), + CopyOptions { + overwrite: true, + ignore_if_exists: false, + }, + ) + .await + .with_context(|| format!("failed to copy snippets from '{}'", snippets_path.display()))?; + } + + Ok(()) +} + +fn test_grammars( + manifest: &ExtensionManifest, + extension_path: &Path, + wasm_store: &mut WasmStore, +) -> Result> { + let mut grammars = HashMap::default(); + let grammars_dir = extension_path.join("grammars"); + + for grammar_name in manifest.grammars.keys() { + let mut grammar_path = grammars_dir.join(grammar_name.as_ref()); + grammar_path.set_extension("wasm"); + + let wasm = fs::read(&grammar_path)?; + let language = wasm_store.load_language(grammar_name, &wasm)?; + log::info!("loaded grammar {grammar_name}"); + grammars.insert(grammar_name.to_string(), language); + } + + Ok(grammars) +} + +fn test_languages( + manifest: &ExtensionManifest, + extension_path: &Path, + grammars: &HashMap, +) -> Result<()> { + for relative_language_dir in &manifest.languages { + let language_dir = extension_path.join(relative_language_dir); + let config_path = language_dir.join("config.toml"); + let config_content = fs::read_to_string(&config_path)?; + let config: LanguageConfig = toml::from_str(&config_content)?; + let grammar = if let Some(name) = &config.grammar { + Some( + grammars + .get(name.as_ref()) + .with_context(|| format!("grammar not found: '{name}'"))?, + ) + } else { + None + }; + + let query_entries = fs::read_dir(&language_dir)?; + for entry in query_entries { + let entry = entry?; + let query_path = entry.path(); + if query_path.extension() == Some("scm".as_ref()) { + let grammar = grammar.with_context(|| { + format! { + "language {} provides query {} but no grammar", + config.name, + query_path.display() + } + })?; + + let query_source = fs::read_to_string(&query_path)?; + let _query = Query::new(grammar, &query_source) + .map_err(|err| anyhow!("{}: {}", query_path.display(), err))?; + } + } + + log::info!("loaded language {}", config.name); + } + + Ok(()) +} + +async fn test_themes( + manifest: &ExtensionManifest, + extension_path: &Path, + fs: Arc, +) -> Result<()> { + for relative_theme_path in &manifest.themes { + let theme_path = extension_path.join(relative_theme_path); + let theme_family = theme::read_user_theme(&theme_path, fs.clone()).await?; + log::info!("loaded theme family {}", theme_family.name); + + for theme in &theme_family.themes { + if theme + .style + .colors + .deprecated_scrollbar_thumb_background + .is_some() + { + bail!( + r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#, + theme_name = theme.name + ) + } + } + } + + Ok(()) +} diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index d6c0501efdacff2a9eaf542695ed44325908ea56..a140f82d020d738c658fb121ca4344a14891bd6b 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -1,84 +1,23 @@ -use std::collections::{BTreeSet, HashMap}; -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::sync::Arc; - -use ::fs::{CopyOptions, Fs, RealFs, copy_recursive}; -use anyhow::{Context as _, Result, bail}; -use clap::Parser; -use extension::ExtensionManifest; -use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; -use language::LanguageConfig; -use reqwest_client::ReqwestClient; -use rpc::ExtensionProvides; -use tree_sitter::{Language, Query, WasmStore}; - -#[derive(Parser, Debug)] -#[command(name = "zed-extension")] -struct Args { - /// The path to the extension directory - #[arg(long)] - source_dir: PathBuf, - /// The output directory to place the packaged extension. - #[arg(long)] - output_dir: PathBuf, - /// The path to a directory where build dependencies are downloaded - #[arg(long)] - scratch_dir: PathBuf, -} +use std::process; #[tokio::main] -async fn main() -> Result<()> { - env_logger::init(); - +pub async fn main() -> Result<()> { let args = Args::parse(); - let fs = Arc::new(RealFs::new(None, gpui::background_executor())); - let engine = wasmtime::Engine::default(); - let mut wasm_store = WasmStore::new(&engine)?; - let extension_path = args - .source_dir + let source_dir = source_dir .canonicalize() .context("failed to canonicalize source_dir")?; - let scratch_dir = args - .scratch_dir + let scratch_dir = scratch_dir .canonicalize() .context("failed to canonicalize scratch_dir")?; + extension_cli::run(source_dir, scratch_dir).await; + let output_dir = if args.output_dir.is_relative() { env::current_dir()?.join(&args.output_dir) } else { args.output_dir }; - log::info!("loading extension manifest"); - let mut manifest = ExtensionManifest::load(fs.clone(), &extension_path).await?; - - log::info!("compiling extension"); - - let user_agent = format!( - "Zed Extension CLI/{} ({}; {})", - env!("CARGO_PKG_VERSION"), - std::env::consts::OS, - std::env::consts::ARCH - ); - let http_client = Arc::new(ReqwestClient::user_agent(&user_agent)?); - - let builder = ExtensionBuilder::new(http_client, scratch_dir); - builder - .compile_extension( - &extension_path, - &mut manifest, - CompileExtensionOptions { release: true }, - ) - .await - .context("failed to compile extension")?; - - let grammars = test_grammars(&manifest, &extension_path, &mut wasm_store)?; - test_languages(&manifest, &extension_path, &grammars)?; - test_themes(&manifest, &extension_path, fs.clone()).await?; - let archive_dir = output_dir.join("archive"); fs::remove_dir_all(&archive_dir).ok(); copy_extension_resources(&manifest, &extension_path, &archive_dir, fs.clone()) @@ -113,288 +52,4 @@ async fn main() -> Result<()> { })?; fs::remove_dir_all(&archive_dir)?; fs::write(output_dir.join("manifest.json"), manifest_json.as_bytes())?; - - Ok(()) -} - -/// Returns the set of features provided by the extension. -fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet { - let mut provides = BTreeSet::default(); - if !manifest.themes.is_empty() { - provides.insert(ExtensionProvides::Themes); - } - - if !manifest.icon_themes.is_empty() { - provides.insert(ExtensionProvides::IconThemes); - } - - if !manifest.languages.is_empty() { - provides.insert(ExtensionProvides::Languages); - } - - if !manifest.grammars.is_empty() { - provides.insert(ExtensionProvides::Grammars); - } - - if !manifest.language_servers.is_empty() { - provides.insert(ExtensionProvides::LanguageServers); - } - - if !manifest.context_servers.is_empty() { - provides.insert(ExtensionProvides::ContextServers); - } - - if manifest.snippets.is_some() { - provides.insert(ExtensionProvides::Snippets); - } - - if !manifest.debug_adapters.is_empty() { - provides.insert(ExtensionProvides::DebugAdapters); - } - - provides -} - -async fn copy_extension_resources( - manifest: &ExtensionManifest, - extension_path: &Path, - output_dir: &Path, - fs: Arc, -) -> Result<()> { - fs::create_dir_all(output_dir).context("failed to create output dir")?; - - let manifest_toml = toml::to_string(&manifest).context("failed to serialize manifest")?; - fs::write(output_dir.join("extension.toml"), &manifest_toml) - .context("failed to write extension.toml")?; - - if manifest.lib.kind.is_some() { - fs::copy( - extension_path.join("extension.wasm"), - output_dir.join("extension.wasm"), - ) - .context("failed to copy extension.wasm")?; - } - - if !manifest.grammars.is_empty() { - let source_grammars_dir = extension_path.join("grammars"); - let output_grammars_dir = output_dir.join("grammars"); - fs::create_dir_all(&output_grammars_dir)?; - for grammar_name in manifest.grammars.keys() { - let mut grammar_filename = PathBuf::from(grammar_name.as_ref()); - grammar_filename.set_extension("wasm"); - fs::copy( - source_grammars_dir.join(&grammar_filename), - output_grammars_dir.join(&grammar_filename), - ) - .with_context(|| format!("failed to copy grammar '{}'", grammar_filename.display()))?; - } - } - - if !manifest.themes.is_empty() { - let output_themes_dir = output_dir.join("themes"); - fs::create_dir_all(&output_themes_dir)?; - for theme_path in &manifest.themes { - fs::copy( - extension_path.join(theme_path), - output_themes_dir.join(theme_path.file_name().context("invalid theme path")?), - ) - .with_context(|| format!("failed to copy theme '{}'", theme_path.display()))?; - } - } - - if !manifest.icon_themes.is_empty() { - let output_icon_themes_dir = output_dir.join("icon_themes"); - fs::create_dir_all(&output_icon_themes_dir)?; - for icon_theme_path in &manifest.icon_themes { - fs::copy( - extension_path.join(icon_theme_path), - output_icon_themes_dir.join( - icon_theme_path - .file_name() - .context("invalid icon theme path")?, - ), - ) - .with_context(|| { - format!("failed to copy icon theme '{}'", icon_theme_path.display()) - })?; - } - - let output_icons_dir = output_dir.join("icons"); - fs::create_dir_all(&output_icons_dir)?; - copy_recursive( - fs.as_ref(), - &extension_path.join("icons"), - &output_icons_dir, - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| "failed to copy icons")?; - } - - if !manifest.languages.is_empty() { - let output_languages_dir = output_dir.join("languages"); - fs::create_dir_all(&output_languages_dir)?; - for language_path in &manifest.languages { - copy_recursive( - fs.as_ref(), - &extension_path.join(language_path), - &output_languages_dir - .join(language_path.file_name().context("invalid language path")?), - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| { - format!("failed to copy language dir '{}'", language_path.display()) - })?; - } - } - - if !manifest.debug_adapters.is_empty() { - for (debug_adapter, entry) in &manifest.debug_adapters { - let schema_path = entry.schema_path.clone().unwrap_or_else(|| { - PathBuf::from("debug_adapter_schemas".to_owned()) - .join(debug_adapter.as_ref()) - .with_extension("json") - }); - let parent = schema_path - .parent() - .with_context(|| format!("invalid empty schema path for {debug_adapter}"))?; - fs::create_dir_all(output_dir.join(parent))?; - copy_recursive( - fs.as_ref(), - &extension_path.join(&schema_path), - &output_dir.join(&schema_path), - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| { - format!( - "failed to copy debug adapter schema '{}'", - schema_path.display() - ) - })?; - } - } - - if let Some(snippets_path) = manifest.snippets.as_ref() { - let parent = snippets_path.parent(); - if let Some(parent) = parent.filter(|p| p.components().next().is_some()) { - fs::create_dir_all(output_dir.join(parent))?; - } - copy_recursive( - fs.as_ref(), - &extension_path.join(&snippets_path), - &output_dir.join(&snippets_path), - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| format!("failed to copy snippets from '{}'", snippets_path.display()))?; - } - - Ok(()) -} - -fn test_grammars( - manifest: &ExtensionManifest, - extension_path: &Path, - wasm_store: &mut WasmStore, -) -> Result> { - let mut grammars = HashMap::default(); - let grammars_dir = extension_path.join("grammars"); - - for grammar_name in manifest.grammars.keys() { - let mut grammar_path = grammars_dir.join(grammar_name.as_ref()); - grammar_path.set_extension("wasm"); - - let wasm = fs::read(&grammar_path)?; - let language = wasm_store.load_language(grammar_name, &wasm)?; - log::info!("loaded grammar {grammar_name}"); - grammars.insert(grammar_name.to_string(), language); - } - - Ok(grammars) -} - -fn test_languages( - manifest: &ExtensionManifest, - extension_path: &Path, - grammars: &HashMap, -) -> Result<()> { - for relative_language_dir in &manifest.languages { - let language_dir = extension_path.join(relative_language_dir); - let config_path = language_dir.join("config.toml"); - let config_content = fs::read_to_string(&config_path)?; - let config: LanguageConfig = toml::from_str(&config_content)?; - let grammar = if let Some(name) = &config.grammar { - Some( - grammars - .get(name.as_ref()) - .with_context(|| format!("grammar not found: '{name}'"))?, - ) - } else { - None - }; - - let query_entries = fs::read_dir(&language_dir)?; - for entry in query_entries { - let entry = entry?; - let query_path = entry.path(); - if query_path.extension() == Some("scm".as_ref()) { - let grammar = grammar.with_context(|| { - format! { - "language {} provides query {} but no grammar", - config.name, - query_path.display() - } - })?; - - let query_source = fs::read_to_string(&query_path)?; - let _query = Query::new(grammar, &query_source)?; - } - } - - log::info!("loaded language {}", config.name); - } - - Ok(()) -} - -async fn test_themes( - manifest: &ExtensionManifest, - extension_path: &Path, - fs: Arc, -) -> Result<()> { - for relative_theme_path in &manifest.themes { - let theme_path = extension_path.join(relative_theme_path); - let theme_family = theme::read_user_theme(&theme_path, fs.clone()).await?; - log::info!("loaded theme family {}", theme_family.name); - - for theme in &theme_family.themes { - if theme - .style - .colors - .deprecated_scrollbar_thumb_background - .is_some() - { - bail!( - r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#, - theme_name = theme.name - ) - } - } - } - - Ok(()) } diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index c933d253c65b525b29eb072ce6910514b15e5932..e4559a0e4bc4eab9f1e9f8e61c194482d5e3e6eb 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -24,6 +24,7 @@ client.workspace = true collections.workspace = true dap.workspace = true extension.workspace = true +extension_cli.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true @@ -52,6 +53,7 @@ util.workspace = true wasmparser.workspace = true wasmtime-wasi.workspace = true wasmtime.workspace = true +workspace.workspace = true workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index b114ad9f4c526f9c270681c55626455531becc2f..d403c25d3dd9039c6f23002bd056a73911dbcdac 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -33,7 +33,7 @@ use futures::{ }; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity, - actions, + Window, actions, }; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use language::{ @@ -55,12 +55,14 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use task::SpawnInTerminal; use url::Url; use util::{ResultExt, paths::RemotePathBuf}; use wasm_host::{ WasmExtension, WasmHost, wit::{is_supported_wasm_api_version, wasm_api_version_range}, }; +use workspace::Workspace; pub use extension::{ ExtensionLibraryKind, GrammarManifestEntry, OldExtensionManifest, SchemaVersion, @@ -1005,27 +1007,58 @@ impl ExtensionStore { }) } - pub fn rebuild_dev_extension(&mut self, extension_id: Arc, cx: &mut Context) { + pub fn rebuild_dev_extension( + &mut self, + extension_id: Arc, + window: &mut Window, + cx: &mut Context, + ) { + let Some(Some(workspace)) = window.root::() else { + return; + }; let path = self.installed_dir.join(extension_id.as_ref()); - let builder = self.builder.clone(); - let fs = self.fs.clone(); - + let compile = workspace.update(cx, |workspace, cx| { + workspace.spawn_in_terminal( + SpawnInTerminal { + id: task::TaskId(format!("rebuild-{}", extension_id)), + full_label: format!("Rebuild extension {}", extension_id), + label: format!("Rebuild extension {}", extension_id), + command: None, + args: Default::default(), + command_label: format!("Rebuild extension {}", extension_id), + cwd: Some(path.clone()), + env: [("RUST_LOG".into(), "info".into())].into_iter().collect(), + use_new_terminal: false, + allow_concurrent_runs: false, + reveal: task::RevealStrategy::Always, + reveal_target: task::RevealTarget::Dock, + hide: task::HideStrategy::Never, + shell: task::Shell::WithArguments { + // todo!() + program: std::env::current_exe() + .ok() + .map(|p| p.to_string_lossy().to_string()) + .unwrap(), + args: dbg!(vec![ + "--build-extension".into(), + path.to_string_lossy().to_string(), + ]), + title_override: None, + }, + show_summary: true, + show_command: true, + show_rerun: true, + }, + window, + cx, + ) + }); match self.outstanding_operations.entry(extension_id.clone()) { btree_map::Entry::Occupied(_) => return, btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Upgrade), }; cx.notify(); - let compile = cx.background_spawn(async move { - let mut manifest = ExtensionManifest::load(fs, &path).await?; - builder - .compile_extension( - &path, - &mut manifest, - CompileExtensionOptions { release: true }, - ) - .await - }); cx.spawn(async move |this, cx| { let result = compile.await; @@ -1035,12 +1068,12 @@ impl ExtensionStore { cx.notify(); })?; - if result.is_ok() { + if let Some(Ok(_)) = &result { this.update(cx, |this, cx| this.reload(Some(extension_id), cx))? .await; } - result + result.ok_or_else(|| anyhow!("rebuild cancelled"))? }) .detach_and_log_err(cx) } diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 0b925dceb1544d97a77082881626bc1e97f3d1b0..f75f2a6fdc5c0981195d632650d294514e6638e2 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -602,9 +602,9 @@ impl ExtensionsPage { .disabled(matches!(status, ExtensionStatus::Upgrading)) .on_click({ let extension_id = extension.id.clone(); - move |_, _, cx| { + move |_, window, cx| { ExtensionStore::global(cx).update(cx, |store, cx| { - store.rebuild_dev_extension(extension_id.clone(), cx) + store.rebuild_dev_extension(extension_id.clone(), window, cx) }); } }), diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bee6c87670c87a08945918a3dd49b26463a3a3ef..374f186dfc87efad9bf6c4d8f73b4af3aa83344a 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -57,6 +57,7 @@ diagnostics.workspace = true editor.workspace = true env_logger.workspace = true extension.workspace = true +extension_cli.workspace = true extension_host.workspace = true extensions_ui.workspace = true feature_flags.workspace = true @@ -151,6 +152,7 @@ theme_selector.workspace = true time.workspace = true title_bar.workspace = true toolchain_selector.workspace = true +tokio.workspace = true ui.workspace = true ui_input.workspace = true ui_prompt.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3287e866e48058a763c7db6633c1db4252fc0bec..debd3ca3a671f82bc493aa6b72b73ef7501cf2f3 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -37,6 +37,7 @@ use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file}; use std::{ env, io::{self, IsTerminal}, + os, path::{Path, PathBuf}, process, sync::Arc, @@ -183,6 +184,22 @@ pub fn main() { return; } + // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass + if let Some(source_dir) = args.build_extension { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + let scratch_dir = source_dir.join("build"); + if let Err(err) = extension_cli::run(source_dir, scratch_dir).await { + eprintln!("{:?}", err); + process::exit(1); + } + }); + return; + } + // `zed --nc` Makes zed operate in nc/netcat mode for use with MCP if let Some(socket) = &args.nc { match nc::main(socket) { @@ -1226,6 +1243,10 @@ struct Args { #[arg(long, hide = true)] crash_handler: Option, + /// Used for running the extension CLI + #[arg(long, hide = true)] + build_extension: Option, + /// Run zed in the foreground, only used on Windows, to match the behavior on macOS. #[arg(long)] #[cfg(target_os = "windows")]