Compare commits

...

2 Commits

Author SHA1 Message Date
Mikayla
cdd9e72557 Check PATH for Node 2024-08-27 10:29:39 -07:00
Mikayla
b7dbf68c18 Add a setting to control binary downloads
implement the setting for node and LSP extensions

co-authored-by: conrad <conrad@zed.dev>
2024-08-26 10:58:48 -07:00
27 changed files with 564 additions and 218 deletions

3
Cargo.lock generated
View File

@@ -5298,6 +5298,7 @@ dependencies = [
"serde",
"serde_json",
"url",
"util",
]
[[package]]
@@ -6846,6 +6847,7 @@ dependencies = [
"async-trait",
"async_zip",
"futures 0.3.30",
"gpui",
"http_client",
"log",
"paths",
@@ -6856,6 +6858,7 @@ dependencies = [
"tempfile",
"util",
"walkdir",
"which 6.0.2",
"windows 0.58.0",
]

View File

@@ -570,6 +570,12 @@
// A list of globs representing files that inline completions should be disabled for.
"disabled_globs": [".env"]
},
// Configuration for how Zed should handle missing binary dependencies (like language servers)
// required by zed or the extensions you install.
// By default "automatic" will use, install, and update dependencies on your behalf (if they are not already available)
// "never" will silently ignore requests
// If you choose never, you must maintain the required binaries yourself.
"dependency_management": "automatic",
// Settings specific to journaling
"journal": {
// The path of the directory where journal entries are stored

View File

@@ -15,12 +15,11 @@ use serde::Serialize;
use serde_json::Value;
use std::ops::Range;
use std::{
any::Any,
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
};
use util::{maybe, ResultExt};
use util::{maybe, AssetVersion, ResultExt};
use wasmtime_wasi::WasiView as _;
pub struct ExtensionLspAdapter {
@@ -38,7 +37,6 @@ impl LspAdapter for ExtensionLspAdapter {
fn get_language_server_command<'a>(
self: Arc<Self>,
_: Arc<Language>,
_: Arc<Path>,
delegate: Arc<dyn LspAdapterDelegate>,
_: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
@@ -104,13 +102,13 @@ impl LspAdapter for ExtensionLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<Box<dyn AssetVersion>> {
unreachable!("get_language_server_command is overridden")
}
async fn fetch_server_binary(
&self,
_: Box<dyn 'static + Send + Any>,
_: Box<dyn AssetVersion>,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {

View File

@@ -12,6 +12,7 @@ use isahc::config::{Configurable, RedirectPolicy};
use language::{
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
};
use node_runtime::NodeAssetVersion;
use project::project_settings::ProjectSettings;
use semantic_version::SemanticVersion;
use std::{
@@ -269,6 +270,7 @@ impl nodejs::Host for WasmState {
.node_runtime
.npm_package_latest_version(&package_name)
.await
.map(|version| version.version)
.to_wasmtime_result()
}
@@ -290,7 +292,13 @@ impl nodejs::Host for WasmState {
) -> wasmtime::Result<Result<(), String>> {
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
.npm_install_packages(
&self.work_dir(),
&[NodeAssetVersion {
name: package_name,
version: version,
}],
)
.await
.to_wasmtime_result()
}

View File

@@ -10,3 +10,8 @@ This list should be updated as we notice things that should be changed so that w
- Rename `SlashCommand.tooltip_text` to `SlashCommand.menu_text`
- We may even want to remove it entirely, as right now this is only used for featured slash commands, and slash commands defined by extensions aren't currently able to be featured.
### Expose extension binaries
- At a basic level, we want to be able to know what language servers an extension may install so that the user can approve them.
- This may tie in to a more general extension capability system.

View File

@@ -26,3 +26,4 @@ serde.workspace = true
serde_json.workspace = true
futures-lite.workspace = true
url.workspace = true
util.workspace = true

View File

@@ -10,6 +10,16 @@ pub struct GitHubLspBinaryVersion {
pub url: String,
}
impl util::AssetVersion for GitHubLspBinaryVersion {
fn description(&self) -> String {
format!("{} from {}", self.name, self.url)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Deserialize, Debug)]
pub struct GithubRelease {
pub tag_name: String,

View File

@@ -42,7 +42,6 @@ use serde_json::Value;
use smol::future::FutureExt as _;
use std::num::NonZeroU32;
use std::{
any::Any,
ffi::OsStr,
fmt::Debug,
hash::Hash,
@@ -61,7 +60,7 @@ use task::RunnableTag;
pub use task_context::{ContextProvider, RunnableRange};
use theme::SyntaxTheme;
use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore};
use util::serde::default_true;
use util::{serde::default_true, AssetVersion};
pub use buffer::Operation;
pub use buffer::*;
@@ -177,9 +176,16 @@ impl CachedLspAdapter {
})
}
pub async fn check_if_user_installed(
&self,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Option<LanguageServerBinary> {
self.adapter.check_if_user_installed(&*delegate, cx).await
}
pub async fn get_language_server_command(
self: Arc<Self>,
language: Arc<Language>,
container_dir: Arc<Path>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
@@ -187,7 +193,7 @@ impl CachedLspAdapter {
let cached_binary = self.cached_binary.lock().await;
self.adapter
.clone()
.get_language_server_command(language, container_dir, delegate, cached_binary, cx)
.get_language_server_command(container_dir, delegate, cached_binary, cx)
.await
}
@@ -278,34 +284,12 @@ pub trait LspAdapter: 'static + Send + Sync {
fn get_language_server_command<'a>(
self: Arc<Self>,
language: Arc<Language>,
container_dir: Arc<Path>,
delegate: Arc<dyn LspAdapterDelegate>,
mut cached_binary: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
cx: &'a mut AsyncAppContext,
) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
async move {
// First we check whether the adapter can give us a user-installed binary.
// If so, we do *not* want to cache that, because each worktree might give us a different
// binary:
//
// worktree 1: user-installed at `.bin/gopls`
// worktree 2: user-installed at `~/bin/gopls`
// worktree 3: no gopls found in PATH -> fallback to Zed installation
//
// We only want to cache when we fall back to the global one,
// because we don't want to download and overwrite our global one
// for each worktree we might have open.
if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), cx).await {
log::info!(
"found user-installed language server for {}. path: {:?}, arguments: {:?}",
language.name(),
binary.path,
binary.arguments
);
return Ok(binary);
}
if let Some(cached_binary) = cached_binary.as_ref() {
return Ok(cached_binary.clone());
}
@@ -359,7 +343,7 @@ pub trait LspAdapter: 'static + Send + Sync {
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>>;
) -> Result<Box<dyn AssetVersion>>;
fn will_fetch_server(
&self,
@@ -379,7 +363,7 @@ pub trait LspAdapter: 'static + Send + Sync {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn AssetVersion>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary>;
@@ -1634,7 +1618,6 @@ impl LspAdapter for FakeLspAdapter {
fn get_language_server_command<'a>(
self: Arc<Self>,
_: Arc<Language>,
_: Arc<Path>,
_: Arc<dyn LspAdapterDelegate>,
_: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
@@ -1646,13 +1629,13 @@ impl LspAdapter for FakeLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<Box<dyn AssetVersion>> {
unreachable!();
}
async fn fetch_server_binary(
&self,
_: Box<dyn 'static + Send + Any>,
_: Box<dyn AssetVersion>,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {

View File

@@ -1,6 +1,7 @@
use crate::{
language_settings::{
all_language_settings, AllLanguageSettingsContent, LanguageSettingsContent,
all_language_settings, AllLanguageSettings, AllLanguageSettingsContent,
DependencyManagement, LanguageSettingsContent,
},
task_context::ContextProvider,
with_parser, CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher,
@@ -19,6 +20,7 @@ use gpui::{AppContext, BackgroundExecutor, Task};
use lsp::LanguageServerId;
use parking_lot::{Mutex, RwLock};
use postage::watch;
use settings::Settings;
use std::{
borrow::Cow,
ffi::OsStr,
@@ -745,6 +747,8 @@ impl LanguageRegistry {
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
let this = Arc::downgrade(self);
let dependency_management = AllLanguageSettings::get_global(cx).dependency_management;
let task = cx.spawn({
let container_dir = container_dir.clone();
move |mut cx| async move {
@@ -752,15 +756,36 @@ impl LanguageRegistry {
// the login shell to be set on our process.
login_shell_env_loaded.await;
let binary_result = adapter
let binary_result = if let Some(binary) = adapter
.clone()
.get_language_server_command(
language.clone(),
container_dir,
delegate.clone(),
&mut cx,
)
.await;
.check_if_user_installed(delegate.clone(), &mut cx)
.await
{
log::info!(
"found user-installed language server for {}. path: {:?}, arguments: {:?}",
language.name(),
binary.path,
binary.arguments
);
Ok(binary)
} else {
match dependency_management {
DependencyManagement::Automatic => {
adapter
.clone()
.get_language_server_command(
container_dir,
delegate.clone(),
&mut cx,
)
.await
}
DependencyManagement::None => Err(anyhow!(
"No language server found for {}. Not installing because \"dependency_management\" is set to \"never\".",
language.name()
)),
}
};
delegate.update_status(adapter.name.clone(), LanguageServerBinaryStatus::None);

View File

@@ -53,6 +53,14 @@ pub fn all_language_settings<'a>(
AllLanguageSettings::get(location, cx)
}
#[derive(Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DependencyManagement {
#[default]
Automatic, // Zed manages everything (how it works today)
None, // We don't do anything except error
}
/// The settings for all languages.
#[derive(Debug, Clone)]
pub struct AllLanguageSettings {
@@ -60,6 +68,7 @@ pub struct AllLanguageSettings {
pub inline_completions: InlineCompletionSettings,
defaults: LanguageSettings,
languages: HashMap<Arc<str>, LanguageSettings>,
pub dependency_management: DependencyManagement,
pub(crate) file_types: HashMap<Arc<str>, GlobSet>,
}
@@ -205,6 +214,9 @@ pub struct AllLanguageSettingsContent {
/// The inline completion settings.
#[serde(default)]
pub inline_completions: Option<InlineCompletionSettingsContent>,
/// How zed should manage external dependencies
#[serde(default)]
pub dependency_management: Option<DependencyManagement>,
/// The default language settings.
#[serde(flatten)]
pub defaults: LanguageSettingsContent,
@@ -876,6 +888,12 @@ impl settings::Settings for AllLanguageSettings {
}
let mut copilot_enabled = default_value.features.as_ref().and_then(|f| f.copilot);
let dependency_management = sources
.user
.and_then(|user| user.dependency_management)
.or(default_value.dependency_management)
.unwrap();
let mut inline_completion_provider = default_value
.features
.as_ref()
@@ -968,6 +986,7 @@ impl settings::Settings for AllLanguageSettings {
.filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
.collect(),
},
dependency_management,
defaults,
languages,
file_types,

View File

@@ -8,8 +8,8 @@ use lsp::LanguageServerBinary;
use project::project_settings::{BinarySettings, ProjectSettings};
use settings::Settings;
use smol::fs::{self, File};
use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
use util::{fs::remove_matching, maybe, ResultExt};
use std::{env::consts, path::PathBuf, sync::Arc};
use util::{fs::remove_matching, maybe, AssetVersion, ResultExt};
pub struct CLspAdapter;
@@ -68,7 +68,7 @@ impl super::LspAdapter for CLspAdapter {
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<Box<dyn AssetVersion>> {
let release =
latest_github_release("clangd/clangd", true, false, delegate.http_client()).await?;
let os_suffix = match consts::OS {
@@ -92,11 +92,14 @@ impl super::LspAdapter for CLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
version: Box<dyn AssetVersion>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let version = version
.as_any()
.downcast_ref::<GitHubLspBinaryVersion>()
.unwrap();
let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
let version_dir = container_dir.join(format!("clangd_{}", version.name));
let binary_path = version_dir.join("bin/clangd");

View File

@@ -3,16 +3,15 @@ use async_trait::async_trait;
use futures::StreamExt;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use node_runtime::{NodeAssetVersion, NodeRuntime};
use serde_json::json;
use smol::fs;
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::{maybe, ResultExt};
use util::{maybe, AssetVersion, ResultExt};
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-css-language-server";
@@ -40,7 +39,7 @@ impl LspAdapter for CssLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
) -> Result<Box<dyn AssetVersion>> {
Ok(Box::new(
self.node
.npm_package_latest_version("vscode-langservers-extracted")
@@ -50,22 +49,25 @@ impl LspAdapter for CssLspAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn AssetVersion>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let latest_version = latest_version
.as_any()
.downcast_ref::<NodeAssetVersion>()
.unwrap()
.clone();
let server_path = container_dir.join(SERVER_PATH);
let package_name = "vscode-langservers-extracted";
let should_install_language_server = self
.node
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
.should_install_npm_package(&latest_version, &server_path, &container_dir)
.await;
if should_install_language_server {
self.node
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
.npm_install_packages(&container_dir, &[latest_version.clone()])
.await?;
}

View File

@@ -24,7 +24,7 @@ use std::{
},
};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::{fs::remove_matching, maybe, ResultExt};
use util::{fs::remove_matching, maybe, AssetVersion, ResultExt};
fn server_binary_arguments() -> Vec<OsString> {
vec!["-mode=stdio".into()]
@@ -44,6 +44,20 @@ lazy_static! {
static ref GO_ESCAPE_SUBTEST_NAME_REGEX: Regex = Regex::new(r#"[.*+?^${}()|\[\]\\]"#).unwrap();
}
struct GoLspAssetVersion {
version: Option<String>,
}
impl util::AssetVersion for GoLspAssetVersion {
fn description(&self) -> String {
format!("gopls version {:?}", self.version)
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[async_trait(?Send)]
impl super::LspAdapter for GoLspAdapter {
fn name(&self) -> LanguageServerName {
@@ -53,7 +67,7 @@ impl super::LspAdapter for GoLspAdapter {
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<Box<dyn AssetVersion>> {
let release =
latest_github_release("golang/tools", false, false, delegate.http_client()).await?;
let version: Option<String> = release.tag_name.strip_prefix("gopls/v").map(str::to_string);
@@ -63,7 +77,7 @@ impl super::LspAdapter for GoLspAdapter {
release.tag_name
);
}
Ok(Box::new(version) as Box<_>)
Ok(Box::new(GoLspAssetVersion { version }) as Box<_>)
}
async fn check_if_user_installed(
@@ -138,14 +152,18 @@ impl super::LspAdapter for GoLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
version: Box<dyn AssetVersion>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<Option<String>>().unwrap();
let version = &version
.as_any()
.downcast_ref::<GoLspAssetVersion>()
.unwrap()
.version;
let this = *self;
if let Some(version) = *version {
if let Some(version) = version {
let binary_path = container_dir.join(&format!("gopls_{version}"));
if let Ok(metadata) = fs::metadata(&binary_path).await {
if metadata.is_file() {

View File

@@ -9,7 +9,7 @@ use gpui::{AppContext, AsyncAppContext};
use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use node_runtime::{NodeAssetVersion, NodeRuntime};
use project::ContextProviderWithTasks;
use serde_json::{json, Value};
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
@@ -18,7 +18,6 @@ use smol::{
io::BufReader,
};
use std::{
any::Any,
env::consts,
ffi::OsString,
path::{Path, PathBuf},
@@ -26,7 +25,7 @@ use std::{
sync::{Arc, OnceLock},
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::{fs::remove_matching, maybe, ResultExt};
use util::{fs::remove_matching, maybe, AssetVersion, ResultExt};
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
@@ -142,7 +141,7 @@ impl LspAdapter for JsonLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<Box<dyn AssetVersion>> {
Ok(Box::new(
self.node
.npm_package_latest_version("vscode-langservers-extracted")
@@ -152,22 +151,25 @@ impl LspAdapter for JsonLspAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn AssetVersion>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let latest_version = latest_version
.as_any()
.downcast_ref::<NodeAssetVersion>()
.unwrap()
.clone();
let server_path = container_dir.join(SERVER_PATH);
let package_name = "vscode-langservers-extracted";
let should_install_language_server = self
.node
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
.should_install_npm_package(&latest_version, &server_path, &container_dir)
.await;
if should_install_language_server {
self.node
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
.npm_install_packages(&container_dir, &[latest_version])
.await?;
}
@@ -277,7 +279,7 @@ impl LspAdapter for NodeVersionAdapter {
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<Box<dyn AssetVersion>> {
let release = latest_github_release(
"zed-industries/package-version-server",
true,
@@ -310,11 +312,14 @@ impl LspAdapter for NodeVersionAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn AssetVersion>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = latest_version.downcast::<GitHubLspBinaryVersion>().unwrap();
let version = latest_version
.as_any()
.downcast_ref::<GitHubLspBinaryVersion>()
.unwrap();
let destination_path = container_dir.join(format!(
"package-version-server-{}{}",
version.name,

View File

@@ -2,14 +2,14 @@ use anyhow::Context;
use gpui::{AppContext, UpdateGlobal};
use json::json_task_context;
pub use language::*;
use node_runtime::NodeRuntime;
use node_runtime::{NodeAssetVersion, NodeRuntime};
use python::PythonContextProvider;
use rust_embed::RustEmbed;
use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{str, sync::Arc};
use std::{any::Any, str, sync::Arc};
use typescript::typescript_task_context;
use util::{asset_str, ResultExt};
use util::{asset_str, AssetVersion, ResultExt};
use crate::{bash::bash_task_context, go::GoContextProvider, rust::RustContextProvider};
@@ -311,3 +311,22 @@ fn load_queries(name: &str) -> LanguageQueries {
}
result
}
struct TypeScriptVersions {
typescript_version: NodeAssetVersion,
server_version: NodeAssetVersion,
}
impl AssetVersion for TypeScriptVersions {
fn description(&self) -> String {
format!(
"{} (LSP: {})",
self.typescript_version.description(),
self.server_version.description()
)
}
fn as_any(&self) -> &dyn Any {
self
}
}

View File

@@ -4,18 +4,19 @@ use gpui::AppContext;
use gpui::AsyncAppContext;
use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeAssetVersion;
use node_runtime::NodeRuntime;
use project::project_settings::ProjectSettings;
use serde_json::Value;
use settings::Settings;
use std::{
any::Any,
borrow::Cow,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::AssetVersion;
use util::ResultExt;
const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
@@ -45,7 +46,7 @@ impl LspAdapter for PythonLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
) -> Result<Box<dyn AssetVersion>> {
Ok(Box::new(
self.node
.npm_package_latest_version(Self::SERVER_NAME)
@@ -55,22 +56,31 @@ impl LspAdapter for PythonLspAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn AssetVersion>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let latest_version = &latest_version
.as_any()
.downcast_ref::<NodeAssetVersion>()
.unwrap()
.version;
let server_path = container_dir.join(SERVER_PATH);
let package_name = Self::SERVER_NAME;
let node_asset = NodeAssetVersion {
name: package_name.to_string(),
version: latest_version.to_string(),
};
let should_install_language_server = self
.node
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
.should_install_npm_package(&node_asset, &server_path, &container_dir)
.await;
if should_install_language_server {
self.node
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
.npm_install_packages(&container_dir, &[node_asset])
.await?;
}

View File

@@ -13,14 +13,13 @@ use regex::Regex;
use settings::Settings;
use smol::fs::{self, File};
use std::{
any::Any,
borrow::Cow,
env::consts,
path::{Path, PathBuf},
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::{fs::remove_matching, maybe, ResultExt};
use util::{fs::remove_matching, maybe, AssetVersion, ResultExt};
pub struct RustLspAdapter;
@@ -85,7 +84,7 @@ impl LspAdapter for RustLspAdapter {
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<Box<dyn AssetVersion>> {
let release = latest_github_release(
"rust-lang/rust-analyzer",
true,
@@ -113,11 +112,14 @@ impl LspAdapter for RustLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
version: Box<dyn AssetVersion>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let version = &version
.as_any()
.downcast_ref::<GitHubLspBinaryVersion>()
.unwrap();
let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
if fs::metadata(&destination_path).await.is_err() {

View File

@@ -5,18 +5,17 @@ use futures::StreamExt;
use gpui::AsyncAppContext;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use node_runtime::{NodeAssetVersion, NodeRuntime};
use project::project_settings::ProjectSettings;
use serde_json::{json, Value};
use settings::Settings;
use smol::fs;
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::{maybe, ResultExt};
use util::{maybe, AssetVersion, ResultExt};
#[cfg(target_os = "windows")]
const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server.ps1";
@@ -82,7 +81,7 @@ impl LspAdapter for TailwindLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
) -> Result<Box<dyn AssetVersion>> {
Ok(Box::new(
self.node
.npm_package_latest_version("@tailwindcss/language-server")
@@ -92,22 +91,25 @@ impl LspAdapter for TailwindLspAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn AssetVersion>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let latest_version = latest_version
.as_any()
.downcast_ref::<NodeAssetVersion>()
.unwrap()
.clone();
let server_path = container_dir.join(SERVER_PATH);
let package_name = "@tailwindcss/language-server";
let should_install_language_server = self
.node
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
.should_install_npm_package(&latest_version, &server_path, &container_dir)
.await;
if should_install_language_server {
self.node
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
.npm_install_packages(&container_dir, &[latest_version])
.await?;
}

View File

@@ -14,13 +14,14 @@ use serde_json::{json, Value};
use settings::Settings;
use smol::{fs, io::BufReader, stream::StreamExt};
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::{fs::remove_matching, maybe, ResultExt};
use util::{fs::remove_matching, maybe, AssetVersion, ResultExt};
use crate::TypeScriptVersions;
pub(super) fn typescript_task_context() -> ContextProviderWithTasks {
ContextProviderWithTasks::new(TaskTemplates(vec![
@@ -86,11 +87,6 @@ impl TypeScriptLspAdapter {
}
}
struct TypeScriptVersions {
typescript_version: String,
server_version: String,
}
#[async_trait(?Send)]
impl LspAdapter for TypeScriptLspAdapter {
fn name(&self) -> LanguageServerName {
@@ -100,7 +96,7 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<Box<dyn AssetVersion>> {
Ok(Box::new(TypeScriptVersions {
typescript_version: self.node.npm_package_latest_version("typescript").await?,
server_version: self
@@ -112,21 +108,22 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn AssetVersion>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
let latest_version = &latest_version
.as_any()
.downcast_ref::<TypeScriptVersions>()
.unwrap();
let server_path = container_dir.join(Self::NEW_SERVER_PATH);
let package_name = "typescript";
let should_install_language_server = self
.node
.should_install_npm_package(
package_name,
&latest_version.typescript_version,
&server_path,
&container_dir,
latest_version.typescript_version.as_str(),
)
.await;
@@ -135,11 +132,8 @@ impl LspAdapter for TypeScriptLspAdapter {
.npm_install_packages(
&container_dir,
&[
(package_name, latest_version.typescript_version.as_str()),
(
"typescript-language-server",
latest_version.server_version.as_str(),
),
latest_version.typescript_version.clone(),
latest_version.server_version.clone(),
],
)
.await?;
@@ -410,7 +404,7 @@ impl LspAdapter for EsLintLspAdapter {
async fn fetch_latest_server_version(
&self,
_delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<Box<dyn AssetVersion>> {
let url = build_asset_url(
"microsoft/vscode-eslint",
Self::CURRENT_VERSION,
@@ -425,11 +419,14 @@ impl LspAdapter for EsLintLspAdapter {
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
version: Box<dyn AssetVersion>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let version = &version
.as_any()
.downcast_ref::<GitHubLspBinaryVersion>()
.unwrap();
let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name));
let server_path = destination_path.join(Self::SERVER_PATH);

View File

@@ -9,12 +9,13 @@ use project::project_settings::{BinarySettings, ProjectSettings};
use serde_json::{json, Value};
use settings::Settings;
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::{maybe, ResultExt};
use util::{maybe, AssetVersion, ResultExt};
use crate::TypeScriptVersions;
fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
@@ -44,11 +45,6 @@ impl VtslsLspAdapter {
}
}
struct TypeScriptVersions {
typescript_version: String,
server_version: String,
}
const SERVER_NAME: &'static str = "vtsls";
#[async_trait(?Send)]
impl LspAdapter for VtslsLspAdapter {
@@ -59,7 +55,7 @@ impl LspAdapter for VtslsLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
) -> Result<Box<dyn AssetVersion>> {
Ok(Box::new(TypeScriptVersions {
typescript_version: self.node.npm_package_latest_version("typescript").await?,
server_version: self
@@ -113,21 +109,22 @@ impl LspAdapter for VtslsLspAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn AssetVersion>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
let latest_version = &latest_version
.as_any()
.downcast_ref::<TypeScriptVersions>()
.unwrap();
let server_path = container_dir.join(Self::SERVER_PATH);
let package_name = "typescript";
let should_install_language_server = self
.node
.should_install_npm_package(
package_name,
&latest_version.typescript_version,
&server_path,
&container_dir,
latest_version.typescript_version.as_str(),
)
.await;
@@ -136,11 +133,8 @@ impl LspAdapter for VtslsLspAdapter {
.npm_install_packages(
&container_dir,
&[
(package_name, latest_version.typescript_version.as_str()),
(
"@vtsls/language-server",
latest_version.server_version.as_str(),
),
latest_version.typescript_version.clone(),
latest_version.server_version.clone(),
],
)
.await?;

View File

@@ -6,16 +6,15 @@ use language::{
language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use node_runtime::{NodeAssetVersion, NodeRuntime};
use serde_json::Value;
use smol::fs;
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::{maybe, ResultExt};
use util::{maybe, AssetVersion, ResultExt};
const SERVER_PATH: &str = "node_modules/yaml-language-server/bin/yaml-language-server";
@@ -42,7 +41,7 @@ impl LspAdapter for YamlLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
) -> Result<Box<dyn AssetVersion>> {
Ok(Box::new(
self.node
.npm_package_latest_version("yaml-language-server")
@@ -52,22 +51,27 @@ impl LspAdapter for YamlLspAdapter {
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
latest_version: Box<dyn AssetVersion>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let latest_version = &latest_version.as_any().downcast_ref::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let package_name = "yaml-language-server";
let node_asset = NodeAssetVersion {
name: package_name.to_string(),
version: latest_version.to_string(),
};
let should_install_language_server = self
.node
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
.should_install_npm_package(&node_asset, &server_path, &container_dir)
.await;
if should_install_language_server {
self.node
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
.npm_install_packages(&container_dir, &[node_asset])
.await?;
}

View File

@@ -32,6 +32,8 @@ smol.workspace = true
tempfile = { workspace = true, optional = true }
util.workspace = true
walkdir = "2.5.0"
gpui.workspace = true
which.workspace = true
[target.'cfg(windows)'.dependencies]
async-std = { version = "1.12.0", features = ["unstable"] }

View File

@@ -4,25 +4,32 @@ use anyhow::{anyhow, bail, Context, Result};
pub use archive::extract_zip;
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use futures::channel::mpsc;
use futures::channel::oneshot;
use futures::AsyncReadExt;
use gpui::SemanticVersion;
use http_client::HttpClient;
use semver::Version;
use serde::Deserialize;
use smol::io::BufReader;
use smol::{fs, lock::Mutex, process::Command};
use std::fmt::Display;
use std::io;
use std::io::ErrorKind;
use std::process::{Output, Stdio};
use std::str::from_utf8;
use std::str::FromStr;
use std::{
env::consts,
path::{Path, PathBuf},
sync::Arc,
};
use util::ResultExt;
use util::{AssetVersion, ResultExt};
#[cfg(windows)]
use smol::process::windows::CommandExt;
const VERSION: &str = "v22.5.1";
const VERSION: SemanticVersion = SemanticVersion::new(22, 5, 1);
#[cfg(not(windows))]
const NODE_PATH: &str = "bin/node";
@@ -52,6 +59,28 @@ pub struct NpmInfoDistTags {
latest: Option<String>,
}
#[derive(Debug, Clone)]
pub struct NodeAssetVersion {
pub name: String,
pub version: String,
}
impl Display for NodeAssetVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}@{}", self.name, self.version))
}
}
impl util::AssetVersion for NodeAssetVersion {
fn description(&self) -> String {
format!("node module {}@{}", self.name, self.version)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[async_trait::async_trait]
pub trait NodeRuntime: Send + Sync {
async fn binary_path(&self) -> Result<PathBuf>;
@@ -63,10 +92,13 @@ pub trait NodeRuntime: Send + Sync {
args: &[&str],
) -> Result<Output>;
async fn npm_package_latest_version(&self, name: &str) -> Result<String>;
async fn npm_package_latest_version(&self, name: &str) -> Result<NodeAssetVersion>;
async fn npm_install_packages(&self, directory: &Path, packages: &[(&str, &str)])
-> Result<()>;
async fn npm_install_packages(
&self,
directory: &Path,
packages: &[NodeAssetVersion],
) -> Result<()>;
async fn npm_package_installed_version(
&self,
@@ -76,10 +108,9 @@ pub trait NodeRuntime: Send + Sync {
async fn should_install_npm_package(
&self,
package_name: &str,
package: &NodeAssetVersion,
local_executable_path: &Path,
local_package_directory: &PathBuf,
latest_version: &str,
) -> bool {
// In the case of the local system not having the package installed,
// or in the instances where we fail to parse package.json data,
@@ -89,7 +120,7 @@ pub trait NodeRuntime: Send + Sync {
}
let Some(installed_version) = self
.npm_package_installed_version(local_package_directory, package_name)
.npm_package_installed_version(local_package_directory, &package.name)
.await
.log_err()
.flatten()
@@ -100,7 +131,7 @@ pub trait NodeRuntime: Send + Sync {
let Some(installed_version) = Version::parse(&installed_version).log_err() else {
return true;
};
let Some(latest_version) = Version::parse(&latest_version).log_err() else {
let Some(latest_version) = Version::parse(&package.version).log_err() else {
return true;
};
@@ -111,19 +142,45 @@ pub trait NodeRuntime: Send + Sync {
pub struct RealNodeRuntime {
http: Arc<dyn HttpClient>,
installation_lock: Mutex<()>,
check_can_install: mpsc::UnboundedSender<(Box<dyn AssetVersion>, oneshot::Sender<Result<()>>)>,
can_use_dependencies: mpsc::UnboundedSender<oneshot::Sender<bool>>,
}
impl RealNodeRuntime {
pub fn new(http: Arc<dyn HttpClient>) -> Arc<dyn NodeRuntime> {
pub fn new(
http: Arc<dyn HttpClient>,
check_can_install: mpsc::UnboundedSender<(
Box<dyn AssetVersion>,
oneshot::Sender<Result<()>>,
)>,
can_use_dependencies: mpsc::UnboundedSender<oneshot::Sender<bool>>,
) -> Arc<dyn NodeRuntime> {
Arc::new(RealNodeRuntime {
http,
installation_lock: Mutex::new(()),
check_can_install,
can_use_dependencies,
})
}
async fn install_if_needed(&self) -> Result<PathBuf> {
let _lock = self.installation_lock.lock().await;
async fn check_dependency(&self, asset_version: impl AssetVersion) -> Result<()> {
let (tx, rx) = oneshot::channel();
self.check_can_install
.unbounded_send((Box::new(asset_version), tx))
.ok();
rx.await?
}
async fn can_install_dependencies(&self) -> bool {
let (tx, rx) = oneshot::channel();
self.can_use_dependencies.unbounded_send(tx).ok();
rx.await.unwrap_or(false)
}
async fn install_if_needed(&self) -> Result<(PathBuf, PathBuf, PathBuf)> {
log::info!("Node runtime install_if_needed");
let _lock = self.installation_lock.lock().await;
let os = match consts::OS {
"macos" => "darwin",
@@ -138,24 +195,101 @@ impl RealNodeRuntime {
other => bail!("Running on unsupported architecture: {other}"),
};
let folder_name = format!("node-{VERSION}-{os}-{arch}");
let mut found_node_version: Option<SemanticVersion> = None;
#[cfg(not(windows))]
let node_path_binary = "node";
#[cfg(not(windows))]
let npm_path_binary = "npm";
#[cfg(windows)]
let node_path_binary = "node.exe";
#[cfg(windows)]
let npm_path_binary = "npm.exe"; // ???
if let Some(node_path) = which::which(node_path_binary).log_err() {
if let Some(npm_path) = which::which(npm_path_binary).log_err() {
let mut path_command = Command::new(dbg!(&node_path));
path_command
.env_clear()
.arg("--version")
.stdin(Stdio::null())
.stderr(Stdio::null());
#[cfg(windows)]
path_command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
let output = path_command.output().await;
if let Some(output) = output.log_err() {
if output.status.success() {
if let Ok(output) = from_utf8(&output.stdout) {
let output = output.trim();
let output = if let Some(output) = output.strip_prefix("v") {
output
} else {
output
};
if let Some(node_version) = SemanticVersion::from_str(output).log_err()
{
found_node_version = Some(node_version);
if node_version >= VERSION {
let folder_name = format!("node-v{node_version}-{os}-{arch}");
let node_containing_dir = paths::support_dir().join("node");
let node_dir = node_containing_dir.join(folder_name);
// Make sure the proper file structure is setup
if fs::metadata(&node_dir)
.await
.is_err_and(|e| e.kind() == ErrorKind::NotFound)
{
_ = fs::create_dir_all(node_dir.join("cache")).await;
_ = fs::write(node_dir.join("blank_user_npmrc"), []).await;
_ = fs::write(node_dir.join("blank_global_npmrc"), [])
.await;
}
return Ok((node_path, npm_path, node_dir));
} else {
log::error!(
"node version on PATH is too old: v{}, Zed requires: v{}",
node_version,
VERSION
)
}
}
}
}
}
}
}
if !self.can_install_dependencies().await {
let err = if let Some(node_version) = found_node_version {
anyhow!(
"Node version on $PATH is too old. Verion required: {}, version found: {}",
VERSION,
node_version
)
} else {
anyhow!("Could not find or use node on $PATH")
};
return Err(err);
}
let folder_name = format!("node-v{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 mut command = Command::new(&node_binary);
command
.env_clear()
.arg(npm_file)
.arg("--version")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.args(["--cache".into(), node_dir.join("cache")])
.args(["--userconfig".into(), node_dir.join("blank_user_npmrc")])
.args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]);
.stdout(Stdio::null());
#[cfg(windows)]
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
@@ -164,6 +298,12 @@ impl RealNodeRuntime {
let valid = matches!(result, Ok(status) if status.success());
if !valid {
self.check_dependency(NodeAssetVersion {
name: "node".to_string(),
version: format!("{}", VERSION),
})
.await?;
_ = fs::remove_dir_all(&node_containing_dir).await;
fs::create_dir(&node_containing_dir)
.await
@@ -176,13 +316,13 @@ impl RealNodeRuntime {
};
let file_name = format!(
"node-{VERSION}-{os}-{arch}.{extension}",
"node-v{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 url = format!("https://nodejs.org/dist/v{VERSION}/{file_name}");
let mut response = self
.http
.get(&url, Default::default(), true)
@@ -198,22 +338,21 @@ impl RealNodeRuntime {
}
ArchiveType::Zip => archive::extract_zip(&node_containing_dir, body).await?,
}
_ = fs::create_dir(node_dir.join("cache")).await;
_ = fs::write(node_dir.join("blank_user_npmrc"), []).await;
_ = fs::write(node_dir.join("blank_global_npmrc"), []).await;
}
// Note: Not in the `if !valid {}` so we can populate these for existing installations
_ = fs::create_dir(node_dir.join("cache")).await;
_ = fs::write(node_dir.join("blank_user_npmrc"), []).await;
_ = fs::write(node_dir.join("blank_global_npmrc"), []).await;
anyhow::Ok(node_dir)
anyhow::Ok((node_dir.join(NODE_PATH), node_dir.join(NPM_PATH), node_dir))
}
}
#[async_trait::async_trait]
impl NodeRuntime for RealNodeRuntime {
async fn binary_path(&self) -> Result<PathBuf> {
let installation_path = self.install_if_needed().await?;
Ok(installation_path.join(NODE_PATH))
let (binary_path, _, _) = self.install_if_needed().await?;
Ok(binary_path)
}
async fn run_npm_subcommand(
@@ -223,10 +362,8 @@ impl NodeRuntime for RealNodeRuntime {
args: &[&str],
) -> Result<Output> {
let attempt = || async move {
let installation_path = self.install_if_needed().await?;
let (node_binary, npm_path, node_dir) = self.install_if_needed().await?;
let node_binary = installation_path.join(NODE_PATH);
let npm_file = installation_path.join(NPM_PATH);
let mut env_path = vec![node_binary
.parent()
.expect("invalid node binary path")
@@ -244,23 +381,17 @@ impl NodeRuntime for RealNodeRuntime {
return Err(anyhow!("missing node binary file"));
}
if smol::fs::metadata(&npm_file).await.is_err() {
if smol::fs::metadata(&npm_path).await.is_err() {
return Err(anyhow!("missing npm file"));
}
let mut command = Command::new(node_binary);
command.env_clear();
command.env("PATH", env_path);
command.arg(npm_file).arg(subcommand);
command.args(["--cache".into(), installation_path.join("cache")]);
command.args([
"--userconfig".into(),
installation_path.join("blank_user_npmrc"),
]);
command.args([
"--globalconfig".into(),
installation_path.join("blank_global_npmrc"),
]);
command.arg(&npm_path).arg(subcommand);
command.args(["--cache".into(), node_dir.join("cache")]);
command.args(["--userconfig".into(), node_dir.join("blank_user_npmrc")]);
command.args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]);
command.args(args);
if let Some(directory) = directory {
@@ -327,7 +458,7 @@ impl NodeRuntime for RealNodeRuntime {
output.map_err(|e| anyhow!("{e}"))
}
async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
async fn npm_package_latest_version(&self, name: &str) -> Result<NodeAssetVersion> {
let output = self
.run_npm_subcommand(
None,
@@ -346,10 +477,16 @@ impl NodeRuntime for RealNodeRuntime {
.await?;
let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
info.dist_tags
let version = info
.dist_tags
.latest
.or_else(|| info.versions.pop())
.ok_or_else(|| anyhow!("no version found for npm package {}", name))
.ok_or_else(|| anyhow!("no version found for npm package {}", name))?;
Ok(NodeAssetVersion {
name: name.to_string(),
version,
})
}
async fn npm_package_installed_version(
@@ -385,11 +522,19 @@ impl NodeRuntime for RealNodeRuntime {
async fn npm_install_packages(
&self,
directory: &Path,
packages: &[(&str, &str)],
packages: &[NodeAssetVersion],
) -> Result<()> {
if packages.len() == 0 {
return Ok(());
}
self.check_dependency(packages[0].clone()).await?;
let packages: Vec<_> = packages
.into_iter()
.map(|(name, version)| format!("{name}@{version}"))
.map(|node_asset_version| {
format!("{}@{}", node_asset_version.name, node_asset_version.version)
})
.collect();
let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect();
@@ -432,7 +577,7 @@ impl NodeRuntime for FakeNodeRuntime {
unreachable!("Should not run npm subcommand '{subcommand}' with args {args:?}")
}
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<NodeAssetVersion> {
unreachable!("Should not query npm package '{name}' for latest version")
}
@@ -447,7 +592,7 @@ impl NodeRuntime for FakeNodeRuntime {
async fn npm_install_packages(
&self,
_: &Path,
packages: &[(&str, &str)],
packages: &[NodeAssetVersion],
) -> anyhow::Result<()> {
unreachable!("Should not install packages {packages:?}")
}

View File

@@ -13,13 +13,16 @@ use futures::{
};
use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
use language::{
language_settings::{Formatter, LanguageSettings, SelectedFormatter},
language_settings::{
AllLanguageSettings, DependencyManagement, Formatter, LanguageSettings, SelectedFormatter,
},
Buffer, LanguageServerName, LocalFile,
};
use lsp::{LanguageServer, LanguageServerId};
use node_runtime::NodeRuntime;
use paths::default_prettier_dir;
use prettier::Prettier;
use settings::Settings;
use util::{ResultExt, TryFutureExt};
use crate::{
@@ -372,13 +375,13 @@ async fn install_prettier_packages(
.chain(Some(&"prettier".into()))
.map(|package_name| async {
let returned_package_name = package_name.to_string();
let latest_version = node
let asset_version = node
.npm_package_latest_version(package_name)
.await
.with_context(|| {
format!("fetching latest npm version for package {returned_package_name}")
})?;
anyhow::Ok((returned_package_name, latest_version))
anyhow::Ok(asset_version)
}),
)
.await
@@ -399,11 +402,7 @@ async fn install_prettier_packages(
}
log::info!("Installing default prettier and plugins: {packages_to_versions:?}");
let borrowed_packages = packages_to_versions
.iter()
.map(|(package, version)| (package.as_str(), version.as_str()))
.collect::<Vec<_>>();
node.npm_install_packages(default_prettier_dir, &borrowed_packages)
node.npm_install_packages(default_prettier_dir, &packages_to_versions)
.await
.context("fetching formatter packages")?;
anyhow::Ok(())
@@ -632,6 +631,7 @@ impl Project {
plugins: impl Iterator<Item = Arc<str>>,
cx: &mut ModelContext<Self>,
) {
let install_dependencies = AllLanguageSettings::get_global(cx).dependency_management;
if cfg!(any(test, feature = "test-support")) {
self.default_prettier.installed_plugins.extend(plugins);
self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance {
@@ -746,7 +746,13 @@ impl Project {
let installed_plugins = new_plugins.clone();
cx.background_executor()
.spawn(async move {
install_prettier_packages(fs.as_ref(), new_plugins, node).await?;
match install_dependencies {
DependencyManagement::Automatic => {
install_prettier_packages(fs.as_ref(), new_plugins, node).await?
},
DependencyManagement::None => Err(anyhow!("No prettier installed"))?,
}
// Save the server file last, so the reinstall need could be determined by the absence of the file.
save_prettier_server_file(fs.as_ref()).await?;
anyhow::Ok(())

View File

@@ -44,8 +44,8 @@ use http_client::HttpClient;
use itertools::Itertools;
use language::{
language_settings::{
language_settings, AllLanguageSettings, FormatOnSave, Formatter, InlayHintKind,
LanguageSettings, SelectedFormatter,
language_settings, AllLanguageSettings, DependencyManagement, FormatOnSave, Formatter,
InlayHintKind, LanguageSettings, SelectedFormatter,
},
markdown, point_to_lsp, prepare_completion_documentation,
proto::{
@@ -3058,16 +3058,15 @@ impl Project {
let stderr_capture = Arc::new(Mutex::new(Some(String::new())));
let lsp_adapter_delegate = ProjectLspAdapterDelegate::new(self, worktree_handle, cx);
let pending_server = match self.languages.create_pending_language_server(
let Some(pending_server) = self.languages.create_pending_language_server(
stderr_capture.clone(),
language.clone(),
adapter.clone(),
Arc::clone(&worktree_path),
lsp_adapter_delegate.clone(),
cx,
) {
Some(pending_server) => pending_server,
None => return,
) else {
return;
};
let project_settings = ProjectSettings::get(
@@ -3091,7 +3090,7 @@ impl Project {
cx.spawn(move |this, mut cx| async move {
let result = Self::setup_and_insert_language_server(
this.clone(),
lsp_adapter_delegate,
lsp_adapter_delegate.clone(),
override_options,
pending_server,
adapter.clone(),
@@ -3110,6 +3109,22 @@ impl Project {
Err(err) => {
log::error!("failed to start language server {server_name:?}: {err}");
let install_dependencies = cx
.update(|cx| AllLanguageSettings::get_global(cx).dependency_management)
.log_err();
if install_dependencies != Some(DependencyManagement::Automatic) {
cx.update(move |cx| {
lsp_adapter_delegate.show_notification(
format!(
"failed to start language server {server_name:?}: {err}"
)
.as_ref(),
cx,
);
})
.log_err();
return None;
}
log::error!("server stderr: {:?}", stderr_capture.lock().take());
let this = this.upgrade()?;
@@ -3119,6 +3134,16 @@ impl Project {
if attempt_count >= MAX_SERVER_REINSTALL_ATTEMPT_COUNT {
let max = MAX_SERVER_REINSTALL_ATTEMPT_COUNT;
log::error!("Hit {max} reinstallation attempts for {server_name:?}");
cx.update(move |cx| {
lsp_adapter_delegate.show_notification(
format!(
"failed to start language server {server_name:?}: {err}"
)
.as_ref(),
cx,
);
})
.log_err();
return None;
}

View File

@@ -8,6 +8,7 @@ pub mod test;
use futures::Future;
use rand::{seq::SliceRandom, Rng};
use regex::Regex;
use std::any::Any;
use std::sync::OnceLock;
use std::{
borrow::Cow,
@@ -328,6 +329,19 @@ pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {
}
}
pub trait AssetVersion: Sync + Send + 'static {
fn description(&self) -> String;
fn as_any(&self) -> &dyn Any;
}
impl std::fmt::Debug for dyn AssetVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn AssetVersion")
.field("description", &self.description())
.finish()
}
}
pub trait ResultExt<E> {
type Ok;

View File

@@ -16,14 +16,17 @@ use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use env_logger::Builder;
use fs::{Fs, RealFs};
use futures::{future, StreamExt};
use futures::{channel::mpsc, future, StreamExt};
use git::GitHostingProviderRegistry;
use gpui::{
Action, App, AppContext, AsyncAppContext, Context, DismissEvent, Global, Task,
UpdateGlobal as _, VisualContext,
};
use image_viewer;
use language::LanguageRegistry;
use language::{
language_settings::{AllLanguageSettings, DependencyManagement},
LanguageRegistry,
};
use log::LevelFilter;
use assets::Assets;
@@ -448,7 +451,43 @@ fn main() {
LanguageRegistry::new(login_shell_env_loaded, 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 (check_server_tx, mut check_server_rx) = mpsc::unbounded();
let (can_use_server_tx, mut can_use_server_rx) = mpsc::unbounded();
let node_runtime = RealNodeRuntime::new(client.http_client(), check_server_tx, can_use_server_tx);
cx.spawn(|cx| {
async move {
while let Some((asset_version, response)) = check_server_rx.next().await {
let install_dependencies = cx
.update(|cx| AllLanguageSettings::get_global(cx).dependency_management)
.log_err();
if install_dependencies == Some(DependencyManagement::Automatic) {
response.send(Ok(())).ok();
} else {
response.send(
Err(
anyhow!("Could not find {}. Not installing because \"dependency_management\" is set to \"never\".", asset_version.description()))).ok();
}
}
}
})
.detach();
cx.spawn(|cx| {
async move {
while let Some(response) = can_use_server_rx.next().await {
let install_dependencies = cx
.update(|cx| AllLanguageSettings::get_global(cx).dependency_management)
.log_err();
match install_dependencies {
Some(DependencyManagement::Automatic) => response.send(true).ok(),
_ => response.send(false).ok(),
};
}
}
})
.detach();
language::init(cx);
languages::init(languages.clone(), node_runtime.clone(), cx);
@@ -484,6 +523,7 @@ fn main() {
});
AppState::set_global(Arc::downgrade(&app_state), cx);
auto_update::init(client.http_client(), cx);
reliability::init(client.http_client(), installation_id, cx);
let prompt_builder = init_common(app_state.clone(), cx);