1147 lines · 39.6 KB · Rust
RawDownload
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, read_package_installed_version};use project::Fs;use project::lsp_store::language_server_settings_for;use semver::Version;use serde::{Deserialize, Serialize};use serde_json::{Value, json};use settings::SettingsLocation;use smol::{fs, stream::StreamExt};use std::{    ffi::OsString,    future::Future,    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,    fs: Arc<dyn Fs>,} impl EsLintLspAdapter {    const CURRENT_VERSION: &'static str = "3.0.24";    const CURRENT_VERSION_TAG_NAME: &'static str = "release/3.0.24";     #[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_V8_21: &'static [&'static str] = &["eslint.config.js"];    const FLAT_CONFIG_FILE_NAMES_V8_57: &'static [&'static str] =        &["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"];    const FLAT_CONFIG_FILE_NAMES_V10: &'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",        ".eslintrc.js",        ".eslintrc.cjs",        ".eslintrc.yaml",        ".eslintrc.yml",        ".eslintrc.json",    ];     pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {        EsLintLspAdapter { node, fs }    }     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: &Arc<dyn LspAdapterDelegate>,        _: bool,        _: &mut AsyncApp,    ) -> Result<GitHubLspBinaryVersion> {        let url = build_asset_url(            "microsoft/vscode-eslint",            Self::CURRENT_VERSION_TAG_NAME,            Self::GITHUB_ASSET_KIND,        )?;         Ok(GitHubLspBinaryVersion {            name: Self::CURRENT_VERSION.into(),            digest: None,            url,        })    }     fn fetch_server_binary(        &self,        version: GitHubLspBinaryVersion,        container_dir: PathBuf,        delegate: &Arc<dyn LspAdapterDelegate>,    ) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {        let delegate = delegate.clone();        let node = self.node.clone();         async move {            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?;                }                 node.run_npm_subcommand(Some(&repo_root), "install", &[])                    .await?;                 node.run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])                    .await?;            }             Ok(LanguageServerBinary {                path: 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);        fs::metadata(&server_path).await.ok()?;        Some(LanguageServerBinary {            path: self.node.binary_path().await.ok()?,            env: None,            arguments: eslint_server_binary_arguments(&server_path),        })    }} #[derive(Debug, Clone, Copy, PartialEq, Eq)]enum EslintConfigKind {    Flat,    Legacy,} #[derive(Debug, Default, Clone, PartialEq, Eq)]struct EslintSettingsOverrides {    use_flat_config: Option<bool>,    experimental_use_flat_config: Option<bool>,} impl EslintSettingsOverrides {    fn apply_to(self, workspace_configuration: &mut Value) {        if let Some(use_flat_config) = self.use_flat_config            && let Some(workspace_configuration) = workspace_configuration.as_object_mut()        {            workspace_configuration.insert("useFlatConfig".to_string(), json!(use_flat_config));        }         if let Some(experimental_use_flat_config) = self.experimental_use_flat_config            && let Some(workspace_configuration) = workspace_configuration.as_object_mut()        {            let experimental = workspace_configuration                .entry("experimental")                .or_insert_with(|| json!({}));            if let Some(experimental) = experimental.as_object_mut() {                experimental.insert(                    "useFlatConfig".to_string(),                    json!(experimental_use_flat_config),                );            }        }    }} #[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 requested_file_path = requested_uri            .as_ref()            .filter(|uri| uri.scheme() == "file")            .and_then(|uri| uri.to_file_path().ok())            .filter(|path| path.starts_with(worktree_root));        let eslint_version = find_eslint_version(            delegate.as_ref(),            worktree_root,            requested_file_path.as_deref(),        )        .await?;        let config_kind = find_eslint_config_kind(            worktree_root,            requested_file_path.as_deref(),            eslint_version.as_ref(),            self.fs.as_ref(),        )        .await;        let eslint_settings_overrides =            eslint_settings_overrides_for(eslint_version.as_ref(), config_kind);         let mut default_workspace_configuration = json!({            "validate": "on",            "rulesCustomizations": [],            "run": "onType",            "nodePath": null,            "workingDirectory": {                "mode": "auto"            },            "workspaceFolder": {                "uri": Uri::from_file_path(worktree_root)                    .map(|uri| uri.as_str().to_owned())                    .unwrap_or_default(),                "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                }            }        });        eslint_settings_overrides.apply_to(&mut default_workspace_configuration);         let file_path = requested_file_path            .as_ref()            .and_then(|abs_path| abs_path.strip_prefix(worktree_root).ok())            .and_then(|p| RelPath::from_unix_str(&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    }} fn ancestor_directories<'a>(    worktree_root: &'a Path,    requested_file: Option<&'a Path>,) -> impl Iterator<Item = &'a Path> + 'a {    let start = requested_file        .filter(|file| file.starts_with(worktree_root))        .and_then(Path::parent)        .unwrap_or(worktree_root);     start        .ancestors()        .take_while(move |dir| dir.starts_with(worktree_root))} fn flat_config_file_names(version: Option<&Version>) -> &'static [&'static str] {    match version {        Some(version) if version.major >= 10 => EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V10,        Some(version) if version.major == 9 => EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_57,        Some(version) if version.major == 8 && version.minor >= 57 => {            EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_57        }        Some(version) if version.major == 8 && version.minor >= 21 => {            EsLintLspAdapter::FLAT_CONFIG_FILE_NAMES_V8_21        }        _ => &[],    }} async fn find_eslint_config_kind(    worktree_root: &Path,    requested_file: Option<&Path>,    version: Option<&Version>,    fs: &dyn Fs,) -> Option<EslintConfigKind> {    let flat_config_file_names = flat_config_file_names(version);     for directory in ancestor_directories(worktree_root, requested_file) {        for file_name in flat_config_file_names {            if fs.is_file(&directory.join(file_name)).await {                return Some(EslintConfigKind::Flat);            }        }         for file_name in EsLintLspAdapter::LEGACY_CONFIG_FILE_NAMES {            if fs.is_file(&directory.join(file_name)).await {                return Some(EslintConfigKind::Legacy);            }        }    }     None} fn eslint_settings_overrides_for(    version: Option<&Version>,    config_kind: Option<EslintConfigKind>,) -> EslintSettingsOverrides {    // vscode-eslint 3.x already discovers config files and chooses a working    // directory from the active file on its own. Zed only overrides settings    // for the two cases where leaving everything unset is known to be wrong:    //    // - ESLint 8.21-8.56 flat config still needs experimental.useFlatConfig.    // - ESLint 9.x legacy config needs useFlatConfig = false.    //    // All other cases should defer to the server's own defaults and discovery.    let Some(version) = version else {        return EslintSettingsOverrides::default();    };     match config_kind {        Some(EslintConfigKind::Flat) if version.major == 8 && (21..57).contains(&version.minor) => {            EslintSettingsOverrides {                use_flat_config: None,                experimental_use_flat_config: Some(true),            }        }        Some(EslintConfigKind::Legacy) if version.major == 9 => EslintSettingsOverrides {            use_flat_config: Some(false),            experimental_use_flat_config: None,        },        _ => EslintSettingsOverrides::default(),    }} async fn find_eslint_version(    delegate: &dyn LspAdapterDelegate,    worktree_root: &Path,    requested_file: Option<&Path>,) -> Result<Option<Version>> {    for directory in ancestor_directories(worktree_root, requested_file) {        if let Some(version) =            read_package_installed_version(directory.join("node_modules"), "eslint").await?        {            return Ok(Some(version));        }    }     Ok(delegate        .npm_package_installed_version("eslint")        .await?        .map(|(_, version)| version))} /// On Windows, converts Unix-style separators (/) to Windows-style (\)./// On Unix, returns the path unchangedfn 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();                    }

Lines 1–500 of 1147.