Compare commits

...

5 Commits

Author SHA1 Message Date
Smit Barmase
be4785fa65 use pathbuf 2025-06-10 12:41:48 +05:30
Smit Barmase
4cfd9bddcf manifest name 2025-06-10 12:41:48 +05:30
Smit Barmase
7372bd0919 feat. workspace scope 2025-06-10 12:41:46 +05:30
Smit Barmase
b3d63324e1 manifest provider 2025-06-10 12:41:07 +05:30
Smit Barmase
97e5ebade9 move eslint server out of ts-server 2025-06-10 12:41:03 +05:30
5 changed files with 397 additions and 275 deletions

View File

@@ -323,6 +323,8 @@ pub trait LspAdapterDelegate: Send + Sync {
fn http_client(&self) -> Arc<dyn HttpClient>;
fn worktree_id(&self) -> WorktreeId;
fn worktree_root_path(&self) -> &Path;
fn subproject_root_path(&self) -> PathBuf;
fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool;
fn update_status(&self, language: LanguageServerName, status: BinaryStatus);
fn registered_lsp_adapters(&self) -> Vec<Arc<dyn LspAdapter>>;
async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>>;
@@ -335,6 +337,7 @@ pub trait LspAdapterDelegate: Send + Sync {
async fn shell_env(&self) -> HashMap<String, String>;
async fn read_text_file(&self, path: PathBuf) -> Result<String>;
async fn try_exec(&self, binary: LanguageServerBinary) -> Result<()>;
fn set_workspace_scope(&self, scope_uri: Option<lsp::Url>);
}
#[async_trait(?Send)]

View File

@@ -0,0 +1,327 @@
use anyhow::{Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use gpui::{AsyncApp, SharedString};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
use language::{
Attach, LanguageToolchainStore, LspAdapter, LspAdapterDelegate, ManifestName, ManifestProvider,
ManifestQuery,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use smol::{fs, io::BufReader, stream::StreamExt};
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::archive::extract_zip;
use util::{fs::remove_matching, merge_json_value_into};
fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![
"--max-old-space-size=8192".into(),
server_path.into(),
"--stdio".into(),
]
}
pub struct EsLintLspAdapter {
node: NodeRuntime,
}
impl EsLintLspAdapter {
const CURRENT_VERSION: &'static str = "2.4.4";
const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
#[cfg(not(windows))]
const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
#[cfg(windows)]
const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.cjs",
"eslint.config.ts",
"eslint.config.cts",
"eslint.config.mts",
];
const LEGACY_CONFIG_FILE_NAMES: &'static [&'static str] = &[
".eslintrc.js",
".eslintrc.cjs",
".eslintrc.mjs",
".eslintrc.yaml",
".eslintrc.yml",
".eslintrc.json",
".eslintrc",
];
pub fn new(node: NodeRuntime) -> Self {
EsLintLspAdapter { node }
}
fn build_destination_path(container_dir: &Path) -> PathBuf {
container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
}
}
pub(crate) struct EsLintConfigProvider;
impl ManifestProvider for EsLintConfigProvider {
fn name(&self) -> ManifestName {
SharedString::new_static("eslint").into()
}
fn search(
&self,
ManifestQuery {
path,
depth,
delegate,
}: ManifestQuery,
) -> Option<Arc<Path>> {
for path in path.ancestors().take(depth) {
for config_file in EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES {
let config_path = path.join(config_file);
if delegate.exists(&config_path, Some(false)) {
return Some(path.into());
}
}
for config_file in EsLintLspAdapter::LEGACY_CONFIG_FILE_NAMES {
let config_path = path.join(config_file);
if delegate.exists(&config_path, Some(false)) {
return Some(path.into());
}
}
}
None
}
}
#[async_trait(?Send)]
impl LspAdapter for EsLintLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME.clone()
}
fn manifest_name(&self) -> Option<ManifestName> {
Some(SharedString::new_static("eslint").into())
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
Some(vec![
CodeActionKind::QUICKFIX,
CodeActionKind::new("source.fixAll.eslint"),
])
}
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncApp,
) -> Result<Value> {
let workspace_root = delegate.worktree_root_path();
dbg!(&delegate.worktree_root_path());
dbg!(&delegate.subproject_root_path());
let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
.iter()
.any(|file| workspace_root.join(file).is_file());
let mut default_workspace_configuration = json!({
"validate": "on",
"rulesCustomizations": [],
"run": "onType",
"nodePath": null,
"workingDirectory": {
"mode": "auto"
},
"workspaceFolder": {
"uri": workspace_root,
"name": workspace_root.file_name()
.unwrap_or(workspace_root.as_os_str())
.to_string_lossy(),
},
"problems": {},
"codeActionOnSave": {
// We enable this, but without also configuring code_actions_on_format
// in the Zed configuration, it doesn't have an effect.
"enable": true,
},
"codeAction": {
"disableRuleComment": {
"enable": true,
"location": "separateLine",
},
"showDocumentation": {
"enable": true
}
},
"experimental": {
"useFlatConfig": use_flat_config,
},
});
let override_options = cx.update(|cx| {
language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
.and_then(|s| s.settings.clone())
})?;
if let Some(override_options) = override_options {
merge_json_value_into(override_options, &mut default_workspace_configuration);
}
Ok(json!({
"": default_workspace_configuration
}))
}
async fn fetch_latest_server_version(
&self,
_delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let url = build_asset_url(
"zed-industries/vscode-eslint",
Self::CURRENT_VERSION_TAG_NAME,
Self::GITHUB_ASSET_KIND,
)?;
Ok(Box::new(GitHubLspBinaryVersion {
name: Self::CURRENT_VERSION.into(),
url,
}))
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let destination_path = Self::build_destination_path(&container_dir);
let server_path = destination_path.join(Self::SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
remove_matching(&container_dir, |entry| entry != destination_path).await;
let mut response = delegate
.http_client()
.get(&version.url, Default::default(), true)
.await
.context("downloading release")?;
match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz => {
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(&destination_path).await.with_context(|| {
format!("extracting {} to {:?}", version.url, destination_path)
})?;
}
AssetKind::Gz => {
let mut decompressed_bytes =
GzipDecoder::new(BufReader::new(response.body_mut()));
let mut file =
fs::File::create(&destination_path).await.with_context(|| {
format!(
"creating a file {:?} for a download from {}",
destination_path, version.url,
)
})?;
futures::io::copy(&mut decompressed_bytes, &mut file)
.await
.with_context(|| {
format!("extracting {} to {:?}", version.url, destination_path)
})?;
}
AssetKind::Zip => {
extract_zip(&destination_path, response.body_mut())
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
}
}
let mut dir = fs::read_dir(&destination_path).await?;
let first = dir.next().await.context("missing first file")??;
let repo_root = destination_path.join("vscode-eslint");
fs::rename(first.path(), &repo_root).await?;
#[cfg(target_os = "windows")]
{
handle_symlink(
repo_root.join("$shared"),
repo_root.join("client").join("src").join("shared"),
)
.await?;
handle_symlink(
repo_root.join("$shared"),
repo_root.join("server").join("src").join("shared"),
)
.await?;
}
self.node
.run_npm_subcommand(&repo_root, "install", &[])
.await?;
self.node
.run_npm_subcommand(&repo_root, "run-script", &["compile"])
.await?;
}
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let server_path =
Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
}
#[cfg(target_os = "windows")]
async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
anyhow::ensure!(
fs::metadata(&src_dir).await.is_ok(),
"Directory {src_dir:?} is not present"
);
if fs::metadata(&dest_dir).await.is_ok() {
fs::remove_file(&dest_dir).await?;
}
fs::create_dir_all(&dest_dir).await?;
let mut entries = fs::read_dir(&src_dir).await?;
while let Some(entry) = entries.try_next().await? {
let entry_path = entry.path();
let entry_name = entry.file_name();
let dest_path = dest_dir.join(&entry_name);
fs::copy(&entry_path, &dest_path).await?;
}
Ok(())
}

