876 lines · 35.2 KB · Rust
RawDownload
use anyhow::{anyhow, Context};use collections::{HashMap, HashSet};use fs::Fs;use gpui::{AsyncAppContext, Model};use language::{language_settings::language_settings, Buffer, Diff};use lsp::{LanguageServer, LanguageServerId};use node_runtime::NodeRuntime;use serde::{Deserialize, Serialize};use std::{    ops::ControlFlow,    path::{Path, PathBuf},    sync::Arc,};use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR}; #[derive(Clone)]pub enum Prettier {    Real(RealPrettier),    #[cfg(any(test, feature = "test-support"))]    Test(TestPrettier),} #[derive(Clone)]pub struct RealPrettier {    default: bool,    prettier_dir: PathBuf,    server: Arc<LanguageServer>,} #[cfg(any(test, feature = "test-support"))]#[derive(Clone)]pub struct TestPrettier {    prettier_dir: PathBuf,    default: bool,} pub const FAIL_THRESHOLD: usize = 4;pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");const PRETTIER_PACKAGE_NAME: &str = "prettier";const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss"; #[cfg(any(test, feature = "test-support"))]pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier"; impl Prettier {    pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[        ".prettierrc",        ".prettierrc.json",        ".prettierrc.json5",        ".prettierrc.yaml",        ".prettierrc.yml",        ".prettierrc.toml",        ".prettierrc.js",        ".prettierrc.cjs",        "package.json",        "prettier.config.js",        "prettier.config.cjs",        ".editorconfig",    ];     pub async fn locate_prettier_installation(        fs: &dyn Fs,        installed_prettiers: &HashSet<PathBuf>,        locate_from: &Path,    ) -> anyhow::Result<ControlFlow<(), Option<PathBuf>>> {        let mut path_to_check = locate_from            .components()            .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules")            .collect::<PathBuf>();        if path_to_check != locate_from {            log::debug!(                "Skipping prettier location for path {path_to_check:?} that is inside node_modules"            );            return Ok(ControlFlow::Break(()));        }        let path_to_check_metadata = fs            .metadata(&path_to_check)            .await            .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))?            .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?;        if !path_to_check_metadata.is_dir {            path_to_check.pop();        }         let mut project_path_with_prettier_dependency = None;        loop {            if installed_prettiers.contains(&path_to_check) {                log::debug!("Found prettier path {path_to_check:?} in installed prettiers");                return Ok(ControlFlow::Continue(Some(path_to_check)));            } else if let Some(package_json_contents) =                read_package_json(fs, &path_to_check).await?            {                if has_prettier_in_package_json(&package_json_contents) {                    if has_prettier_in_node_modules(fs, &path_to_check).await? {                        log::debug!("Found prettier path {path_to_check:?} in both package.json and node_modules");                        return Ok(ControlFlow::Continue(Some(path_to_check)));                    } else if project_path_with_prettier_dependency.is_none() {                        project_path_with_prettier_dependency = Some(path_to_check.clone());                    }                } else {                    match package_json_contents.get("workspaces") {                            Some(serde_json::Value::Array(workspaces)) => {                                match &project_path_with_prettier_dependency {                                    Some(project_path_with_prettier_dependency) => {                                        let subproject_path = project_path_with_prettier_dependency.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");                                        if workspaces.iter().filter_map(|value| {                                            if let serde_json::Value::String(s) = value {                                                Some(s.clone())                                            } else {                                                log::warn!("Skipping non-string 'workspaces' value: {value:?}");                                                None                                            }                                        }).any(|workspace_definition| {                                            if let Some(path_matcher) = PathMatcher::new(&workspace_definition).ok() {                                                path_matcher.is_match(subproject_path)                                            } else {                                                workspace_definition == subproject_path.to_string_lossy()                                            }                                        }) {                                            anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}, but it's not installed into workspace root's node_modules");                                            log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}");                                            return Ok(ControlFlow::Continue(Some(path_to_check)));                                        } else {                                            log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but is not included in its package.json workspaces {workspaces:?}");                                        }                                    }                                    None => {                                        log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but has no prettier in its package.json");                                    }                                }                            },                            Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."),                            None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"),                        }                }            }             if !path_to_check.pop() {                match project_path_with_prettier_dependency {                    Some(closest_prettier_discovered) => {                        anyhow::bail!("No prettier found in node_modules for ancestors of {locate_from:?}, but discovered prettier package.json dependency in {closest_prettier_discovered:?}")                    }                    None => {                        log::debug!("Found no prettier in ancestors of {locate_from:?}");                        return Ok(ControlFlow::Continue(None));                    }                }            }        }    }     #[cfg(any(test, feature = "test-support"))]    pub async fn start(        _: LanguageServerId,        prettier_dir: PathBuf,        _: Arc<dyn NodeRuntime>,        _: AsyncAppContext,    ) -> anyhow::Result<Self> {        Ok(Self::Test(TestPrettier {            default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),            prettier_dir,        }))    }     #[cfg(not(any(test, feature = "test-support")))]    pub async fn start(        server_id: LanguageServerId,        prettier_dir: PathBuf,        node: Arc<dyn NodeRuntime>,        cx: AsyncAppContext,    ) -> anyhow::Result<Self> {        use lsp::LanguageServerBinary;         let executor = cx.background_executor().clone();        anyhow::ensure!(            prettier_dir.is_dir(),            "Prettier dir {prettier_dir:?} is not a directory"        );        let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);        anyhow::ensure!(            prettier_server.is_file(),            "no prettier server package found at {prettier_server:?}"        );         let node_path = executor            .spawn(async move { node.binary_path().await })            .await?;        let server = LanguageServer::new(            Arc::new(parking_lot::Mutex::new(None)),            server_id,            LanguageServerBinary {                path: node_path,                arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],                env: None,            },            &prettier_dir,            None,            cx.clone(),        )        .context("prettier server creation")?;        let server = cx            .update(|cx| executor.spawn(server.initialize(None, cx)))?            .await            .context("prettier server initialization")?;        Ok(Self::Real(RealPrettier {            server,            default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),            prettier_dir,        }))    }     pub async fn format(        &self,        buffer: &Model<Buffer>,        buffer_path: Option<PathBuf>,        cx: &mut AsyncAppContext,    ) -> anyhow::Result<Diff> {        match self {            Self::Real(local) => {                let params = buffer                    .update(cx, |buffer, cx| {                        let buffer_language = buffer.language();                        let language_settings = language_settings(buffer_language, buffer.file(), cx);                        let prettier_settings = &language_settings.prettier;                        anyhow::ensure!(                            prettier_settings.allowed,                            "Cannot format: prettier is not allowed for language {buffer_language:?}"                        );                        let prettier_node_modules = self.prettier_dir().join("node_modules");                        anyhow::ensure!(                            prettier_node_modules.is_dir(),                            "Prettier node_modules dir does not exist: {prettier_node_modules:?}"                        );                        let plugin_name_into_path = |plugin_name: &str| {                            let prettier_plugin_dir = prettier_node_modules.join(plugin_name);                            [                                prettier_plugin_dir.join("dist").join("index.mjs"),                                prettier_plugin_dir.join("dist").join("index.js"),                                prettier_plugin_dir.join("dist").join("plugin.js"),                                prettier_plugin_dir.join("index.mjs"),                                prettier_plugin_dir.join("index.js"),                                prettier_plugin_dir.join("plugin.js"),                                // this one is for @prettier/plugin-php                                prettier_plugin_dir.join("standalone.js"),                                prettier_plugin_dir,                            ]                            .into_iter()                            .find(|possible_plugin_path| possible_plugin_path.is_file())                        };                         // Tailwind plugin requires being added last                        // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins                        let mut add_tailwind_back = false;                         let mut located_plugins = prettier_settings.plugins.iter()                            .filter(|plugin_name| {                                if plugin_name.as_str() == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {                                    add_tailwind_back = true;                                    false                                } else {                                    true                                }                            })                            .map(|plugin_name| {                                let plugin_path = plugin_name_into_path(plugin_name);                                (plugin_name.clone(), plugin_path)                            })                            .collect::<Vec<_>>();                        if add_tailwind_back {                            located_plugins.push((                                TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.to_owned(),                                plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME),                            ));                        }                         let prettier_options = if self.is_default() {                            let mut options = prettier_settings.options.clone();                            if !options.contains_key("tabWidth") {                                options.insert(                                    "tabWidth".to_string(),                                    serde_json::Value::Number(serde_json::Number::from(                                        language_settings.tab_size.get(),                                    )),                                );                            }                            if !options.contains_key("printWidth") {                                options.insert(                                    "printWidth".to_string(),                                    serde_json::Value::Number(serde_json::Number::from(                                        language_settings.preferred_line_length,                                    )),                                );                            }                            if !options.contains_key("useTabs") {                                options.insert(                                    "useTabs".to_string(),                                    serde_json::Value::Bool(language_settings.hard_tabs),                                );                            }                            Some(options)                        } else {                            None                        };                         let plugins = located_plugins                            .into_iter()                            .filter_map(|(plugin_name, located_plugin_path)| {                                match located_plugin_path {                                    Some(path) => Some(path),                                    None => {                                        log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");                                        None                                    }                                }                            })                            .collect();                         let prettier_parser = prettier_settings.parser.as_deref().or_else(|| buffer_language.and_then(|language| language.prettier_parser_name()));                         if prettier_parser.is_none() && buffer_path.is_none() {                            log::error!("Formatting unsaved file with prettier failed. No prettier parser configured for language {buffer_language:?}");                            return Err(anyhow!("Cannot determine prettier parser for unsaved file"));                        }                         log::debug!(                            "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}",                            buffer.file().map(|f| f.full_path(cx)),                            plugins,                            prettier_options,                        );                         anyhow::Ok(FormatParams {                            text: buffer.text(),                            options: FormatOptions {                                parser: prettier_parser.map(ToOwned::to_owned),                                plugins,                                path: buffer_path,                                prettier_options,                            },                        })                    })?                    .context("prettier params calculation")?;                 let response = local                    .server                    .request::<Format>(params)                    .await                    .context("prettier format request")?;                let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx))?;                Ok(diff_task.await)            }            #[cfg(any(test, feature = "test-support"))]            Self::Test(_) => Ok(buffer                .update(cx, |buffer, cx| {                    match buffer                        .language()                        .map(|language| language.lsp_id())                        .as_deref()                    {                        Some("rust") => anyhow::bail!("prettier does not support Rust"),                        Some(_other) => {                            let formatted_text = buffer.text() + FORMAT_SUFFIX;                            Ok(buffer.diff(formatted_text, cx))                        }                        None => panic!("Should not format buffer without a language with prettier"),                    }                })??                .await),        }    }     pub async fn clear_cache(&self) -> anyhow::Result<()> {        match self {            Self::Real(local) => local                .server                .request::<ClearCache>(())                .await                .context("prettier clear cache"),            #[cfg(any(test, feature = "test-support"))]            Self::Test(_) => Ok(()),        }    }     pub fn server(&self) -> Option<&Arc<LanguageServer>> {        match self {            Self::Real(local) => Some(&local.server),            #[cfg(any(test, feature = "test-support"))]            Self::Test(_) => None,        }    }     pub fn is_default(&self) -> bool {        match self {            Self::Real(local) => local.default,            #[cfg(any(test, feature = "test-support"))]            Self::Test(test_prettier) => test_prettier.default,        }    }     pub fn prettier_dir(&self) -> &Path {        match self {            Self::Real(local) => &local.prettier_dir,            #[cfg(any(test, feature = "test-support"))]            Self::Test(test_prettier) => &test_prettier.prettier_dir,        }    }} async fn has_prettier_in_node_modules(fs: &dyn Fs, path: &Path) -> anyhow::Result<bool> {    let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);    if let Some(node_modules_location_metadata) = fs        .metadata(&possible_node_modules_location)        .await        .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?    {        return Ok(node_modules_location_metadata.is_dir);    }    Ok(false)} async fn read_package_json(    fs: &dyn Fs,    path: &Path,) -> anyhow::Result<Option<HashMap<String, serde_json::Value>>> {    let possible_package_json = path.join("package.json");    if let Some(package_json_metadata) = fs        .metadata(&possible_package_json)        .await        .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))?    {        if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {            let package_json_contents = fs                .load(&possible_package_json)                .await                .with_context(|| format!("reading {possible_package_json:?} file contents"))?;            return serde_json::from_str::<HashMap<String, serde_json::Value>>(                &package_json_contents,            )            .map(Some)            .with_context(|| format!("parsing {possible_package_json:?} file contents"));        }    }    Ok(None)} fn has_prettier_in_package_json(    package_json_contents: &HashMap<String, serde_json::Value>,) -> bool {    if let Some(serde_json::Value::Object(o)) = package_json_contents.get("dependencies") {        if o.contains_key(PRETTIER_PACKAGE_NAME) {            return true;        }    }    if let Some(serde_json::Value::Object(o)) = package_json_contents.get("devDependencies") {        if o.contains_key(PRETTIER_PACKAGE_NAME) {            return true;        }    }    false} enum Format {} #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]struct FormatParams {    text: String,    options: FormatOptions,} #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]struct FormatOptions {    plugins: Vec<PathBuf>,    parser: Option<String>,    #[serde(rename = "filepath")]    path: Option<PathBuf>,    prettier_options: Option<HashMap<String, serde_json::Value>>,} #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]#[serde(rename_all = "camelCase")]struct FormatResult {    text: String,} impl lsp::request::Request for Format {    type Params = FormatParams;    type Result = FormatResult;    const METHOD: &'static str = "prettier/format";} enum ClearCache {} impl lsp::request::Request for ClearCache {    type Params = ();    type Result = ();    const METHOD: &'static str = "prettier/clear_cache";}

Lines 1–500 of 876.