diff --git a/Cargo.lock b/Cargo.lock index e345736295..894dd00f6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7190,6 +7190,7 @@ dependencies = [ "async-std", "async-tar", "async-trait", + "async-watch", "async_zip", "futures 0.3.30", "http_client", @@ -7202,6 +7203,7 @@ dependencies = [ "tempfile", "util", "walkdir", + "which 6.0.3", "windows 0.58.0", ] @@ -14393,6 +14395,7 @@ dependencies = [ "ashpd", "assets", "assistant", + "async-watch", "audio", "auto_update", "backtrace", @@ -14466,6 +14469,7 @@ dependencies = [ "session", "settings", "settings_ui", + "shellexpand 2.1.2", "simplelog", "smol", "snippet_provider", diff --git a/assets/settings/default.json b/assets/settings/default.json index e04ab90f21..3e8d3c8c70 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -771,6 +771,21 @@ "pyrightconfig.json" ] }, + /// By default use a recent system version of node, or install our own. + /// You can override this to use a version of node that is not in $PATH with: + /// { + /// "node": { + /// "node_path": "/path/to/node" + /// "npm_path": "/path/to/npm" (defaults to node_path/../npm) + /// } + /// } + /// or to ensure Zed always downloads and installs an isolated version of node: + /// { + /// "node": { + /// "disable_path_lookup": true + /// } + /// NOTE: changing this setting currently requires restarting Zed. + "node": {}, // The extensions that Zed should automatically install on startup. // // If you don't want any of these extensions, add this field to your settings diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 1421e4c7f7..6f07d76b0b 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -21,7 +21,7 @@ use git::GitHostingProviderRegistry; use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext}; use http_client::FakeHttpClient; use language::LanguageRegistry; -use node_runtime::FakeNodeRuntime; +use node_runtime::NodeRuntime; use notifications::NotificationStore; use parking_lot::Mutex; use project::{Project, WorktreeId}; @@ -278,7 +278,7 @@ impl TestServer { languages: language_registry, fs: fs.clone(), build_window_options: |_, _| Default::default(), - node_runtime: FakeNodeRuntime::new(), + node_runtime: NodeRuntime::unavailable(), session, }); @@ -408,7 +408,7 @@ impl TestServer { languages: language_registry, fs: fs.clone(), build_window_options: |_, _| Default::default(), - node_runtime: FakeNodeRuntime::new(), + node_runtime: NodeRuntime::unavailable(), session, }); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index cdbe65ba1d..a1fd7a9bb9 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -57,7 +57,7 @@ pub fn init( new_server_id: LanguageServerId, fs: Arc, http: Arc, - node_runtime: Arc, + node_runtime: NodeRuntime, cx: &mut AppContext, ) { copilot_chat::init(fs, http.clone(), cx); @@ -302,7 +302,7 @@ pub struct Completion { pub struct Copilot { http: Arc, - node_runtime: Arc, + node_runtime: NodeRuntime, server: CopilotServer, buffers: HashSet>, server_id: LanguageServerId, @@ -334,7 +334,7 @@ impl Copilot { fn start( new_server_id: LanguageServerId, http: Arc, - node_runtime: Arc, + node_runtime: NodeRuntime, cx: &mut ModelContext, ) -> Self { let mut this = Self { @@ -392,7 +392,7 @@ impl Copilot { #[cfg(any(test, feature = "test-support"))] pub fn fake(cx: &mut gpui::TestAppContext) -> (Model, lsp::FakeLanguageServer) { use lsp::FakeLanguageServer; - use node_runtime::FakeNodeRuntime; + use node_runtime::NodeRuntime; let (server, fake_server) = FakeLanguageServer::new( LanguageServerId(0), @@ -406,7 +406,7 @@ impl Copilot { cx.to_async(), ); let http = http_client::FakeHttpClient::create(|_| async { unreachable!() }); - let node_runtime = FakeNodeRuntime::new(); + let node_runtime = NodeRuntime::unavailable(); let this = cx.new_model(|cx| Self { server_id: LanguageServerId(0), http: http.clone(), @@ -425,7 +425,7 @@ impl Copilot { async fn start_language_server( new_server_id: LanguageServerId, http: Arc, - node_runtime: Arc, + node_runtime: NodeRuntime, this: WeakModel, mut cx: AsyncAppContext, ) { diff --git a/crates/evals/src/eval.rs b/crates/evals/src/eval.rs index e2c8b42644..899d821053 100644 --- a/crates/evals/src/eval.rs +++ b/crates/evals/src/eval.rs @@ -9,7 +9,7 @@ use git::GitHostingProviderRegistry; use gpui::{AsyncAppContext, BackgroundExecutor, Context, Model}; use http_client::{HttpClient, Method}; use language::LanguageRegistry; -use node_runtime::FakeNodeRuntime; +use node_runtime::NodeRuntime; use open_ai::OpenAiEmbeddingModel; use project::Project; use semantic_index::{ @@ -292,7 +292,7 @@ async fn run_evaluation( let user_store = cx .new_model(|cx| UserStore::new(client.clone(), cx)) .unwrap(); - let node_runtime = Arc::new(FakeNodeRuntime {}); + let node_runtime = NodeRuntime::unavailable(); let evaluations = fs::read(&evaluations_path).expect("failed to read evaluations.json"); let evaluations: Vec = serde_json::from_slice(&evaluations).unwrap(); diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 8dbd618a25..5f9fbffb11 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -177,7 +177,7 @@ actions!(zed, [ReloadExtensions]); pub fn init( fs: Arc, client: Arc, - node_runtime: Arc, + node_runtime: NodeRuntime, language_registry: Arc, theme_registry: Arc, cx: &mut AppContext, @@ -228,7 +228,7 @@ impl ExtensionStore { http_client: Arc, builder_client: Arc, telemetry: Option>, - node_runtime: Arc, + node_runtime: NodeRuntime, language_registry: Arc, theme_registry: Arc, slash_command_registry: Arc, diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index 4bdafaa32c..126e6b2cfb 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/crates/extension/src/extension_store_test.rs @@ -15,7 +15,7 @@ use http_client::{FakeHttpClient, Response}; use indexed_docs::IndexedDocsRegistry; use isahc_http_client::IsahcHttpClient; use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName}; -use node_runtime::FakeNodeRuntime; +use node_runtime::NodeRuntime; use parking_lot::Mutex; use project::{Project, DEFAULT_COMPLETION_CONTEXT}; use release_channel::AppVersion; @@ -264,7 +264,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { let slash_command_registry = SlashCommandRegistry::new(); let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor())); let snippet_registry = Arc::new(SnippetRegistry::new()); - let node_runtime = FakeNodeRuntime::new(); + let node_runtime = NodeRuntime::unavailable(); let store = cx.new_model(|cx| { ExtensionStore::new( @@ -490,7 +490,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { let slash_command_registry = SlashCommandRegistry::new(); let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor())); let snippet_registry = Arc::new(SnippetRegistry::new()); - let node_runtime = FakeNodeRuntime::new(); + let node_runtime = NodeRuntime::unavailable(); let mut status_updates = language_registry.language_server_binary_statuses(); diff --git a/crates/extension/src/wasm_host.rs b/crates/extension/src/wasm_host.rs index 039f2d923b..b3fd13a5ba 100644 --- a/crates/extension/src/wasm_host.rs +++ b/crates/extension/src/wasm_host.rs @@ -33,7 +33,7 @@ pub(crate) struct WasmHost { engine: Engine, release_channel: ReleaseChannel, http_client: Arc, - node_runtime: Arc, + node_runtime: NodeRuntime, pub(crate) language_registry: Arc, fs: Arc, pub(crate) work_dir: PathBuf, @@ -80,7 +80,7 @@ impl WasmHost { pub fn new( fs: Arc, http_client: Arc, - node_runtime: Arc, + node_runtime: NodeRuntime, language_registry: Arc, work_dir: PathBuf, cx: &mut AppContext, diff --git a/crates/headless/src/headless.rs b/crates/headless/src/headless.rs index a5504500da..1405577643 100644 --- a/crates/headless/src/headless.rs +++ b/crates/headless/src/headless.rs @@ -25,7 +25,7 @@ pub struct DevServer { } pub struct AppState { - pub node_runtime: Arc, + pub node_runtime: NodeRuntime, pub user_store: Model, pub languages: Arc, pub fs: Arc, diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index d78b2dd23c..c063015151 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -264,6 +264,35 @@ pub fn read_proxy_from_env() -> Option { None } +pub struct BlockedHttpClient; + +impl HttpClient for BlockedHttpClient { + fn send( + &self, + _req: Request, + ) -> BoxFuture<'static, Result, anyhow::Error>> { + Box::pin(async { + Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "BlockedHttpClient disallowed request", + ) + .into()) + }) + } + + fn proxy(&self) -> Option<&Uri> { + None + } + + fn send_with_redirect_policy( + &self, + req: Request, + _: bool, + ) -> BoxFuture<'static, Result, anyhow::Error>> { + self.send(req) + } +} + #[cfg(feature = "test-support")] type FakeHttpHandler = Box< dyn Fn(Request) -> BoxFuture<'static, Result, anyhow::Error>> diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 29a7ac1860..166d846f86 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -564,6 +564,7 @@ async fn try_fetch_server_binary let name = adapter.name(); log::info!("fetching latest version of language server {:?}", name.0); delegate.update_status(name.clone(), LanguageServerBinaryStatus::CheckingForUpdate); + let latest_version = adapter .fetch_latest_server_version(delegate.as_ref()) .await?; diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index cf259d69d3..7b7e9ae77f 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -22,11 +22,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct CssLspAdapter { - node: Arc, + node: NodeRuntime, } impl CssLspAdapter { - pub fn new(node: Arc) -> Self { + pub fn new(node: NodeRuntime) -> Self { CssLspAdapter { node } } } @@ -81,14 +81,14 @@ impl LspAdapter for CssLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &*self.node).await + get_cached_server_binary(container_dir, &self.node).await } async fn installation_test_binary( &self, container_dir: PathBuf, ) -> Option { - get_cached_server_binary(container_dir, &*self.node).await + get_cached_server_binary(container_dir, &self.node).await } async fn initialization_options( @@ -103,7 +103,7 @@ impl LspAdapter for CssLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &dyn NodeRuntime, + node: &NodeRuntime, ) -> Option { maybe!(async { let mut last_version_dir = None; diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 6b5f74c263..44cc683876 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -59,13 +59,13 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct JsonLspAdapter { - node: Arc, + node: NodeRuntime, languages: Arc, workspace_config: OnceLock, } impl JsonLspAdapter { - pub fn new(node: Arc, languages: Arc) -> Self { + pub fn new(node: NodeRuntime, languages: Arc) -> Self { Self { node, languages, @@ -183,14 +183,14 @@ impl LspAdapter for JsonLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &*self.node).await + get_cached_server_binary(container_dir, &self.node).await } async fn installation_test_binary( &self, container_dir: PathBuf, ) -> Option { - get_cached_server_binary(container_dir, &*self.node).await + get_cached_server_binary(container_dir, &self.node).await } async fn initialization_options( @@ -226,7 +226,7 @@ impl LspAdapter for JsonLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &dyn NodeRuntime, + node: &NodeRuntime, ) -> Option { maybe!(async { let mut last_version_dir = None; diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 0a3fc71d08..7435ddb131 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -30,11 +30,7 @@ mod yaml; #[exclude = "*.rs"] struct LanguageDir; -pub fn init( - languages: Arc, - node_runtime: Arc, - cx: &mut AppContext, -) { +pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mut AppContext) { languages.register_native_grammars([ ("bash", tree_sitter_bash::LANGUAGE), ("c", tree_sitter_c::LANGUAGE), diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 0dce8fb661..75f124489c 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -26,13 +26,13 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct PythonLspAdapter { - node: Arc, + node: NodeRuntime, } impl PythonLspAdapter { const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pyright"); - pub fn new(node: Arc) -> Self { + pub fn new(node: NodeRuntime) -> Self { PythonLspAdapter { node } } } @@ -94,14 +94,14 @@ impl LspAdapter for PythonLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &*self.node).await + get_cached_server_binary(container_dir, &self.node).await } async fn installation_test_binary( &self, container_dir: PathBuf, ) -> Option { - get_cached_server_binary(container_dir, &*self.node).await + get_cached_server_binary(container_dir, &self.node).await } async fn process_completions(&self, items: &mut [lsp::CompletionItem]) { @@ -198,7 +198,7 @@ impl LspAdapter for PythonLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &dyn NodeRuntime, + node: &NodeRuntime, ) -> Option { let server_path = container_dir.join(SERVER_PATH); if server_path.exists() { diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index e3e17a8fa7..62d967d6a4 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -28,14 +28,14 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct TailwindLspAdapter { - node: Arc, + node: NodeRuntime, } impl TailwindLspAdapter { const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("tailwindcss-language-server"); - pub fn new(node: Arc) -> Self { + pub fn new(node: NodeRuntime) -> Self { TailwindLspAdapter { node } } } @@ -122,14 +122,14 @@ impl LspAdapter for TailwindLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &*self.node).await + get_cached_server_binary(container_dir, &self.node).await } async fn installation_test_binary( &self, container_dir: PathBuf, ) -> Option { - get_cached_server_binary(container_dir, &*self.node).await + get_cached_server_binary(container_dir, &self.node).await } async fn initialization_options( @@ -198,7 +198,7 @@ impl LspAdapter for TailwindLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &dyn NodeRuntime, + node: &NodeRuntime, ) -> Option { maybe!(async { let mut last_version_dir = None; diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index b09216c970..25a97c8014 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -65,7 +65,7 @@ fn eslint_server_binary_arguments(server_path: &Path) -> Vec { } pub struct TypeScriptLspAdapter { - node: Arc, + node: NodeRuntime, } impl TypeScriptLspAdapter { @@ -73,7 +73,7 @@ impl TypeScriptLspAdapter { const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs"; const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("typescript-language-server"); - pub fn new(node: Arc) -> Self { + pub fn new(node: NodeRuntime) -> Self { TypeScriptLspAdapter { node } } async fn tsdk_path(adapter: &Arc) -> &'static str { @@ -161,14 +161,14 @@ impl LspAdapter for TypeScriptLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_ts_server_binary(container_dir, &*self.node).await + get_cached_ts_server_binary(container_dir, &self.node).await } async fn installation_test_binary( &self, container_dir: PathBuf, ) -> Option { - get_cached_ts_server_binary(container_dir, &*self.node).await + get_cached_ts_server_binary(container_dir, &self.node).await } fn code_action_kinds(&self) -> Option> { @@ -264,7 +264,7 @@ impl LspAdapter for TypeScriptLspAdapter { async fn get_cached_ts_server_binary( container_dir: PathBuf, - node: &dyn NodeRuntime, + node: &NodeRuntime, ) -> Option { maybe!(async { let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH); @@ -293,7 +293,7 @@ async fn get_cached_ts_server_binary( } pub struct EsLintLspAdapter { - node: Arc, + node: NodeRuntime, } impl EsLintLspAdapter { @@ -310,7 +310,7 @@ impl EsLintLspAdapter { const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"]; - pub fn new(node: Arc) -> Self { + pub fn new(node: NodeRuntime) -> Self { EsLintLspAdapter { node } } } @@ -476,11 +476,11 @@ impl LspAdapter for EsLintLspAdapter { } self.node - .run_npm_subcommand(Some(&repo_root), "install", &[]) + .run_npm_subcommand(&repo_root, "install", &[]) .await?; self.node - .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"]) + .run_npm_subcommand(&repo_root, "run-script", &["compile"]) .await?; } @@ -496,20 +496,20 @@ impl LspAdapter for EsLintLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_eslint_server_binary(container_dir, &*self.node).await + get_cached_eslint_server_binary(container_dir, &self.node).await } async fn installation_test_binary( &self, container_dir: PathBuf, ) -> Option { - get_cached_eslint_server_binary(container_dir, &*self.node).await + get_cached_eslint_server_binary(container_dir, &self.node).await } } async fn get_cached_eslint_server_binary( container_dir: PathBuf, - node: &dyn NodeRuntime, + node: &NodeRuntime, ) -> Option { maybe!(async { // This is unfortunate but we don't know what the version is to build a path directly diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 5ec3121384..3c1cf0fcbe 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -20,13 +20,13 @@ fn typescript_server_binary_arguments(server_path: &Path) -> Vec { } pub struct VtslsLspAdapter { - node: Arc, + node: NodeRuntime, } impl VtslsLspAdapter { const SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js"; - pub fn new(node: Arc) -> Self { + pub fn new(node: NodeRuntime) -> Self { VtslsLspAdapter { node } } async fn tsdk_path(adapter: &Arc) -> &'static str { @@ -154,14 +154,14 @@ impl LspAdapter for VtslsLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_ts_server_binary(container_dir, &*self.node).await + get_cached_ts_server_binary(container_dir, &self.node).await } async fn installation_test_binary( &self, container_dir: PathBuf, ) -> Option { - get_cached_ts_server_binary(container_dir, &*self.node).await + get_cached_ts_server_binary(container_dir, &self.node).await } fn code_action_kinds(&self) -> Option> { @@ -298,7 +298,7 @@ impl LspAdapter for VtslsLspAdapter { async fn get_cached_ts_server_binary( container_dir: PathBuf, - node: &dyn NodeRuntime, + node: &NodeRuntime, ) -> Option { maybe!(async { let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH); diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 583961f4b1..32ca73168a 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -26,12 +26,12 @@ fn server_binary_arguments(server_path: &Path) -> Vec { } pub struct YamlLspAdapter { - node: Arc, + node: NodeRuntime, } impl YamlLspAdapter { const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("yaml-language-server"); - pub fn new(node: Arc) -> Self { + pub fn new(node: NodeRuntime) -> Self { YamlLspAdapter { node } } } @@ -117,14 +117,14 @@ impl LspAdapter for YamlLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir, &*self.node).await + get_cached_server_binary(container_dir, &self.node).await } async fn installation_test_binary( &self, container_dir: PathBuf, ) -> Option { - get_cached_server_binary(container_dir, &*self.node).await + get_cached_server_binary(container_dir, &self.node).await } async fn workspace_configuration( @@ -157,7 +157,7 @@ impl LspAdapter for YamlLspAdapter { async fn get_cached_server_binary( container_dir: PathBuf, - node: &dyn NodeRuntime, + node: &NodeRuntime, ) -> Option { maybe!(async { let mut last_version_dir = None; diff --git a/crates/markdown/examples/markdown.rs b/crates/markdown/examples/markdown.rs index c2f3ab8158..0514ebcf4e 100644 --- a/crates/markdown/examples/markdown.rs +++ b/crates/markdown/examples/markdown.rs @@ -2,7 +2,7 @@ use assets::Assets; use gpui::{prelude::*, rgb, App, KeyBinding, StyleRefinement, View, WindowOptions}; use language::{language_settings::AllLanguageSettings, LanguageRegistry}; use markdown::{Markdown, MarkdownStyle}; -use node_runtime::FakeNodeRuntime; +use node_runtime::NodeRuntime; use settings::SettingsStore; use std::sync::Arc; use theme::LoadThemes; @@ -102,7 +102,7 @@ pub fn main() { }); cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]); - let node_runtime = FakeNodeRuntime::new(); + let node_runtime = NodeRuntime::unavailable(); theme::init(LoadThemes::JustBase, cx); let language_registry = LanguageRegistry::new(cx.background_executor().clone()); diff --git a/crates/markdown/examples/markdown_as_child.rs b/crates/markdown/examples/markdown_as_child.rs index 829e69436a..3700e64364 100644 --- a/crates/markdown/examples/markdown_as_child.rs +++ b/crates/markdown/examples/markdown_as_child.rs @@ -2,7 +2,7 @@ use assets::Assets; use gpui::*; use language::{language_settings::AllLanguageSettings, LanguageRegistry}; use markdown::{Markdown, MarkdownStyle}; -use node_runtime::FakeNodeRuntime; +use node_runtime::NodeRuntime; use settings::SettingsStore; use std::sync::Arc; use theme::LoadThemes; @@ -28,7 +28,7 @@ pub fn main() { }); cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]); - let node_runtime = FakeNodeRuntime::new(); + let node_runtime = NodeRuntime::unavailable(); let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); languages::init(language_registry.clone(), node_runtime, cx); theme::init(LoadThemes::JustBase, cx); diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml index b7aee58336..d852b7ebdf 100644 --- a/crates/node_runtime/Cargo.toml +++ b/crates/node_runtime/Cargo.toml @@ -18,6 +18,7 @@ test-support = ["tempfile"] [dependencies] anyhow.workspace = true async-compression.workspace = true +async-watch.workspace = true async-tar.workspace = true async-trait.workspace = true async_zip.workspace = true @@ -32,6 +33,7 @@ smol.workspace = true tempfile = { workspace = true, optional = true } util.workspace = true walkdir = "2.5.0" +which.workspace = true [target.'cfg(windows)'.dependencies] async-std = { version = "1.12.0", features = ["unstable"] } diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 4aa65ab6db..72c74ce7cf 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -5,7 +5,7 @@ pub use archive::extract_zip; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use futures::AsyncReadExt; -use http_client::HttpClient; +use http_client::{HttpClient, Uri}; use semver::Version; use serde::Deserialize; use smol::io::BufReader; @@ -23,60 +23,166 @@ use util::ResultExt; #[cfg(windows)] use smol::process::windows::CommandExt; -const VERSION: &str = "v22.5.1"; - -#[cfg(not(windows))] -const NODE_PATH: &str = "bin/node"; -#[cfg(windows)] -const NODE_PATH: &str = "node.exe"; - -#[cfg(not(windows))] -const NPM_PATH: &str = "bin/npm"; -#[cfg(windows)] -const NPM_PATH: &str = "node_modules/npm/bin/npm-cli.js"; - -enum ArchiveType { - TarGz, - Zip, +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct NodeBinaryOptions { + pub allow_path_lookup: bool, + pub allow_binary_download: bool, + pub use_paths: Option<(PathBuf, PathBuf)>, } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct NpmInfo { - #[serde(default)] - dist_tags: NpmInfoDistTags, - versions: Vec, +#[derive(Clone)] +pub struct NodeRuntime(Arc>); + +struct NodeRuntimeState { + http: Arc, + instance: Option>, + last_options: Option, + options: async_watch::Receiver>, } -#[derive(Debug, Deserialize, Default)] -pub struct NpmInfoDistTags { - latest: Option, -} +impl NodeRuntime { + pub fn new( + http: Arc, + options: async_watch::Receiver>, + ) -> Self { + NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState { + http, + instance: None, + last_options: None, + options, + }))) + } -#[async_trait::async_trait] -pub trait NodeRuntime: Send + Sync { - async fn binary_path(&self) -> Result; - async fn node_environment_path(&self) -> Result; + pub fn unavailable() -> Self { + NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState { + http: Arc::new(http_client::BlockedHttpClient), + instance: None, + last_options: None, + options: async_watch::channel(Some(NodeBinaryOptions::default())).1, + }))) + } - async fn run_npm_subcommand( + async fn instance(&self) -> Result> { + let mut state = self.0.lock().await; + + while state.options.borrow().is_none() { + state.options.changed().await?; + } + let options = state.options.borrow().clone().unwrap(); + if state.last_options.as_ref() != Some(&options) { + state.instance.take(); + } + if let Some(instance) = state.instance.as_ref() { + return Ok(instance.boxed_clone()); + } + + if let Some((node, npm)) = options.use_paths.as_ref() { + let instance = SystemNodeRuntime::new(node.clone(), npm.clone()).await?; + state.instance = Some(instance.boxed_clone()); + return Ok(instance); + } + + if options.allow_path_lookup { + if let Some(instance) = SystemNodeRuntime::detect().await { + state.instance = Some(instance.boxed_clone()); + return Ok(instance); + } + } + + let instance = if options.allow_binary_download { + ManagedNodeRuntime::install_if_needed(&state.http).await? + } else { + Box::new(UnavailableNodeRuntime) + }; + + state.instance = Some(instance.boxed_clone()); + return Ok(instance); + } + + pub async fn binary_path(&self) -> Result { + self.instance().await?.binary_path() + } + + pub async fn run_npm_subcommand( &self, - directory: Option<&Path>, + directory: &Path, subcommand: &str, args: &[&str], - ) -> Result; + ) -> Result { + let http = self.0.lock().await.http.clone(); + self.instance() + .await? + .run_npm_subcommand(Some(directory), http.proxy(), subcommand, args) + .await + } - async fn npm_package_latest_version(&self, name: &str) -> Result; - - async fn npm_install_packages(&self, directory: &Path, packages: &[(&str, &str)]) - -> Result<()>; - - async fn npm_package_installed_version( + pub async fn npm_package_installed_version( &self, local_package_directory: &Path, name: &str, - ) -> Result>; + ) -> Result> { + self.instance() + .await? + .npm_package_installed_version(local_package_directory, name) + .await + } - async fn should_install_npm_package( + pub async fn npm_package_latest_version(&self, name: &str) -> Result { + let http = self.0.lock().await.http.clone(); + let output = self + .instance() + .await? + .run_npm_subcommand( + None, + http.proxy(), + "info", + &[ + name, + "--json", + "--fetch-retry-mintimeout", + "2000", + "--fetch-retry-maxtimeout", + "5000", + "--fetch-timeout", + "5000", + ], + ) + .await?; + + let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + info.dist_tags + .latest + .or_else(|| info.versions.pop()) + .ok_or_else(|| anyhow!("no version found for npm package {}", name)) + } + + pub async fn npm_install_packages( + &self, + directory: &Path, + packages: &[(&str, &str)], + ) -> Result<()> { + let packages: Vec<_> = packages + .iter() + .map(|(name, version)| format!("{name}@{version}")) + .collect(); + + let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect(); + arguments.extend_from_slice(&[ + "--save-exact", + "--fetch-retry-mintimeout", + "2000", + "--fetch-retry-maxtimeout", + "5000", + "--fetch-timeout", + "5000", + ]); + + self.run_npm_subcommand(directory, "install", &arguments) + .await?; + Ok(()) + } + + pub async fn should_install_npm_package( &self, package_name: &str, local_executable_path: &Path, @@ -110,21 +216,78 @@ pub trait NodeRuntime: Send + Sync { } } -pub struct RealNodeRuntime { - http: Arc, - installation_lock: Mutex<()>, +enum ArchiveType { + TarGz, + Zip, } -impl RealNodeRuntime { - pub fn new(http: Arc) -> Arc { - Arc::new(RealNodeRuntime { - http, - installation_lock: Mutex::new(()), - }) +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct NpmInfo { + #[serde(default)] + dist_tags: NpmInfoDistTags, + versions: Vec, +} + +#[derive(Debug, Deserialize, Default)] +pub struct NpmInfoDistTags { + latest: Option, +} + +#[async_trait::async_trait] +trait NodeRuntimeTrait: Send + Sync { + fn boxed_clone(&self) -> Box; + fn binary_path(&self) -> Result; + + async fn run_npm_subcommand( + &self, + directory: Option<&Path>, + proxy: Option<&Uri>, + subcommand: &str, + args: &[&str], + ) -> Result; + + async fn npm_package_installed_version( + &self, + local_package_directory: &Path, + name: &str, + ) -> Result>; +} + +#[derive(Clone)] +struct ManagedNodeRuntime { + installation_path: PathBuf, +} + +impl ManagedNodeRuntime { + const VERSION: &str = "v22.5.1"; + + #[cfg(not(windows))] + const NODE_PATH: &str = "bin/node"; + #[cfg(windows)] + const NODE_PATH: &str = "node.exe"; + + #[cfg(not(windows))] + const NPM_PATH: &str = "bin/npm"; + #[cfg(windows)] + const NPM_PATH: &str = "node_modules/npm/bin/npm-cli.js"; + + async fn node_environment_path(&self) -> Result { + let node_binary = self.installation_path.join(Self::NODE_PATH); + let mut env_path = vec![node_binary + .parent() + .expect("invalid node binary path") + .to_path_buf()]; + + if let Some(existing_path) = std::env::var_os("PATH") { + let mut paths = std::env::split_paths(&existing_path).collect::>(); + env_path.append(&mut paths); + } + + std::env::join_paths(env_path).context("failed to create PATH env variable") } - async fn install_if_needed(&self) -> Result { - let _lock = self.installation_lock.lock().await; + async fn install_if_needed(http: &Arc) -> Result> { log::info!("Node runtime install_if_needed"); let os = match consts::OS { @@ -140,11 +303,12 @@ impl RealNodeRuntime { other => bail!("Running on unsupported architecture: {other}"), }; - let folder_name = format!("node-{VERSION}-{os}-{arch}"); + let version = Self::VERSION; + let folder_name = format!("node-{version}-{os}-{arch}"); let node_containing_dir = paths::support_dir().join("node"); let node_dir = node_containing_dir.join(folder_name); - let node_binary = node_dir.join(NODE_PATH); - let npm_file = node_dir.join(NPM_PATH); + let node_binary = node_dir.join(Self::NODE_PATH); + let npm_file = node_dir.join(Self::NPM_PATH); let mut command = Command::new(&node_binary); @@ -177,16 +341,16 @@ impl RealNodeRuntime { other => bail!("Running on unsupported os: {other}"), }; + let version = Self::VERSION; let file_name = format!( - "node-{VERSION}-{os}-{arch}.{extension}", + "node-{version}-{os}-{arch}.{extension}", extension = match archive_type { ArchiveType::TarGz => "tar.gz", ArchiveType::Zip => "zip", } ); - let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}"); - let mut response = self - .http + let url = format!("https://nodejs.org/dist/{version}/{file_name}"); + let mut response = http .get(&url, Default::default(), true) .await .context("error downloading Node binary tarball")?; @@ -207,43 +371,32 @@ impl RealNodeRuntime { _ = fs::write(node_dir.join("blank_user_npmrc"), []).await; _ = fs::write(node_dir.join("blank_global_npmrc"), []).await; - anyhow::Ok(node_dir) + anyhow::Ok(Box::new(ManagedNodeRuntime { + installation_path: node_dir, + })) } } #[async_trait::async_trait] -impl NodeRuntime for RealNodeRuntime { - async fn binary_path(&self) -> Result { - let installation_path = self.install_if_needed().await?; - Ok(installation_path.join(NODE_PATH)) +impl NodeRuntimeTrait for ManagedNodeRuntime { + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) } - async fn node_environment_path(&self) -> Result { - let installation_path = self.install_if_needed().await?; - let node_binary = installation_path.join(NODE_PATH); - let mut env_path = vec![node_binary - .parent() - .expect("invalid node binary path") - .to_path_buf()]; - - if let Some(existing_path) = std::env::var_os("PATH") { - let mut paths = std::env::split_paths(&existing_path).collect::>(); - env_path.append(&mut paths); - } - - Ok(std::env::join_paths(env_path).context("failed to create PATH env variable")?) + fn binary_path(&self) -> Result { + Ok(self.installation_path.join(Self::NODE_PATH)) } async fn run_npm_subcommand( &self, directory: Option<&Path>, + proxy: Option<&Uri>, subcommand: &str, args: &[&str], ) -> Result { let attempt = || async move { - let installation_path = self.install_if_needed().await?; - let node_binary = installation_path.join(NODE_PATH); - let npm_file = installation_path.join(NPM_PATH); + let node_binary = self.installation_path.join(Self::NODE_PATH); + let npm_file = self.installation_path.join(Self::NPM_PATH); let env_path = self.node_environment_path().await?; if smol::fs::metadata(&node_binary).await.is_err() { @@ -258,54 +411,17 @@ impl NodeRuntime for RealNodeRuntime { command.env_clear(); command.env("PATH", env_path); command.arg(npm_file).arg(subcommand); - command.args(["--cache".into(), installation_path.join("cache")]); + command.args(["--cache".into(), self.installation_path.join("cache")]); command.args([ "--userconfig".into(), - installation_path.join("blank_user_npmrc"), + self.installation_path.join("blank_user_npmrc"), ]); command.args([ "--globalconfig".into(), - installation_path.join("blank_global_npmrc"), + self.installation_path.join("blank_global_npmrc"), ]); command.args(args); - - if let Some(directory) = directory { - command.current_dir(directory); - command.args(["--prefix".into(), directory.to_path_buf()]); - } - - if let Some(proxy) = self.http.proxy() { - // Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809` - // NodeRuntime without environment information can not parse `localhost` - // correctly. - // TODO: map to `[::1]` if we are using ipv6 - let proxy = proxy - .to_string() - .to_ascii_lowercase() - .replace("localhost", "127.0.0.1"); - - command.args(["--proxy", &proxy]); - } - - #[cfg(windows)] - { - // SYSTEMROOT is a critical environment variables for Windows. - if let Some(val) = std::env::var("SYSTEMROOT") - .context("Missing environment variable: SYSTEMROOT!") - .log_err() - { - command.env("SYSTEMROOT", val); - } - // Without ComSpec, the post-install will always fail. - if let Some(val) = std::env::var("ComSpec") - .context("Missing environment variable: ComSpec!") - .log_err() - { - command.env("ComSpec", val); - } - command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } - + configure_npm_command(&mut command, directory, proxy); command.output().await.map_err(|e| anyhow!("{e}")) }; @@ -332,30 +448,123 @@ impl NodeRuntime for RealNodeRuntime { output.map_err(|e| anyhow!("{e}")) } + async fn npm_package_installed_version( + &self, + local_package_directory: &Path, + name: &str, + ) -> Result> { + read_package_installed_version(local_package_directory.join("node_modules"), name).await + } +} - async fn npm_package_latest_version(&self, name: &str) -> Result { - let output = self - .run_npm_subcommand( - None, - "info", - &[ - name, - "--json", - "--fetch-retry-mintimeout", - "2000", - "--fetch-retry-maxtimeout", - "5000", - "--fetch-timeout", - "5000", - ], +#[derive(Clone)] +pub struct SystemNodeRuntime { + node: PathBuf, + npm: PathBuf, + global_node_modules: PathBuf, + scratch_dir: PathBuf, +} + +impl SystemNodeRuntime { + const MIN_VERSION: semver::Version = Version::new(18, 0, 0); + async fn new(node: PathBuf, npm: PathBuf) -> Result> { + let output = Command::new(&node) + .arg("--version") + .output() + .await + .with_context(|| format!("running node from {:?}", node))?; + if !output.status.success() { + anyhow::bail!( + "failed to run node --version. stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + let version_str = String::from_utf8_lossy(&output.stdout); + let version = semver::Version::parse(version_str.trim().trim_start_matches('v'))?; + if version < Self::MIN_VERSION { + anyhow::bail!( + "node at {} is too old. want: {}, got: {}", + node.to_string_lossy(), + Self::MIN_VERSION, + version ) - .await?; + } - let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; - info.dist_tags - .latest - .or_else(|| info.versions.pop()) - .ok_or_else(|| anyhow!("no version found for npm package {}", name)) + let scratch_dir = paths::support_dir().join("node"); + fs::create_dir(&scratch_dir).await.ok(); + fs::create_dir(scratch_dir.join("cache")).await.ok(); + fs::write(scratch_dir.join("blank_user_npmrc"), []) + .await + .ok(); + fs::write(scratch_dir.join("blank_global_npmrc"), []) + .await + .ok(); + + let mut this = Self { + node, + npm, + global_node_modules: PathBuf::default(), + scratch_dir, + }; + let output = this.run_npm_subcommand(None, None, "root", &["-g"]).await?; + this.global_node_modules = + PathBuf::from(String::from_utf8_lossy(&output.stdout).to_string()); + + Ok(Box::new(this)) + } + + async fn detect() -> Option> { + let node = which::which("node").ok()?; + let npm = which::which("npm").ok()?; + Self::new(node, npm).await.log_err() + } +} + +#[async_trait::async_trait] +impl NodeRuntimeTrait for SystemNodeRuntime { + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn binary_path(&self) -> Result { + Ok(self.node.clone()) + } + + async fn run_npm_subcommand( + &self, + directory: Option<&Path>, + proxy: Option<&Uri>, + subcommand: &str, + args: &[&str], + ) -> anyhow::Result { + let mut command = Command::new(self.node.clone()); + command + .env_clear() + .env("PATH", std::env::var_os("PATH").unwrap_or_default()) + .arg(self.npm.clone()) + .arg(subcommand) + .args(["--cache".into(), self.scratch_dir.join("cache")]) + .args([ + "--userconfig".into(), + self.scratch_dir.join("blank_user_npmrc"), + ]) + .args([ + "--globalconfig".into(), + self.scratch_dir.join("blank_global_npmrc"), + ]) + .args(args); + configure_npm_command(&mut command, directory, proxy); + let output = command.output().await?; + if !output.status.success() { + return Err(anyhow!( + "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + + Ok(output) } async fn npm_package_installed_version( @@ -363,151 +572,104 @@ impl NodeRuntime for RealNodeRuntime { local_package_directory: &Path, name: &str, ) -> Result> { - let mut package_json_path = local_package_directory.to_owned(); - package_json_path.extend(["node_modules", name, "package.json"]); + read_package_installed_version(local_package_directory.join("node_modules"), name).await + // todo: allow returning a globally installed version (requires callers not to hard-code the path) + } +} - let mut file = match fs::File::open(package_json_path).await { - Ok(file) => file, - Err(err) => { - if err.kind() == io::ErrorKind::NotFound { - return Ok(None); - } +async fn read_package_installed_version( + node_module_directory: PathBuf, + name: &str, +) -> Result> { + let package_json_path = node_module_directory.join(name).join("package.json"); - Err(err)? + let mut file = match fs::File::open(package_json_path).await { + Ok(file) => file, + Err(err) => { + if err.kind() == io::ErrorKind::NotFound { + return Ok(None); } - }; - #[derive(Deserialize)] - struct PackageJson { - version: String, + Err(err)? } + }; - let mut contents = String::new(); - file.read_to_string(&mut contents).await?; - let package_json: PackageJson = serde_json::from_str(&contents)?; - Ok(Some(package_json.version)) + #[derive(Deserialize)] + struct PackageJson { + version: String, } - async fn npm_install_packages( - &self, - directory: &Path, - packages: &[(&str, &str)], - ) -> Result<()> { - let packages: Vec<_> = packages - .iter() - .map(|(name, version)| format!("{name}@{version}")) - .collect(); - - let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect(); - arguments.extend_from_slice(&[ - "--save-exact", - "--fetch-retry-mintimeout", - "2000", - "--fetch-retry-maxtimeout", - "5000", - "--fetch-timeout", - "5000", - ]); - - self.run_npm_subcommand(Some(directory), "install", &arguments) - .await?; - Ok(()) - } + let mut contents = String::new(); + file.read_to_string(&mut contents).await?; + let package_json: PackageJson = serde_json::from_str(&contents)?; + Ok(Some(package_json.version)) } -pub struct FakeNodeRuntime; - -impl FakeNodeRuntime { - pub fn new() -> Arc { - Arc::new(Self) - } -} +pub struct UnavailableNodeRuntime; #[async_trait::async_trait] -impl NodeRuntime for FakeNodeRuntime { - async fn binary_path(&self) -> anyhow::Result { - unreachable!() +impl NodeRuntimeTrait for UnavailableNodeRuntime { + fn boxed_clone(&self) -> Box { + Box::new(UnavailableNodeRuntime) } - - async fn node_environment_path(&self) -> anyhow::Result { - unreachable!() + fn binary_path(&self) -> Result { + bail!("binary_path: no node runtime available") } async fn run_npm_subcommand( &self, _: Option<&Path>, - subcommand: &str, - args: &[&str], + _: Option<&Uri>, + _: &str, + _: &[&str], ) -> anyhow::Result { - unreachable!("Should not run npm subcommand '{subcommand}' with args {args:?}") - } - - async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result { - unreachable!("Should not query npm package '{name}' for latest version") + bail!("run_npm_subcommand: no node runtime available") } async fn npm_package_installed_version( &self, _local_package_directory: &Path, - name: &str, + _: &str, ) -> Result> { - unreachable!("Should not query npm package '{name}' for installed version") - } - - async fn npm_install_packages( - &self, - _: &Path, - packages: &[(&str, &str)], - ) -> anyhow::Result<()> { - unreachable!("Should not install packages {packages:?}") + bail!("npm_package_installed_version: no node runtime available") } } -// TODO: Remove this when headless binary can run node -pub struct DummyNodeRuntime; +fn configure_npm_command(command: &mut Command, directory: Option<&Path>, proxy: Option<&Uri>) { + if let Some(directory) = directory { + command.current_dir(directory); + command.args(["--prefix".into(), directory.to_path_buf()]); + } -impl DummyNodeRuntime { - pub fn new() -> Arc { - Arc::new(Self) - } -} - -#[async_trait::async_trait] -impl NodeRuntime for DummyNodeRuntime { - async fn binary_path(&self) -> anyhow::Result { - anyhow::bail!("Dummy Node Runtime") - } - - async fn node_environment_path(&self) -> anyhow::Result { - anyhow::bail!("Dummy node runtime") - } - - async fn run_npm_subcommand( - &self, - _: Option<&Path>, - _subcommand: &str, - _args: &[&str], - ) -> anyhow::Result { - anyhow::bail!("Dummy node runtime") - } - - async fn npm_package_latest_version(&self, _name: &str) -> anyhow::Result { - anyhow::bail!("Dummy node runtime") - } - - async fn npm_package_installed_version( - &self, - _local_package_directory: &Path, - _name: &str, - ) -> Result> { - anyhow::bail!("Dummy node runtime") - } - - async fn npm_install_packages( - &self, - _: &Path, - _packages: &[(&str, &str)], - ) -> anyhow::Result<()> { - anyhow::bail!("Dummy node runtime") + if let Some(proxy) = proxy { + // Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809` + // NodeRuntime without environment information can not parse `localhost` + // correctly. + // TODO: map to `[::1]` if we are using ipv6 + let proxy = proxy + .to_string() + .to_ascii_lowercase() + .replace("localhost", "127.0.0.1"); + + command.args(["--proxy", &proxy]); + } + + #[cfg(windows)] + { + // SYSTEMROOT is a critical environment variables for Windows. + if let Some(val) = std::env::var("SYSTEMROOT") + .context("Missing environment variable: SYSTEMROOT!") + .log_err() + { + command.env("SYSTEMROOT", val); + } + // Without ComSpec, the post-install will always fail. + if let Some(val) = std::env::var("ComSpec") + .context("Missing environment variable: ComSpec!") + .log_err() + { + command.env("ComSpec", val); + } + command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); } } diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 59ed915453..012beb3fd7 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -138,7 +138,7 @@ impl Prettier { pub async fn start( _: LanguageServerId, prettier_dir: PathBuf, - _: Arc, + _: NodeRuntime, _: AsyncAppContext, ) -> anyhow::Result { Ok(Self::Test(TestPrettier { @@ -151,7 +151,7 @@ impl Prettier { pub async fn start( server_id: LanguageServerId, prettier_dir: PathBuf, - node: Arc, + node: NodeRuntime, cx: AsyncAppContext, ) -> anyhow::Result { use lsp::LanguageServerBinary; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 6673f9da1d..6c71d4baeb 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -17,7 +17,7 @@ use async_trait::async_trait; use client::{proto, TypedEnvelope}; use collections::{btree_map, BTreeMap, HashMap, HashSet}; use futures::{ - future::{join_all, BoxFuture, Shared}, + future::{join_all, Shared}, select, stream::FuturesUnordered, AsyncWriteExt, Future, FutureExt, StreamExt, @@ -27,7 +27,7 @@ use gpui::{ AppContext, AsyncAppContext, Context, Entity, EventEmitter, Model, ModelContext, PromptLevel, Task, WeakModel, }; -use http_client::{AsyncBody, HttpClient, Request, Response, Uri}; +use http_client::{BlockedHttpClient, HttpClient}; use language::{ language_settings::{ all_language_settings, language_settings, AllLanguageSettings, FormatOnSave, Formatter, @@ -7979,35 +7979,6 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { } } -struct BlockedHttpClient; - -impl HttpClient for BlockedHttpClient { - fn send( - &self, - _req: Request, - ) -> BoxFuture<'static, Result, anyhow::Error>> { - Box::pin(async { - Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - "ssh host blocked http connection", - ) - .into()) - }) - } - - fn proxy(&self) -> Option<&Uri> { - None - } - - fn send_with_redirect_policy( - &self, - req: Request, - _: bool, - ) -> BoxFuture<'static, Result, anyhow::Error>> { - self.send(req) - } -} - struct SshLspAdapterDelegate { lsp_store: WeakModel, worktree: worktree::Snapshot, diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index 82bd8464b2..65e2aa2e76 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -30,7 +30,7 @@ use crate::{ }; pub struct PrettierStore { - node: Arc, + node: NodeRuntime, fs: Arc, languages: Arc, worktree_store: Model, @@ -52,7 +52,7 @@ impl EventEmitter for PrettierStore {} impl PrettierStore { pub fn new( - node: Arc, + node: NodeRuntime, fs: Arc, languages: Arc, worktree_store: Model, @@ -212,7 +212,7 @@ impl PrettierStore { } fn start_prettier( - node: Arc, + node: NodeRuntime, prettier_dir: PathBuf, worktree_id: Option, cx: &mut ModelContext, @@ -241,7 +241,7 @@ impl PrettierStore { } fn start_default_prettier( - node: Arc, + node: NodeRuntime, worktree_id: Option, cx: &mut ModelContext, ) -> Task> { @@ -749,7 +749,7 @@ impl DefaultPrettier { pub fn prettier_task( &mut self, - node: &Arc, + node: &NodeRuntime, worktree_id: Option, cx: &mut ModelContext, ) -> Option>> { @@ -767,7 +767,7 @@ impl DefaultPrettier { impl PrettierInstance { pub fn prettier_task( &mut self, - node: &Arc, + node: &NodeRuntime, prettier_dir: Option<&Path>, worktree_id: Option, cx: &mut ModelContext, @@ -786,7 +786,7 @@ impl PrettierInstance { None => match prettier_dir { Some(prettier_dir) => { let new_task = PrettierStore::start_prettier( - Arc::clone(node), + node.clone(), prettier_dir.to_path_buf(), worktree_id, cx, @@ -797,7 +797,7 @@ impl PrettierInstance { } None => { self.attempt += 1; - let node = Arc::clone(node); + let node = node.clone(); cx.spawn(|prettier_store, mut cx| async move { prettier_store .update(&mut cx, |_, cx| { @@ -818,7 +818,7 @@ impl PrettierInstance { async fn install_prettier_packages( fs: &dyn Fs, plugins_to_install: HashSet>, - node: Arc, + node: NodeRuntime, ) -> anyhow::Result<()> { let packages_to_versions = future::try_join_all( plugins_to_install diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index dc9337674b..0015af3802 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -153,7 +153,7 @@ pub struct Project { git_diff_debouncer: DebouncedDelay, remotely_created_models: Arc>, terminals: Terminals, - node: Option>, + node: Option, tasks: Model, hosted_project_id: Option, dev_server_project_id: Option, @@ -579,7 +579,7 @@ impl Project { pub fn local( client: Arc, - node: Arc, + node: NodeRuntime, user_store: Model, languages: Arc, fs: Arc, @@ -675,7 +675,7 @@ impl Project { pub fn ssh( ssh: Arc, client: Arc, - node: Arc, + node: NodeRuntime, user_store: Model, languages: Arc, fs: Arc, @@ -1064,7 +1064,7 @@ impl Project { .update(|cx| { Project::local( client, - node_runtime::FakeNodeRuntime::new(), + node_runtime::NodeRuntime::unavailable(), user_store, Arc::new(languages), fs, @@ -1104,7 +1104,7 @@ impl Project { let project = cx.update(|cx| { Project::local( client, - node_runtime::FakeNodeRuntime::new(), + node_runtime::NodeRuntime::unavailable(), user_store, Arc::new(languages), fs, @@ -1157,7 +1157,7 @@ impl Project { self.user_store.clone() } - pub fn node_runtime(&self) -> Option<&Arc> { + pub fn node_runtime(&self) -> Option<&NodeRuntime> { self.node.as_ref() } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 904efe0a6b..d6f5600a55 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -34,6 +34,10 @@ pub struct ProjectSettings { #[serde(default)] pub git: GitSettings, + /// Configuration for Node-related features + #[serde(default)] + pub node: NodeBinarySettings, + /// Configuration for how direnv configuration should be loaded #[serde(default)] pub load_direnv: DirenvSettings, @@ -43,6 +47,17 @@ pub struct ProjectSettings { pub session: SessionSettings, } +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct NodeBinarySettings { + /// The path to the node binary + pub path: Option, + /// The path to the npm binary Zed should use (defaults to .path/../npm) + pub npm_path: Option, + /// If disabled, zed will download its own copy of node. + #[serde(default)] + pub disable_path_lookup: Option, +} + #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum DirenvSettings { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 0d644a64a6..87c9583077 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use fs::Fs; use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext}; use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry}; -use node_runtime::DummyNodeRuntime; +use node_runtime::NodeRuntime; use project::{ buffer_store::{BufferStore, BufferStoreEvent}, project_settings::SettingsObserver, @@ -57,7 +57,7 @@ impl HeadlessProject { }); let prettier_store = cx.new_model(|cx| { PrettierStore::new( - DummyNodeRuntime::new(), + NodeRuntime::unavailable(), fs.clone(), languages.clone(), worktree_store.clone(), diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index b5ab1c4007..ba59d310c8 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -9,7 +9,7 @@ use language::{ Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName, }; use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind}; -use node_runtime::FakeNodeRuntime; +use node_runtime::NodeRuntime; use project::{ search::{SearchQuery, SearchResult}, Project, @@ -502,7 +502,7 @@ fn build_project(ssh: Arc, cx: &mut TestAppContext) -> Model, pub fs: Arc, pub build_window_options: fn(Option, &mut AppContext) -> WindowOptions, - pub node_runtime: Arc, + pub node_runtime: NodeRuntime, pub session: Model, } @@ -590,7 +590,7 @@ impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { - use node_runtime::FakeNodeRuntime; + use node_runtime::NodeRuntime; use session::Session; use settings::SettingsStore; use ui::Context as _; @@ -619,7 +619,7 @@ impl AppState { languages, user_store, workspace_store, - node_runtime: FakeNodeRuntime::new(), + node_runtime: NodeRuntime::unavailable(), build_window_options: |_, _| Default::default(), session, }) @@ -4418,7 +4418,7 @@ impl Workspace { #[cfg(any(test, feature = "test-support"))] pub fn test_new(project: Model, cx: &mut ViewContext) -> Self { - use node_runtime::FakeNodeRuntime; + use node_runtime::NodeRuntime; use session::Session; let client = project.read(cx).client(); @@ -4434,7 +4434,7 @@ impl Workspace { user_store, fs: project.read(cx).fs().clone(), build_window_options: |_, _| Default::default(), - node_runtime: FakeNodeRuntime::new(), + node_runtime: NodeRuntime::unavailable(), session, }); let workspace = Self::new(Default::default(), project, app_state, cx); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ad2e7cd48c..65724480f6 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -19,6 +19,7 @@ activity_indicator.workspace = true anyhow.workspace = true assets.workspace = true assistant.workspace = true +async-watch.workspace = true audio.workspace = true auto_update.workspace = true backtrace = "0.3" @@ -92,6 +93,7 @@ serde_json.workspace = true session.workspace = true settings.workspace = true settings_ui.workspace = true +shellexpand.workspace = true simplelog.workspace = true smol.workspace = true snippet_provider.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d3eb97c9aa..309931f616 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -29,8 +29,9 @@ use language::LanguageRegistry; use log::LevelFilter; use assets::Assets; -use node_runtime::RealNodeRuntime; +use node_runtime::{NodeBinaryOptions, NodeRuntime}; use parking_lot::Mutex; +use project::project_settings::ProjectSettings; use recent_projects::open_ssh_project; use release_channel::{AppCommitSha, AppVersion}; use session::{AppSession, Session}; @@ -43,7 +44,7 @@ use std::{ env, fs::OpenOptions, io::{IsTerminal, Write}, - path::Path, + path::{Path, PathBuf}, process, sync::Arc, }; @@ -477,7 +478,32 @@ fn main() { let mut languages = LanguageRegistry::new(cx.background_executor().clone()); languages.set_language_server_download_dir(paths::languages_dir().clone()); let languages = Arc::new(languages); - let node_runtime = RealNodeRuntime::new(client.http_client()); + let (tx, rx) = async_watch::channel(None); + cx.observe_global::(move |cx| { + let settings = &ProjectSettings::get_global(cx).node; + let options = NodeBinaryOptions { + allow_path_lookup: !settings.disable_path_lookup.unwrap_or_default(), + // TODO: Expose this setting + allow_binary_download: true, + use_paths: settings.path.as_ref().map(|node_path| { + let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref()); + let npm_path = settings + .npm_path + .as_ref() + .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref())); + ( + node_path.clone(), + npm_path.unwrap_or_else(|| { + let base_path = PathBuf::new(); + node_path.parent().unwrap_or(&base_path).join("npm") + }), + ) + }), + }; + tx.send(Some(options)).log_err(); + }) + .detach(); + let node_runtime = NodeRuntime::new(client.http_client(), rx); language::init(cx); languages::init(languages.clone(), node_runtime.clone(), cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index b0e023f42e..8f4f1af243 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -3365,7 +3365,7 @@ mod tests { cx.set_global(settings); let languages = LanguageRegistry::test(cx.executor()); let languages = Arc::new(languages); - let node_runtime = node_runtime::FakeNodeRuntime::new(); + let node_runtime = node_runtime::NodeRuntime::unavailable(); cx.update(|cx| { languages::init(languages.clone(), node_runtime, cx); });