View File

@@ -1,4 +1,5 @@
use anyhow::Context as _;
use eslint::EsLintConfigProvider;
use gpui::{App, UpdateGlobal};
use json::json_task_context;
use node_runtime::NodeRuntime;
@@ -15,6 +16,7 @@ pub use language::*;
mod bash;
mod c;
mod css;
mod eslint;
mod go;
mod json;
mod python;
@@ -75,7 +77,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
let c_lsp_adapter = Arc::new(c::CLspAdapter);
let css_lsp_adapter = Arc::new(css::CssLspAdapter::new(node.clone()));
let eslint_adapter = Arc::new(typescript::EsLintLspAdapter::new(node.clone()));
let eslint_adapter = Arc::new(eslint::EsLintLspAdapter::new(node.clone()));
let go_context_provider = Arc::new(go::GoContextProvider);
let go_lsp_adapter = Arc::new(go::GoLspAdapter);
let json_context_provider = Arc::new(json_task_context());
@@ -303,9 +305,10 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
anyhow::Ok(())
})
.detach();
let manifest_providers: [Arc<dyn ManifestProvider>; 2] = [
let manifest_providers: [Arc<dyn ManifestProvider>; 3] = [
Arc::from(CargoManifestProvider),
Arc::from(PyprojectTomlManifestProvider),
Arc::from(EsLintConfigProvider),
];
for provider in manifest_providers {
project::ManifestProviders::global(cx).register(provider);

View File

@@ -1,11 +1,8 @@
use anyhow::{Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use chrono::{DateTime, Local};
use collections::HashMap;
use gpui::{App, AppContext, AsyncApp, Task};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
use language::{
ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
};
@@ -13,7 +10,7 @@ use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt};
use smol::lock::RwLock;
use std::{
any::Any,
borrow::Cow,
@@ -22,9 +19,7 @@ use std::{
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::archive::extract_zip;
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
use util::{ResultExt, maybe};
pub(crate) struct TypeScriptContextProvider {
last_package_json: PackageJsonContents,
@@ -713,250 +708,6 @@ async fn get_cached_ts_server_binary(
.log_err()
}
pub struct EsLintLspAdapter {
node: NodeRuntime,
}
impl EsLintLspAdapter {
const CURRENT_VERSION: &'static str = "2.4.4";
const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
#[cfg(not(windows))]
const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
#[cfg(windows)]
const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.cjs",
"eslint.config.ts",
"eslint.config.cts",
"eslint.config.mts",
];
pub fn new(node: NodeRuntime) -> Self {
EsLintLspAdapter { node }
}
fn build_destination_path(container_dir: &Path) -> PathBuf {
container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
}
}
#[async_trait(?Send)]
impl LspAdapter for EsLintLspAdapter {
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
Some(vec![
CodeActionKind::QUICKFIX,
CodeActionKind::new("source.fixAll.eslint"),
])
}
async fn workspace_configuration(
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncApp,
) -> Result<Value> {
let workspace_root = delegate.worktree_root_path();
let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
.iter()
.any(|file| workspace_root.join(file).is_file());
let mut default_workspace_configuration = json!({
"validate": "on",
"rulesCustomizations": [],
"run": "onType",
"nodePath": null,
"workingDirectory": {
"mode": "auto"
},
"workspaceFolder": {
"uri": workspace_root,
"name": workspace_root.file_name()
.unwrap_or(workspace_root.as_os_str())
.to_string_lossy(),
},
"problems": {},
"codeActionOnSave": {
// We enable this, but without also configuring code_actions_on_format
// in the Zed configuration, it doesn't have an effect.
"enable": true,
},
"codeAction": {
"disableRuleComment": {
"enable": true,
"location": "separateLine",
},
"showDocumentation": {
"enable": true
}
},
"experimental": {
"useFlatConfig": use_flat_config,
},
});
let override_options = cx.update(|cx| {
language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
.and_then(|s| s.settings.clone())
})?;
if let Some(override_options) = override_options {
merge_json_value_into(override_options, &mut default_workspace_configuration);
}
Ok(json!({
"": default_workspace_configuration
}))
}
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME.clone()
}
async fn fetch_latest_server_version(
&self,
_delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let url = build_asset_url(
"zed-industries/vscode-eslint",
Self::CURRENT_VERSION_TAG_NAME,
Self::GITHUB_ASSET_KIND,
)?;
Ok(Box::new(GitHubLspBinaryVersion {
name: Self::CURRENT_VERSION.into(),
url,
}))
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let destination_path = Self::build_destination_path(&container_dir);
let server_path = destination_path.join(Self::SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
remove_matching(&container_dir, |entry| entry != destination_path).await;
let mut response = delegate
.http_client()
.get(&version.url, Default::default(), true)
.await
.context("downloading release")?;
match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz => {
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(&destination_path).await.with_context(|| {
format!("extracting {} to {:?}", version.url, destination_path)
})?;
}
AssetKind::Gz => {
let mut decompressed_bytes =
GzipDecoder::new(BufReader::new(response.body_mut()));
let mut file =
fs::File::create(&destination_path).await.with_context(|| {
format!(
"creating a file {:?} for a download from {}",
destination_path, version.url,
)
})?;
futures::io::copy(&mut decompressed_bytes, &mut file)
.await
.with_context(|| {
format!("extracting {} to {:?}", version.url, destination_path)
})?;
}
AssetKind::Zip => {
extract_zip(&destination_path, response.body_mut())
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
}
}
let mut dir = fs::read_dir(&destination_path).await?;
let first = dir.next().await.context("missing first file")??;
let repo_root = destination_path.join("vscode-eslint");
fs::rename(first.path(), &repo_root).await?;
#[cfg(target_os = "windows")]
{
handle_symlink(
repo_root.join("$shared"),
repo_root.join("client").join("src").join("shared"),
)
.await?;
handle_symlink(
repo_root.join("$shared"),
repo_root.join("server").join("src").join("shared"),
)
.await?;
}
self.node
.run_npm_subcommand(&repo_root, "install", &[])
.await?;
self.node
.run_npm_subcommand(&repo_root, "run-script", &["compile"])
.await?;
}
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let server_path =
Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: eslint_server_binary_arguments(&server_path),
})
}
}
#[cfg(target_os = "windows")]
async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
anyhow::ensure!(
fs::metadata(&src_dir).await.is_ok(),
"Directory {src_dir:?} is not present"
);
if fs::metadata(&dest_dir).await.is_ok() {
fs::remove_file(&dest_dir).await?;
}
fs::create_dir_all(&dest_dir).await?;
let mut entries = fs::read_dir(&src_dir).await?;
while let Some(entry) = entries.try_next().await? {
let entry_path = entry.path();
let entry_name = entry.file_name();
let dest_path = dest_dir.join(&entry_name);
fs::copy(&entry_path, &dest_path).await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use gpui::{AppContext as _, TestAppContext};

View File

@@ -515,29 +515,34 @@ impl LocalLspStore {
let toolchains =
this.update(&mut cx, |this, cx| this.toolchain_store(cx))?;
let workspace_config = Self::workspace_configuration_for_adapter(
adapter.clone(),
fs.as_ref(),
&delegate,
toolchains.clone(),
&mut cx,
)
.await?;
let mut results = Vec::new();
Ok(params
.items
.into_iter()
.map(|item| {
if let Some(section) = &item.section {
workspace_config
.get(section)
.cloned()
.unwrap_or(serde_json::Value::Null)
} else {
workspace_config.clone()
}
})
.collect())
for item in params.items {
let workspace_config =
Self::with_workspace_scope(&delegate, item.scope_uri, || {
Self::workspace_configuration_for_adapter(
adapter.clone(),
fs.as_ref(),
&delegate,
toolchains.clone(),
&mut cx,
)
})
.await?;
let config_value = if let Some(section) = &item.section {
workspace_config
.get(section)
.cloned()
.unwrap_or(serde_json::Value::Null)
} else {
workspace_config
};
results.push(config_value);
}
Ok(results)
}
}
})
@@ -3400,6 +3405,25 @@ impl LocalLspStore {
Ok(Some(initialization_config))
}
async fn with_workspace_scope<F, Fut, T, E>(
delegate: &Arc<dyn LspAdapterDelegate>,
scope_uri: Option<Url>,
f: F,
) -> Result<T, E>
where
F: FnOnce() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
if let Some(uri) = scope_uri {
delegate.set_workspace_scope(Some(uri));
let result = f().await;
delegate.set_workspace_scope(None);
result
} else {
f().await
}
}
async fn workspace_configuration_for_adapter(
adapter: Arc<dyn LspAdapter>,
fs: &dyn Fs,
@@ -10581,6 +10605,7 @@ pub struct LocalLspAdapterDelegate {
http_client: Arc<dyn HttpClient>,
language_registry: Arc<LanguageRegistry>,
load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
workspace_scope: Mutex<Option<lsp::Url>>,
}
impl LocalLspAdapterDelegate {
@@ -10604,6 +10629,7 @@ impl LocalLspAdapterDelegate {
http_client,
language_registry,
load_shell_env_task,
workspace_scope: Mutex::new(None),
})
}
@@ -10646,6 +10672,14 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
self.worktree.abs_path().as_ref()
}
fn subproject_root_path(&self) -> PathBuf {
if let Some(url) = &*self.workspace_scope.lock() {
// get_with_adapters
} else {
self.worktree_root_path().to_path_buf()
}
}
async fn shell_env(&self) -> HashMap<String, String> {
let task = self.load_shell_env_task.clone();
task.await.unwrap_or_default()
@@ -10761,6 +10795,10 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
self.fs.load(&abs_path).await
}
fn set_workspace_scope(&self, scope_uri: Option<lsp::Url>) {
*self.workspace_scope.lock() = scope_uri;
}
}
async fn populate_labels_for_symbols(