Cargo.lock 🔗
@@ -9015,6 +9015,7 @@ dependencies = [
"chrono",
"collections",
"futures 0.3.31",
+ "globset",
"gpui",
"http_client",
"itertools 0.14.0",
Smit Barmase and Piotr Osiewicz created
Closes #9648 #9755
Release Notes:
- Added way to configure ESLint's working directories in settings. For
example:
`{"lsp":{"eslint":{"settings":{"workingDirectories":["./client","./server"]}}}}`
---------
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Cargo.lock | 1
crates/language/src/language.rs | 5
crates/language_extension/src/extension_lsp_adapter.rs | 3
crates/languages/Cargo.toml | 1
crates/languages/src/css.rs | 3
crates/languages/src/eslint.rs | 772 ++++++++++++
crates/languages/src/json.rs | 3
crates/languages/src/lib.rs | 3
crates/languages/src/python.rs | 6
crates/languages/src/tailwind.rs | 3
crates/languages/src/typescript.rs | 240 ---
crates/languages/src/vtsls.rs | 3
crates/languages/src/yaml.rs | 4
crates/project/src/lsp_store.rs | 50
14 files changed, 837 insertions(+), 260 deletions(-)
@@ -9015,6 +9015,7 @@ dependencies = [
"chrono",
"collections",
"futures 0.3.31",
+ "globset",
"gpui",
"http_client",
"itertools 0.14.0",
@@ -36,7 +36,9 @@ use http_client::HttpClient;
pub use language_registry::{
LanguageName, LanguageServerStatusUpdate, LoadedLanguage, ServerHealth,
};
-use lsp::{CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions};
+use lsp::{
+ CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions, Uri,
+};
pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery};
use parking_lot::Mutex;
use regex::Regex;
@@ -406,6 +408,7 @@ pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller {
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
+ _: Option<Uri>,
_cx: &mut AsyncApp,
) -> Result<Value> {
Ok(serde_json::json!({}))
@@ -14,7 +14,7 @@ use language::{
};
use lsp::{
CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName,
- LanguageServerSelector,
+ LanguageServerSelector, Uri,
};
use serde::Serialize;
use serde_json::Value;
@@ -282,6 +282,7 @@ impl LspAdapter for ExtensionLspAdapter {
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
+ _: Option<Uri>,
_cx: &mut AsyncApp,
) -> Result<Value> {
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
@@ -42,6 +42,7 @@ async-trait.workspace = true
chrono.workspace = true
collections.workspace = true
futures.workspace = true
+globset.workspace = true
gpui.workspace = true
http_client.workspace = true
json_schema_store.workspace = true
@@ -2,7 +2,7 @@ use anyhow::Result;
use async_trait::async_trait;
use gpui::AsyncApp;
use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
-use lsp::{LanguageServerBinary, LanguageServerName};
+use lsp::{LanguageServerBinary, LanguageServerName, Uri};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::lsp_store::language_server_settings;
use serde_json::json;
@@ -142,6 +142,7 @@ impl LspAdapter for CssLspAdapter {
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
+ _: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<serde_json::Value> {
let mut default_config = json!({
@@ -0,0 +1,772 @@
+use anyhow::{Context as _, Result};
+use async_trait::async_trait;
+use gpui::AsyncApp;
+use http_client::{
+ github::{AssetKind, GitHubLspBinaryVersion, build_asset_url},
+ github_download::download_server_binary,
+};
+use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
+use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
+use node_runtime::NodeRuntime;
+use project::lsp_store::language_server_settings_for;
+use serde::{Deserialize, Serialize};
+use serde_json::{Value, json};
+use settings::SettingsLocation;
+use smol::{fs, stream::StreamExt};
+use std::{
+ ffi::OsString,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+use util::merge_json_value_into;
+use util::{fs::remove_matching, rel_path::RelPath};
+
+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",
+ ];
+
+ 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))
+ }
+}
+
+impl LspInstaller for EsLintLspAdapter {
+ type BinaryVersion = GitHubLspBinaryVersion;
+
+ async fn fetch_latest_server_version(
+ &self,
+ _delegate: &dyn LspAdapterDelegate,
+ _: bool,
+ _: &mut AsyncApp,
+ ) -> Result<GitHubLspBinaryVersion> {
+ let url = build_asset_url(
+ "zed-industries/vscode-eslint",
+ Self::CURRENT_VERSION_TAG_NAME,
+ Self::GITHUB_ASSET_KIND,
+ )?;
+
+ Ok(GitHubLspBinaryVersion {
+ name: Self::CURRENT_VERSION.into(),
+ digest: None,
+ url,
+ })
+ }
+
+ async fn fetch_server_binary(
+ &self,
+ version: GitHubLspBinaryVersion,
+ container_dir: PathBuf,
+ delegate: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ 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, |_| true).await;
+
+ download_server_binary(
+ &*delegate.http_client(),
+ &version.url,
+ None,
+ &destination_path,
+ Self::GITHUB_ASSET_KIND,
+ )
+ .await?;
+
+ 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),
+ })
+ }
+}
+
+#[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>,
+ delegate: &Arc<dyn LspAdapterDelegate>,
+ _: Option<Toolchain>,
+ requested_uri: Option<Uri>,
+ cx: &mut AsyncApp,
+ ) -> Result<Value> {
+ let worktree_root = delegate.worktree_root_path();
+ let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
+ .iter()
+ .any(|file| worktree_root.join(file).is_file());
+
+ let mut default_workspace_configuration = json!({
+ "validate": "on",
+ "rulesCustomizations": [],
+ "run": "onType",
+ "nodePath": null,
+ "workingDirectory": {
+ "mode": "auto"
+ },
+ "workspaceFolder": {
+ "uri": worktree_root,
+ "name": worktree_root.file_name()
+ .unwrap_or(worktree_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 file_path = requested_uri
+ .as_ref()
+ .and_then(|uri| {
+ (uri.scheme() == "file")
+ .then(|| uri.to_file_path().ok())
+ .flatten()
+ })
+ .and_then(|abs_path| {
+ abs_path
+ .strip_prefix(&worktree_root)
+ .ok()
+ .map(ToOwned::to_owned)
+ });
+ let file_path = file_path
+ .and_then(|p| RelPath::unix(&p).ok().map(ToOwned::to_owned))
+ .unwrap_or_else(|| RelPath::empty().to_owned());
+ let override_options = cx.update(|cx| {
+ language_server_settings_for(
+ SettingsLocation {
+ worktree_id: delegate.worktree_id(),
+ path: &file_path,
+ },
+ &Self::SERVER_NAME,
+ cx,
+ )
+ .and_then(|s| s.settings.clone())
+ })?;
+
+ if let Some(override_options) = override_options {
+ let working_directories = override_options.get("workingDirectories").and_then(|wd| {
+ serde_json::from_value::<WorkingDirectories>(wd.clone())
+ .ok()
+ .and_then(|wd| wd.0)
+ });
+
+ merge_json_value_into(override_options, &mut default_workspace_configuration);
+
+ let working_directory = working_directories
+ .zip(requested_uri)
+ .and_then(|(wd, uri)| {
+ determine_working_directory(uri, wd, worktree_root.to_owned())
+ });
+
+ if let Some(working_directory) = working_directory
+ && let Some(wd) = default_workspace_configuration.get_mut("workingDirectory")
+ {
+ *wd = serde_json::to_value(working_directory)?;
+ }
+ }
+
+ Ok(json!({
+ "": default_workspace_configuration
+ }))
+ }
+
+ fn name(&self) -> LanguageServerName {
+ Self::SERVER_NAME
+ }
+}
+
+/// On Windows, converts Unix-style separators (/) to Windows-style (\).
+/// On Unix, returns the path unchanged
+fn normalize_path_separators(path: &str) -> String {
+ #[cfg(windows)]
+ {
+ path.replace('/', "\\")
+ }
+ #[cfg(not(windows))]
+ {
+ path.to_string()
+ }
+}
+
+fn determine_working_directory(
+ uri: Uri,
+ working_directories: Vec<WorkingDirectory>,
+ workspace_folder_path: PathBuf,
+) -> Option<ResultWorkingDirectory> {
+ let mut working_directory = None;
+
+ for item in working_directories {
+ let mut directory: Option<String> = None;
+ let mut pattern: Option<String> = None;
+ let mut no_cwd = false;
+ match item {
+ WorkingDirectory::String(contents) => {
+ directory = Some(normalize_path_separators(&contents));
+ }
+ WorkingDirectory::LegacyDirectoryItem(legacy_directory_item) => {
+ directory = Some(normalize_path_separators(&legacy_directory_item.directory));
+ no_cwd = !legacy_directory_item.change_process_cwd;
+ }
+ WorkingDirectory::DirectoryItem(directory_item) => {
+ directory = Some(normalize_path_separators(&directory_item.directory));
+ if let Some(not_cwd) = directory_item.not_cwd {
+ no_cwd = not_cwd;
+ }
+ }
+ WorkingDirectory::PatternItem(pattern_item) => {
+ pattern = Some(normalize_path_separators(&pattern_item.pattern));
+ if let Some(not_cwd) = pattern_item.not_cwd {
+ no_cwd = not_cwd;
+ }
+ }
+ WorkingDirectory::ModeItem(mode_item) => {
+ working_directory = Some(ResultWorkingDirectory::ModeItem(mode_item));
+ continue;
+ }
+ }
+
+ let mut item_value: Option<String> = None;
+ if directory.is_some() || pattern.is_some() {
+ let file_path: Option<PathBuf> = (uri.scheme() == "file")
+ .then(|| uri.to_file_path().ok())
+ .flatten();
+ if let Some(file_path) = file_path {
+ if let Some(mut directory) = directory {
+ if Path::new(&directory).is_relative() {
+ directory = workspace_folder_path
+ .join(directory)
+ .to_string_lossy()
+ .to_string();
+ }
+ if !directory.ends_with(std::path::MAIN_SEPARATOR) {
+ directory.push(std::path::MAIN_SEPARATOR);
+ }
+ if file_path.starts_with(&directory) {
+ item_value = Some(directory);
+ }
+ } else if let Some(mut pattern) = pattern
+ && !pattern.is_empty()
+ {
+ if Path::new(&pattern).is_relative() {
+ pattern = workspace_folder_path
+ .join(pattern)
+ .to_string_lossy()
+ .to_string();
+ }
+ if !pattern.ends_with(std::path::MAIN_SEPARATOR) {
+ pattern.push(std::path::MAIN_SEPARATOR);
+ }
+ if let Some(matched) = match_glob_pattern(&pattern, &file_path) {
+ item_value = Some(matched);
+ }
+ }
+ }
+ }
+ if let Some(item_value) = item_value {
+ if working_directory
+ .as_ref()
+ .is_none_or(|wd| matches!(wd, ResultWorkingDirectory::ModeItem(_)))
+ {
+ working_directory = Some(ResultWorkingDirectory::DirectoryItem(DirectoryItem {
+ directory: item_value,
+ not_cwd: Some(no_cwd),
+ }));
+ } else if let Some(ResultWorkingDirectory::DirectoryItem(item)) = &mut working_directory
+ && item.directory.len() < item_value.len()
+ {
+ item.directory = item_value;
+ item.not_cwd = Some(no_cwd);
+ }
+ }
+ }
+
+ working_directory
+}
+
+fn match_glob_pattern(pattern: &str, file_path: &Path) -> Option<String> {
+ use globset::GlobBuilder;
+
+ let glob = GlobBuilder::new(pattern)
+ .literal_separator(true)
+ .build()
+ .ok()?
+ .compile_matcher();
+
+ let mut current = file_path.to_path_buf();
+ let mut matched: Option<String> = None;
+
+ while let Some(parent) = current.parent() {
+ let mut prefix = parent.to_string_lossy().to_string();
+ if !prefix.ends_with(std::path::MAIN_SEPARATOR) {
+ prefix.push(std::path::MAIN_SEPARATOR);
+ }
+ if glob.is_match(&prefix) {
+ matched = Some(prefix);
+ }
+ current = parent.to_path_buf();
+ }
+
+ matched
+}
+
+#[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(())
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct LegacyDirectoryItem {
+ directory: String,
+ #[serde(rename = "changeProcessCWD")]
+ change_process_cwd: bool,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct DirectoryItem {
+ directory: String,
+ #[serde(rename = "!cwd")]
+ not_cwd: Option<bool>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct PatternItem {
+ pattern: String,
+ #[serde(rename = "!cwd")]
+ not_cwd: Option<bool>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct ModeItem {
+ mode: ModeEnum,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "lowercase")]
+enum ModeEnum {
+ Auto,
+ Location,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(untagged)]
+enum WorkingDirectory {
+ String(String),
+ LegacyDirectoryItem(LegacyDirectoryItem),
+ DirectoryItem(DirectoryItem),
+ PatternItem(PatternItem),
+ ModeItem(ModeItem),
+}
+#[derive(Serialize, Deserialize)]
+struct WorkingDirectories(Option<Vec<WorkingDirectory>>);
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(untagged)]
+enum ResultWorkingDirectory {
+ ModeItem(ModeItem),
+ DirectoryItem(DirectoryItem),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ mod glob_patterns {
+ use super::*;
+
+ #[test]
+ fn test_match_glob_pattern() {
+ let pattern = unix_path_to_platform("/test/*/");
+ let file_path = PathBuf::from(unix_path_to_platform("/test/foo/bar/file.txt"));
+ let matched = match_glob_pattern(&pattern, &file_path);
+ assert_eq!(matched, Some(unix_path_to_platform("/test/foo/")));
+ }
+
+ #[test]
+ fn test_match_glob_pattern_globstar() {
+ let pattern = unix_path_to_platform("/workspace/**/src/");
+ let file_path = PathBuf::from(unix_path_to_platform(
+ "/workspace/packages/core/src/index.ts",
+ ));
+ let matched = match_glob_pattern(&pattern, &file_path);
+ assert_eq!(
+ matched,
+ Some(unix_path_to_platform("/workspace/packages/core/src/"))
+ );
+ }
+
+ #[test]
+ fn test_match_glob_pattern_no_match() {
+ let pattern = unix_path_to_platform("/other/*/");
+ let file_path = PathBuf::from(unix_path_to_platform("/test/foo/bar/file.txt"));
+ let matched = match_glob_pattern(&pattern, &file_path);
+ assert_eq!(matched, None);
+ }
+ }
+
+ mod unix_style_paths {
+ use super::*;
+
+ #[test]
+ fn test_working_directory_string() {
+ let uri = make_uri("/workspace/packages/foo/src/file.ts");
+ let working_directories = vec![WorkingDirectory::String("packages/foo".to_string())];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/packages/foo/"),
+ false,
+ );
+ }
+
+ #[test]
+ fn test_working_directory_absolute_path() {
+ let uri = make_uri("/workspace/packages/foo/src/file.ts");
+ let working_directories = vec![WorkingDirectory::String(unix_path_to_platform(
+ "/workspace/packages/foo",
+ ))];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/packages/foo/"),
+ false,
+ );
+ }
+
+ #[test]
+ fn test_working_directory_directory_item() {
+ let uri = make_uri("/workspace/packages/foo/src/file.ts");
+ let working_directories = vec![WorkingDirectory::DirectoryItem(DirectoryItem {
+ directory: "packages/foo".to_string(),
+ not_cwd: Some(true),
+ })];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/packages/foo/"),
+ true,
+ );
+ }
+
+ #[test]
+ fn test_working_directory_legacy_item() {
+ let uri = make_uri("/workspace/packages/foo/src/file.ts");
+ let working_directories =
+ vec![WorkingDirectory::LegacyDirectoryItem(LegacyDirectoryItem {
+ directory: "packages/foo".to_string(),
+ change_process_cwd: false,
+ })];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/packages/foo/"),
+ true,
+ );
+ }
+
+ #[test]
+ fn test_working_directory_pattern_item() {
+ let uri = make_uri("/workspace/packages/foo/src/file.ts");
+ let working_directories = vec![WorkingDirectory::PatternItem(PatternItem {
+ pattern: "packages/*/".to_string(),
+ not_cwd: Some(false),
+ })];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/packages/foo/"),
+ false,
+ );
+ }
+
+ #[test]
+ fn test_working_directory_multiple_patterns() {
+ let uri = make_uri("/workspace/apps/web/src/file.ts");
+ let working_directories = vec![
+ WorkingDirectory::PatternItem(PatternItem {
+ pattern: "packages/*/".to_string(),
+ not_cwd: None,
+ }),
+ WorkingDirectory::PatternItem(PatternItem {
+ pattern: "apps/*/".to_string(),
+ not_cwd: None,
+ }),
+ ];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/apps/web/"),
+ false,
+ );
+ }
+ }
+
+ #[cfg(windows)]
+ mod windows_style_paths {
+ use super::*;
+
+ #[test]
+ fn test_working_directory_string() {
+ let uri = make_uri("/workspace/packages/foo/src/file.ts");
+ let working_directories = vec![WorkingDirectory::String("packages\\foo".to_string())];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/packages/foo/"),
+ false,
+ );
+ }
+
+ #[test]
+ fn test_working_directory_absolute_path() {
+ let uri = make_uri("/workspace/packages/foo/src/file.ts");
+ let working_directories = vec![WorkingDirectory::String(
+ unix_path_to_platform("/workspace/packages/foo").replace('/', "\\"),
+ )];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/packages/foo/"),
+ false,
+ );
+ }
+
+ #[test]
+ fn test_working_directory_directory_item() {
+ let uri = make_uri("/workspace/packages/foo/src/file.ts");
+ let working_directories = vec![WorkingDirectory::DirectoryItem(DirectoryItem {
+ directory: "packages\\foo".to_string(),
+ not_cwd: Some(true),
+ })];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/packages/foo/"),
+ true,
+ );
+ }
+
+ #[test]
+ fn test_working_directory_legacy_item() {
+ let uri = make_uri("/workspace/packages/foo/src/file.ts");
+ let working_directories =
+ vec![WorkingDirectory::LegacyDirectoryItem(LegacyDirectoryItem {
+ directory: "packages\\foo".to_string(),
+ change_process_cwd: false,
+ })];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/packages/foo/"),
+ true,
+ );
+ }
+
+ #[test]
+ fn test_working_directory_pattern_item() {
+ let uri = make_uri("/workspace/packages/foo/src/file.ts");
+ let working_directories = vec![WorkingDirectory::PatternItem(PatternItem {
+ pattern: "packages\\*\\".to_string(),
+ not_cwd: Some(false),
+ })];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/packages/foo/"),
+ false,
+ );
+ }
+
+ #[test]
+ fn test_working_directory_multiple_patterns() {
+ let uri = make_uri("/workspace/apps/web/src/file.ts");
+ let working_directories = vec![
+ WorkingDirectory::PatternItem(PatternItem {
+ pattern: "packages\\*\\".to_string(),
+ not_cwd: None,
+ }),
+ WorkingDirectory::PatternItem(PatternItem {
+ pattern: "apps\\*\\".to_string(),
+ not_cwd: None,
+ }),
+ ];
+ let workspace_folder = PathBuf::from(unix_path_to_platform("/workspace"));
+
+ let result = determine_working_directory(uri, working_directories, workspace_folder);
+ assert_directory_result(
+ result,
+ &unix_path_to_platform("/workspace/apps/web/"),
+ false,
+ );
+ }
+ }
+
+ /// Converts a Unix-style path to a platform-specific path.
+ /// On Windows, converts "/workspace/foo/bar" to "C:\workspace\foo\bar"
+ /// On Unix, returns the path unchanged.
+ fn unix_path_to_platform(path: &str) -> String {
+ #[cfg(windows)]
+ {
+ if path.starts_with('/') {
+ format!("C:{}", path.replace('/', "\\"))
+ } else {
+ path.replace('/', "\\")
+ }
+ }
+ #[cfg(not(windows))]
+ {
+ path.to_string()
+ }
+ }
+
+ fn make_uri(path: &str) -> Uri {
+ let platform_path = unix_path_to_platform(path);
+ Uri::from_file_path(&platform_path).unwrap()
+ }
+
+ fn assert_directory_result(
+ result: Option<ResultWorkingDirectory>,
+ expected_directory: &str,
+ expected_not_cwd: bool,
+ ) {
+ match result {
+ Some(ResultWorkingDirectory::DirectoryItem(item)) => {
+ assert_eq!(item.directory, expected_directory);
+ assert_eq!(item.not_cwd, Some(expected_not_cwd));
+ }
+ other => panic!("Expected DirectoryItem, got {:?}", other),
+ }
+ }
+}
@@ -10,7 +10,7 @@ use language::{
ContextProvider, LanguageName, LocalFile as _, LspAdapter, LspAdapterDelegate, LspInstaller,
Toolchain,
};
-use lsp::{LanguageServerBinary, LanguageServerName};
+use lsp::{LanguageServerBinary, LanguageServerName, Uri};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::lsp_store::language_server_settings;
use serde_json::{Value, json};
@@ -251,6 +251,7 @@ impl LspAdapter for JsonLspAdapter {
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
+ _: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<Value> {
let mut config = cx.update(|cx| {
@@ -21,6 +21,7 @@ mod bash;
mod c;
mod cpp;
mod css;
+mod eslint;
mod go;
mod json;
mod package_json;
@@ -84,7 +85,7 @@ pub fn init(languages: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, node: NodeRuntime
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(JsonTaskProvider);
@@ -10,8 +10,8 @@ use language::{ContextLocation, LanguageToolchainStore, LspInstaller};
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata};
-use lsp::LanguageServerBinary;
use lsp::LanguageServerName;
+use lsp::{LanguageServerBinary, Uri};
use node_runtime::{NodeRuntime, VersionStrategy};
use pet_core::Configuration;
use pet_core::os_environment::Environment;
@@ -206,6 +206,7 @@ impl LspAdapter for TyLspAdapter {
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
toolchain: Option<Toolchain>,
+ _: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<Value> {
let mut ret = cx
@@ -517,6 +518,7 @@ impl LspAdapter for PyrightLspAdapter {
self: Arc<Self>,
adapter: &Arc<dyn LspAdapterDelegate>,
toolchain: Option<Toolchain>,
+ _: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<Value> {
cx.update(move |cx| {
@@ -1593,6 +1595,7 @@ impl LspAdapter for PyLspAdapter {
self: Arc<Self>,
adapter: &Arc<dyn LspAdapterDelegate>,
toolchain: Option<Toolchain>,
+ _: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<Value> {
cx.update(move |cx| {
@@ -1884,6 +1887,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
self: Arc<Self>,
adapter: &Arc<dyn LspAdapterDelegate>,
toolchain: Option<Toolchain>,
+ _: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<Value> {
cx.update(move |cx| {
@@ -3,7 +3,7 @@ use async_trait::async_trait;
use collections::HashMap;
use gpui::AsyncApp;
use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
-use lsp::{LanguageServerBinary, LanguageServerName};
+use lsp::{LanguageServerBinary, LanguageServerName, Uri};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::lsp_store::language_server_settings;
use serde_json::{Value, json};
@@ -154,6 +154,7 @@ impl LspAdapter for TailwindLspAdapter {
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
+ _: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<Value> {
let mut tailwind_user_settings = cx.update(|cx| {
@@ -4,18 +4,16 @@ use chrono::{DateTime, Local};
use collections::HashMap;
use futures::future::join_all;
use gpui::{App, AppContext, AsyncApp, Task};
-use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
-use http_client::github_download::download_server_binary;
use itertools::Itertools as _;
use language::{
ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter,
LspAdapterDelegate, LspInstaller, Toolchain,
};
-use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
+use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
-use smol::{fs, lock::RwLock, stream::StreamExt};
+use smol::lock::RwLock;
use std::{
borrow::Cow,
ffi::OsString,
@@ -23,8 +21,8 @@ use std::{
sync::{Arc, LazyLock},
};
use task::{TaskTemplate, TaskTemplates, VariableName};
-use util::{ResultExt, fs::remove_matching, maybe};
-use util::{merge_json_value_into, rel_path::RelPath};
+use util::rel_path::RelPath;
+use util::{ResultExt, maybe};
use crate::{PackageJson, PackageJsonData};
@@ -589,14 +587,6 @@ fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
-fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
- vec![
- "--max-old-space-size=8192".into(),
- server_path.into(),
- "--stdio".into(),
- ]
-}
-
fn replace_test_name_parameters(test_name: &str) -> String {
static PATTERN: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(\$([A-Za-z0-9_\.]+|[\#])|%[psdifjo#\$%])").unwrap());
@@ -811,9 +801,9 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
-
delegate: &Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
+ _: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<Value> {
let override_options = cx.update(|cx| {
@@ -866,226 +856,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))
- }
-}
-
-impl LspInstaller for EsLintLspAdapter {
- type BinaryVersion = GitHubLspBinaryVersion;
-
- async fn fetch_latest_server_version(
- &self,
- _delegate: &dyn LspAdapterDelegate,
- _: bool,
- _: &mut AsyncApp,
- ) -> Result<GitHubLspBinaryVersion> {
- let url = build_asset_url(
- "zed-industries/vscode-eslint",
- Self::CURRENT_VERSION_TAG_NAME,
- Self::GITHUB_ASSET_KIND,
- )?;
-
- Ok(GitHubLspBinaryVersion {
- name: Self::CURRENT_VERSION.into(),
- digest: None,
- url,
- })
- }
-
- async fn fetch_server_binary(
- &self,
- version: GitHubLspBinaryVersion,
- container_dir: PathBuf,
- delegate: &dyn LspAdapterDelegate,
- ) -> Result<LanguageServerBinary> {
- 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, |_| true).await;
-
- download_server_binary(
- &*delegate.http_client(),
- &version.url,
- None,
- &destination_path,
- Self::GITHUB_ASSET_KIND,
- )
- .await?;
-
- 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),
- })
- }
-}
-
-#[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>,
- delegate: &Arc<dyn LspAdapterDelegate>,
- _: Option<Toolchain>,
- 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
- }
-}
-
-#[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 std::path::Path;
@@ -3,7 +3,7 @@ use async_trait::async_trait;
use collections::HashMap;
use gpui::AsyncApp;
use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
-use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
+use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use regex::Regex;
@@ -228,6 +228,7 @@ impl LspAdapter for VtslsLspAdapter {
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
+ _: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<Value> {
let tsdk_path = self.tsdk_path(delegate).await;
@@ -4,7 +4,7 @@ use gpui::AsyncApp;
use language::{
LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain, language_settings::AllLanguageSettings,
};
-use lsp::{LanguageServerBinary, LanguageServerName};
+use lsp::{LanguageServerBinary, LanguageServerName, Uri};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::lsp_store::language_server_settings;
use serde_json::Value;
@@ -132,9 +132,9 @@ impl LspAdapter for YamlLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
-
delegate: &Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
+ _: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<Value> {
let location = SettingsLocation {
@@ -436,6 +436,7 @@ impl LocalLspStore {
adapter.adapter.clone(),
&delegate,
toolchain,
+ None,
cx,
)
.await?;
@@ -720,25 +721,42 @@ impl LocalLspStore {
)
})?
.context("Expected the LSP store to be in a local mode")?;
- let workspace_config = Self::workspace_configuration_for_adapter(
- adapter.clone(),
- &delegate,
- toolchain_for_id,
- &mut cx,
- )
- .await?;
+
+ let mut scope_uri_to_workspace_config = BTreeMap::new();
+ for item in ¶ms.items {
+ let scope_uri = item.scope_uri.clone();
+ let std::collections::btree_map::Entry::Vacant(new_scope_uri) =
+ scope_uri_to_workspace_config.entry(scope_uri.clone())
+ else {
+ // We've already queried workspace configuration of this URI.
+ continue;
+ };
+ let workspace_config = Self::workspace_configuration_for_adapter(
+ adapter.clone(),
+ &delegate,
+ toolchain_for_id.clone(),
+ scope_uri,
+ &mut cx,
+ )
+ .await?;
+ new_scope_uri.insert(workspace_config);
+ }
Ok(params
.items
.into_iter()
- .map(|item| {
+ .filter_map(|item| {
+ let workspace_config =
+ scope_uri_to_workspace_config.get(&item.scope_uri)?;
if let Some(section) = &item.section {
- workspace_config
- .get(section)
- .cloned()
- .unwrap_or(serde_json::Value::Null)
+ Some(
+ workspace_config
+ .get(section)
+ .cloned()
+ .unwrap_or(serde_json::Value::Null),
+ )
} else {
- workspace_config.clone()
+ Some(workspace_config.clone())
}
})
.collect())
@@ -3527,11 +3545,12 @@ impl LocalLspStore {
adapter: Arc<dyn LspAdapter>,
delegate: &Arc<dyn LspAdapterDelegate>,
toolchain: Option<Toolchain>,
+ requested_uri: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<serde_json::Value> {
let mut workspace_config = adapter
.clone()
- .workspace_configuration(delegate, toolchain, cx)
+ .workspace_configuration(delegate, toolchain, requested_uri, cx)
.await?;
for other_adapter in delegate.registered_lsp_adapters() {
@@ -7963,6 +7982,7 @@ impl LspStore {
adapter.adapter.clone(),
&delegate,
toolchain,
+ None,
cx,
)
.await
@@ -13619,7 +13639,7 @@ pub fn language_server_settings<'a>(
)
}
-pub(crate) fn language_server_settings_for<'a>(
+pub fn language_server_settings_for<'a>(
location: SettingsLocation<'a>,
language: &LanguageServerName,
cx: &'a App,