ZIm/crates/prettier/src/prettier.rs

490 lines
19 KiB
Rust

use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::Context;
use collections::HashMap;
use fs::Fs;
use gpui::{AsyncAppContext, ModelHandle};
use language::language_settings::language_settings;
use language::{Buffer, Diff};
use lsp::{LanguageServer, LanguageServerId};
use node_runtime::NodeRuntime;
use serde::{Deserialize, Serialize};
use util::paths::DEFAULT_PRETTIER_DIR;
pub enum Prettier {
Real(RealPrettier),
#[cfg(any(test, feature = "test-support"))]
Test(TestPrettier),
}
pub struct RealPrettier {
worktree_id: Option<usize>,
default: bool,
prettier_dir: PathBuf,
server: Arc<LanguageServer>,
}
#[cfg(any(test, feature = "test-support"))]
pub struct TestPrettier {
worktree_id: Option<usize>,
prettier_dir: PathBuf,
default: bool,
}
#[derive(Debug)]
pub struct LocateStart {
pub worktree_root_path: Arc<Path>,
pub starting_path: Arc<Path>,
}
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(
starting_path: Option<LocateStart>,
fs: Arc<dyn Fs>,
) -> anyhow::Result<PathBuf> {
let paths_to_check = match starting_path.as_ref() {
Some(starting_path) => {
let worktree_root = starting_path
.worktree_root_path
.components()
.into_iter()
.take_while(|path_component| {
path_component.as_os_str().to_string_lossy() != "node_modules"
})
.collect::<PathBuf>();
if worktree_root != starting_path.worktree_root_path.as_ref() {
vec![worktree_root]
} else {
let (worktree_root_metadata, start_path_metadata) = if starting_path
.starting_path
.as_ref()
== Path::new("")
{
let worktree_root_data =
fs.metadata(&worktree_root).await.with_context(|| {
format!(
"FS metadata fetch for worktree root path {worktree_root:?}",
)
})?;
(worktree_root_data.unwrap_or_else(|| {
panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
}), None)
} else {
let full_starting_path = worktree_root.join(&starting_path.starting_path);
let (worktree_root_data, start_path_data) = futures::try_join!(
fs.metadata(&worktree_root),
fs.metadata(&full_starting_path),
)
.with_context(|| {
format!("FS metadata fetch for starting path {full_starting_path:?}",)
})?;
(
worktree_root_data.unwrap_or_else(|| {
panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
}),
start_path_data,
)
};
match start_path_metadata {
Some(start_path_metadata) => {
anyhow::ensure!(worktree_root_metadata.is_dir,
"For non-empty start path, worktree root {starting_path:?} should be a directory");
anyhow::ensure!(
!start_path_metadata.is_dir,
"For non-empty start path, it should not be a directory {starting_path:?}"
);
anyhow::ensure!(
!start_path_metadata.is_symlink,
"For non-empty start path, it should not be a symlink {starting_path:?}"
);
let file_to_format = starting_path.starting_path.as_ref();
let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
let mut current_path = worktree_root;
for path_component in file_to_format.components().into_iter() {
current_path = current_path.join(path_component);
paths_to_check.push_front(current_path.clone());
if path_component.as_os_str().to_string_lossy() == "node_modules" {
break;
}
}
paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
Vec::from(paths_to_check)
}
None => {
anyhow::ensure!(
!worktree_root_metadata.is_dir,
"For empty start path, worktree root should not be a directory {starting_path:?}"
);
anyhow::ensure!(
!worktree_root_metadata.is_symlink,
"For empty start path, worktree root should not be a symlink {starting_path:?}"
);
worktree_root
.parent()
.map(|path| vec![path.to_path_buf()])
.unwrap_or_default()
}
}
}
}
None => Vec::new(),
};
match find_closest_prettier_dir(paths_to_check, fs.as_ref())
.await
.with_context(|| format!("finding prettier starting with {starting_path:?}"))?
{
Some(prettier_dir) => Ok(prettier_dir),
None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()),
}
}
#[cfg(any(test, feature = "test-support"))]
pub async fn start(
worktree_id: Option<usize>,
_: LanguageServerId,
prettier_dir: PathBuf,
_: Arc<dyn NodeRuntime>,
_: AsyncAppContext,
) -> anyhow::Result<Self> {
Ok(
#[cfg(any(test, feature = "test-support"))]
Self::Test(TestPrettier {
worktree_id,
default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
prettier_dir,
}),
)
}
#[cfg(not(any(test, feature = "test-support")))]
pub async fn start(
worktree_id: Option<usize>,
server_id: LanguageServerId,
prettier_dir: PathBuf,
node: Arc<dyn NodeRuntime>,
cx: AsyncAppContext,
) -> anyhow::Result<Self> {
use lsp::LanguageServerBinary;
let backgroud = cx.background();
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 = backgroud
.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()],
},
Path::new("/"),
None,
cx,
)
.context("prettier server creation")?;
let server = backgroud
.spawn(server.initialize(None))
.await
.context("prettier server initialization")?;
Ok(Self::Real(RealPrettier {
worktree_id,
server,
default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
prettier_dir,
}))
}
pub async fn format(
&self,
buffer: &ModelHandle<Buffer>,
buffer_path: Option<PathBuf>,
cx: &AsyncAppContext,
) -> anyhow::Result<Diff> {
match self {
Self::Real(local) => {
let params = buffer.read_with(cx, |buffer, cx| {
let buffer_language = buffer.language();
let parser_with_plugins = buffer_language.and_then(|l| {
let prettier_parser = l.prettier_parser_name()?;
let mut prettier_plugins = l
.lsp_adapters()
.iter()
.flat_map(|adapter| adapter.prettier_plugins())
.collect::<Vec<_>>();
prettier_plugins.dedup();
Some((prettier_parser, prettier_plugins))
});
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);
for possible_plugin_path in [
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"),
prettier_plugin_dir,
] {
if possible_plugin_path.is_file() {
return Some(possible_plugin_path);
}
}
None
};
let (parser, located_plugins) = match parser_with_plugins {
Some((parser, plugins)) => {
// 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 plugins = plugins.into_iter().filter(|&&plugin_name| {
if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
add_tailwind_back = true;
false
} else {
true
}
}).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::<Vec<_>>();
if add_tailwind_back {
plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME)));
}
(Some(parser.to_string()), plugins)
},
None => (None, Vec::new()),
};
let prettier_options = if self.is_default() {
let language_settings = language_settings(buffer_language, buffer.file(), cx);
let mut options = language_settings.prettier.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,
)),
);
}
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();
log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx)));
anyhow::Ok(FormatParams {
text: buffer.text(),
options: FormatOptions {
parser,
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.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
Ok(diff_task.await)
}
#[cfg(any(test, feature = "test-support"))]
Self::Test(_) => Ok(buffer
.read_with(cx, |buffer, cx| {
let formatted_text = buffer.text() + FORMAT_SUFFIX;
buffer.diff(formatted_text, cx)
})
.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,
}
}
pub fn worktree_id(&self) -> Option<usize> {
match self {
Self::Real(local) => local.worktree_id,
#[cfg(any(test, feature = "test-support"))]
Self::Test(test_prettier) => test_prettier.worktree_id,
}
}
}
async fn find_closest_prettier_dir(
paths_to_check: Vec<PathBuf>,
fs: &dyn Fs,
) -> anyhow::Result<Option<PathBuf>> {
for path in paths_to_check {
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 {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"))?;
if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
&package_json_contents,
) {
if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return Ok(Some(path));
}
}
if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
{
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return Ok(Some(path));
}
}
}
}
}
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:?}"))?
{
if node_modules_location_metadata.is_dir {
return Ok(Some(path));
}
}
}
Ok(None)
}
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";
}