diff --git a/Cargo.lock b/Cargo.lock index 9dc5c45f34..6c9bd59c6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9015,6 +9015,7 @@ dependencies = [ "chrono", "collections", "futures 0.3.31", + "globset", "gpui", "http_client", "itertools 0.14.0", diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index c0a2807673..3cd619c0d2 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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, _: &Arc, _: Option, + _: Option, _cx: &mut AsyncApp, ) -> Result { Ok(serde_json::json!({})) diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index 566e6f3b36..920bf07f5d 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -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, delegate: &Arc, _: Option, + _: Option, _cx: &mut AsyncApp, ) -> Result { let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 635cb11ba5..235f7a94c7 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -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 diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index 8531cd4478..6a925586a6 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -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, delegate: &Arc, _: Option, + _: Option, cx: &mut AsyncApp, ) -> Result { let mut default_config = json!({ diff --git a/crates/languages/src/eslint.rs b/crates/languages/src/eslint.rs new file mode 100644 index 0000000000..4f18149265 --- /dev/null +++ b/crates/languages/src/eslint.rs @@ -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 { + 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 { + 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 { + 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 { + 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> { + Some(vec![ + CodeActionKind::QUICKFIX, + CodeActionKind::new("source.fixAll.eslint"), + ]) + } + + async fn workspace_configuration( + self: Arc, + delegate: &Arc, + _: Option, + requested_uri: Option, + cx: &mut AsyncApp, + ) -> Result { + 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::(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, + workspace_folder_path: PathBuf, +) -> Option { + let mut working_directory = None; + + for item in working_directories { + let mut directory: Option = None; + let mut pattern: Option = 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 = None; + if directory.is_some() || pattern.is_some() { + let file_path: Option = (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 { + 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 = 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, +} + +#[derive(Serialize, Deserialize, Debug)] +struct PatternItem { + pattern: String, + #[serde(rename = "!cwd")] + not_cwd: Option, +} + +#[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>); + +#[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, + 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), + } + } +} diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index d75d994fee..f695512c1a 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -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, delegate: &Arc, _: Option, + _: Option, cx: &mut AsyncApp, ) -> Result { let mut config = cx.update(|cx| { diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 0fe5a5b774..d27b5ece0d 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -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, fs: Arc, 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); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index a451afa6f1..56512ee8af 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -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, delegate: &Arc, toolchain: Option, + _: Option, cx: &mut AsyncApp, ) -> Result { let mut ret = cx @@ -517,6 +518,7 @@ impl LspAdapter for PyrightLspAdapter { self: Arc, adapter: &Arc, toolchain: Option, + _: Option, cx: &mut AsyncApp, ) -> Result { cx.update(move |cx| { @@ -1593,6 +1595,7 @@ impl LspAdapter for PyLspAdapter { self: Arc, adapter: &Arc, toolchain: Option, + _: Option, cx: &mut AsyncApp, ) -> Result { cx.update(move |cx| { @@ -1884,6 +1887,7 @@ impl LspAdapter for BasedPyrightLspAdapter { self: Arc, adapter: &Arc, toolchain: Option, + _: Option, cx: &mut AsyncApp, ) -> Result { cx.update(move |cx| { diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index c7baa9734b..1696924644 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -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, delegate: &Arc, _: Option, + _: Option, cx: &mut AsyncApp, ) -> Result { let mut tailwind_user_settings = cx.update(|cx| { diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 00e4113ba1..eeca5ebb6f 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -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 { vec![server_path.into(), "--stdio".into()] } -fn eslint_server_binary_arguments(server_path: &Path) -> Vec { - vec![ - "--max-old-space-size=8192".into(), - server_path.into(), - "--stdio".into(), - ] -} - fn replace_test_name_parameters(test_name: &str) -> String { static PATTERN: LazyLock = 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, - delegate: &Arc, _: Option, + _: Option, cx: &mut AsyncApp, ) -> Result { 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 { - 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 { - 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 { - 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> { - Some(vec![ - CodeActionKind::QUICKFIX, - CodeActionKind::new("source.fixAll.eslint"), - ]) - } - - async fn workspace_configuration( - self: Arc, - delegate: &Arc, - _: Option, - cx: &mut AsyncApp, - ) -> Result { - 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; diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index fa1f47ff79..a057f40336 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -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, delegate: &Arc, _: Option, + _: Option, cx: &mut AsyncApp, ) -> Result { let tsdk_path = self.tsdk_path(delegate).await; diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index c79f029491..57f254a68f 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -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, - delegate: &Arc, _: Option, + _: Option, cx: &mut AsyncApp, ) -> Result { let location = SettingsLocation { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index a69d255369..a4d92fcf1a 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -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, delegate: &Arc, toolchain: Option, + requested_uri: Option, cx: &mut AsyncApp, ) -> Result { 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,