
There's still a bit more work to do on this, but this PR is compiling (with warnings) after eliminating the key types. When the tasks below are complete, this will be the new narrative for GPUI: - `Entity<T>` - This replaces `View<T>`/`Model<T>`. It represents a unit of state, and if `T` implements `Render`, then `Entity<T>` implements `Element`. - `&mut App` This replaces `AppContext` and represents the app. - `&mut Context<T>` This replaces `ModelContext` and derefs to `App`. It is provided by the framework when updating an entity. - `&mut Window` Broken out of `&mut WindowContext` which no longer exists. Every method that once took `&mut WindowContext` now takes `&mut Window, &mut App` and every method that took `&mut ViewContext<T>` now takes `&mut Window, &mut Context<T>` Not pictured here are the two other failed attempts. It's been quite a month! Tasks: - [x] Remove `View`, `ViewContext`, `WindowContext` and thread through `Window` - [x] [@cole-miller @mikayla-maki] Redraw window when entities change - [x] [@cole-miller @mikayla-maki] Get examples and Zed running - [x] [@cole-miller @mikayla-maki] Fix Zed rendering - [x] [@mikayla-maki] Fix todo! macros and comments - [x] Fix a bug where the editor would not be redrawn because of view caching - [x] remove publicness window.notify() and replace with `AppContext::notify` - [x] remove `observe_new_window_models`, replace with `observe_new_models` with an optional window - [x] Fix a bug where the project panel would not be redrawn because of the wrong refresh() call being used - [x] Fix the tests - [x] Fix warnings by eliminating `Window` params or using `_` - [x] Fix conflicts - [x] Simplify generic code where possible - [x] Rename types - [ ] Update docs ### issues post merge - [x] Issues switching between normal and insert mode - [x] Assistant re-rendering failure - [x] Vim test failures - [x] Mac build issue Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Mikayla <mikayla@zed.dev> Co-authored-by: Joseph <joseph@zed.dev> Co-authored-by: max <max@zed.dev> Co-authored-by: Michael Sloan <michael@zed.dev> Co-authored-by: Mikayla Maki <mikaylamaki@Mikaylas-MacBook-Pro.local> Co-authored-by: Mikayla <mikayla.c.maki@gmail.com> Co-authored-by: joão <joao@zed.dev>
1101 lines
44 KiB
Rust
1101 lines
44 KiB
Rust
use anyhow::{anyhow, Context as _};
|
|
use collections::{HashMap, HashSet};
|
|
use fs::Fs;
|
|
use gpui::{AsyncAppContext, Entity};
|
|
use language::{language_settings::language_settings, Buffer, Diff};
|
|
use lsp::{LanguageServer, LanguageServerId};
|
|
use node_runtime::NodeRuntime;
|
|
use paths::default_prettier_dir;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{
|
|
ops::ControlFlow,
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
};
|
|
use util::paths::PathMatcher;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum Prettier {
|
|
Real(RealPrettier),
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
Test(TestPrettier),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct RealPrettier {
|
|
default: bool,
|
|
prettier_dir: PathBuf,
|
|
server: Arc<LanguageServer>,
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
#[derive(Debug, 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",
|
|
".prettierignore",
|
|
];
|
|
|
|
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 closest_package_json_path = 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_node_modules(fs, &path_to_check).await? {
|
|
log::debug!("Found prettier path {path_to_check:?} in the node_modules");
|
|
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
|
} else {
|
|
match &closest_package_json_path {
|
|
None => closest_package_json_path = Some(path_to_check.clone()),
|
|
Some(closest_package_json_path) => {
|
|
match package_json_contents.get("workspaces") {
|
|
Some(serde_json::Value::Array(workspaces)) => {
|
|
let subproject_path = closest_package_json_path.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| {
|
|
workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().map_or(false, |path_matcher| path_matcher.is_match(subproject_path))
|
|
}) {
|
|
anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Path {path_to_check:?} is the workspace root for project in {closest_package_json_path:?}, but it has no prettier installed");
|
|
log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {closest_package_json_path:?}");
|
|
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
|
} else {
|
|
log::warn!("Skipping path {path_to_check:?} workspace root with workspaces {workspaces:?} that have no prettier installed");
|
|
}
|
|
},
|
|
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() {
|
|
log::debug!("Found no prettier in ancestors of {locate_from:?}");
|
|
return Ok(ControlFlow::Continue(None));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn locate_prettier_ignore(
|
|
fs: &dyn Fs,
|
|
prettier_ignores: &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 ignore 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 closest_package_json_path = None;
|
|
loop {
|
|
if prettier_ignores.contains(&path_to_check) {
|
|
log::debug!("Found prettier ignore at {path_to_check:?}");
|
|
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
|
} else if let Some(package_json_contents) =
|
|
read_package_json(fs, &path_to_check).await?
|
|
{
|
|
let ignore_path = path_to_check.join(".prettierignore");
|
|
if let Some(metadata) = fs
|
|
.metadata(&ignore_path)
|
|
.await
|
|
.with_context(|| format!("fetching metadata for {ignore_path:?}"))?
|
|
{
|
|
if !metadata.is_dir && !metadata.is_symlink {
|
|
log::info!("Found prettier ignore at {ignore_path:?}");
|
|
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
|
}
|
|
}
|
|
match &closest_package_json_path {
|
|
None => closest_package_json_path = Some(path_to_check.clone()),
|
|
Some(closest_package_json_path) => {
|
|
if let Some(serde_json::Value::Array(workspaces)) =
|
|
package_json_contents.get("workspaces")
|
|
{
|
|
let subproject_path = closest_package_json_path
|
|
.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| {
|
|
workspace_definition == subproject_path.to_string_lossy()
|
|
|| PathMatcher::new(&[workspace_definition])
|
|
.ok()
|
|
.map_or(false, |path_matcher| {
|
|
path_matcher.is_match(subproject_path)
|
|
})
|
|
})
|
|
{
|
|
let workspace_ignore = path_to_check.join(".prettierignore");
|
|
if let Some(metadata) = fs.metadata(&workspace_ignore).await? {
|
|
if !metadata.is_dir {
|
|
log::info!("Found prettier ignore at workspace root {workspace_ignore:?}");
|
|
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !path_to_check.pop() {
|
|
log::debug!("Found no prettier ignore in ancestors of {locate_from:?}");
|
|
return Ok(ControlFlow::Continue(None));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub async fn start(
|
|
_: LanguageServerId,
|
|
prettier_dir: PathBuf,
|
|
_: 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: NodeRuntime,
|
|
cx: AsyncAppContext,
|
|
) -> anyhow::Result<Self> {
|
|
use lsp::{LanguageServerBinary, LanguageServerName};
|
|
|
|
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_name = LanguageServerName("prettier".into());
|
|
let server_binary = LanguageServerBinary {
|
|
path: node_path,
|
|
arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
|
|
env: None,
|
|
};
|
|
let server = LanguageServer::new(
|
|
Arc::new(parking_lot::Mutex::new(None)),
|
|
server_id,
|
|
server_name,
|
|
server_binary,
|
|
&prettier_dir,
|
|
None,
|
|
cx.clone(),
|
|
)
|
|
.context("prettier server creation")?;
|
|
|
|
let initialize_params = None;
|
|
let configuration = lsp::DidChangeConfigurationParams {
|
|
settings: Default::default(),
|
|
};
|
|
let server = cx
|
|
.update(|cx| {
|
|
executor.spawn(server.initialize(initialize_params, configuration.into(), 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: &Entity<Buffer>,
|
|
buffer_path: Option<PathBuf>,
|
|
ignore_dir: 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.map(|l| l.name()), 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("src").join("plugin.js"),
|
|
prettier_plugin_dir.join("lib").join("index.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 mut prettier_parser = prettier_settings.parser.as_deref();
|
|
if buffer_path.is_none() {
|
|
prettier_parser = prettier_parser.or_else(|| buffer_language.and_then(|language| language.prettier_parser_name()));
|
|
if prettier_parser.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"));
|
|
}
|
|
|
|
}
|
|
|
|
let ignore_path = ignore_dir.and_then(|dir| {
|
|
let ignore_file = dir.join(".prettierignore");
|
|
ignore_file.is_file().then_some(ignore_file)
|
|
});
|
|
|
|
log::debug!(
|
|
"Formatting file {:?} with prettier, plugins :{:?}, options: {:?}, ignore_path: {:?}",
|
|
buffer.file().map(|f| f.full_path(cx)),
|
|
plugins,
|
|
prettier_options,
|
|
ignore_path,
|
|
);
|
|
|
|
anyhow::Ok(FormatParams {
|
|
text: buffer.text(),
|
|
options: FormatOptions {
|
|
parser: prettier_parser.map(ToOwned::to_owned),
|
|
plugins,
|
|
path: buffer_path,
|
|
prettier_options,
|
|
ignore_path,
|
|
},
|
|
})
|
|
})?
|
|
.context("prettier params calculation")?;
|
|
|
|
let response = local.server.request::<Format>(params).await?;
|
|
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)
|
|
}
|
|
|
|
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>>,
|
|
ignore_path: Option<PathBuf>,
|
|
}
|
|
|
|
#[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";
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use fs::FakeFs;
|
|
use serde_json::json;
|
|
|
|
use super::*;
|
|
|
|
#[gpui::test]
|
|
async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) {
|
|
let fs = FakeFs::new(cx.executor());
|
|
fs.insert_tree(
|
|
"/root",
|
|
json!({
|
|
".config": {
|
|
"zed": {
|
|
"settings.json": r#"{ "formatter": "auto" }"#,
|
|
},
|
|
},
|
|
"work": {
|
|
"project": {
|
|
"src": {
|
|
"index.js": "// index.js file contents",
|
|
},
|
|
"node_modules": {
|
|
"expect": {
|
|
"build": {
|
|
"print.js": "// print.js file contents",
|
|
},
|
|
"package.json": r#"{
|
|
"devDependencies": {
|
|
"prettier": "2.5.1"
|
|
}
|
|
}"#,
|
|
},
|
|
"prettier": {
|
|
"index.js": "// Dummy prettier package file",
|
|
},
|
|
},
|
|
"package.json": r#"{}"#
|
|
},
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/.config/zed/settings.json"),
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Continue(None),
|
|
"Should find no prettier for path hierarchy without it"
|
|
);
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/work/project/src/index.js")
|
|
)
|
|
.await.unwrap(),
|
|
ControlFlow::Continue(Some(PathBuf::from("/root/work/project"))),
|
|
"Should successfully find a prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
|
|
);
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/work/project/node_modules/expect/build/print.js")
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Break(()),
|
|
"Should not format files inside node_modules/"
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) {
|
|
let fs = FakeFs::new(cx.executor());
|
|
fs.insert_tree(
|
|
"/root",
|
|
json!({
|
|
"web_blog": {
|
|
"node_modules": {
|
|
"prettier": {
|
|
"index.js": "// Dummy prettier package file",
|
|
},
|
|
"expect": {
|
|
"build": {
|
|
"print.js": "// print.js file contents",
|
|
},
|
|
"package.json": r#"{
|
|
"devDependencies": {
|
|
"prettier": "2.5.1"
|
|
}
|
|
}"#,
|
|
},
|
|
},
|
|
"pages": {
|
|
"[slug].tsx": "// [slug].tsx file contents",
|
|
},
|
|
"package.json": r#"{
|
|
"devDependencies": {
|
|
"prettier": "2.3.0"
|
|
},
|
|
"prettier": {
|
|
"semi": false,
|
|
"printWidth": 80,
|
|
"htmlWhitespaceSensitivity": "strict",
|
|
"tabWidth": 4
|
|
}
|
|
}"#
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/web_blog/pages/[slug].tsx")
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Continue(Some(PathBuf::from("/root/web_blog"))),
|
|
"Should find a preinstalled prettier in the project root"
|
|
);
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/web_blog/node_modules/expect/build/print.js")
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Break(()),
|
|
"Should not allow formatting node_modules/ contents"
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) {
|
|
let fs = FakeFs::new(cx.executor());
|
|
fs.insert_tree(
|
|
"/root",
|
|
json!({
|
|
"work": {
|
|
"web_blog": {
|
|
"node_modules": {
|
|
"expect": {
|
|
"build": {
|
|
"print.js": "// print.js file contents",
|
|
},
|
|
"package.json": r#"{
|
|
"devDependencies": {
|
|
"prettier": "2.5.1"
|
|
}
|
|
}"#,
|
|
},
|
|
},
|
|
"pages": {
|
|
"[slug].tsx": "// [slug].tsx file contents",
|
|
},
|
|
"package.json": r#"{
|
|
"devDependencies": {
|
|
"prettier": "2.3.0"
|
|
},
|
|
"prettier": {
|
|
"semi": false,
|
|
"printWidth": 80,
|
|
"htmlWhitespaceSensitivity": "strict",
|
|
"tabWidth": 4
|
|
}
|
|
}"#
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/work/web_blog/pages/[slug].tsx")
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Continue(None),
|
|
"Should find no prettier when node_modules don't have it"
|
|
);
|
|
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::from_iter(
|
|
[PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
|
|
),
|
|
Path::new("/root/work/web_blog/pages/[slug].tsx")
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Continue(Some(PathBuf::from("/root/work"))),
|
|
"Should return closest cached value found without path checks"
|
|
);
|
|
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Break(()),
|
|
"Should not allow formatting files inside node_modules/"
|
|
);
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::from_iter(
|
|
[PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
|
|
),
|
|
Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Break(()),
|
|
"Should ignore cache lookup for files inside node_modules/"
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) {
|
|
let fs = FakeFs::new(cx.executor());
|
|
fs.insert_tree(
|
|
"/root",
|
|
json!({
|
|
"work": {
|
|
"full-stack-foundations": {
|
|
"exercises": {
|
|
"03.loading": {
|
|
"01.problem.loader": {
|
|
"app": {
|
|
"routes": {
|
|
"users+": {
|
|
"$username_+": {
|
|
"notes.tsx": "// notes.tsx file contents",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"node_modules": {
|
|
"test.js": "// test.js contents",
|
|
},
|
|
"package.json": r#"{
|
|
"devDependencies": {
|
|
"prettier": "^3.0.3"
|
|
}
|
|
}"#
|
|
},
|
|
},
|
|
},
|
|
"package.json": r#"{
|
|
"workspaces": ["exercises/*/*", "examples/*"]
|
|
}"#,
|
|
"node_modules": {
|
|
"prettier": {
|
|
"index.js": "// Dummy prettier package file",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx"),
|
|
).await.unwrap(),
|
|
ControlFlow::Continue(Some(PathBuf::from("/root/work/full-stack-foundations"))),
|
|
"Should ascend to the multi-workspace root and find the prettier there",
|
|
);
|
|
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/work/full-stack-foundations/node_modules/prettier/index.js")
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Break(()),
|
|
"Should not allow formatting files inside root node_modules/"
|
|
);
|
|
assert_eq!(
|
|
Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/node_modules/test.js")
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Break(()),
|
|
"Should not allow formatting files inside submodule's node_modules/"
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_prettier_lookup_in_npm_workspaces_for_not_installed(
|
|
cx: &mut gpui::TestAppContext,
|
|
) {
|
|
let fs = FakeFs::new(cx.executor());
|
|
fs.insert_tree(
|
|
"/root",
|
|
json!({
|
|
"work": {
|
|
"full-stack-foundations": {
|
|
"exercises": {
|
|
"03.loading": {
|
|
"01.problem.loader": {
|
|
"app": {
|
|
"routes": {
|
|
"users+": {
|
|
"$username_+": {
|
|
"notes.tsx": "// notes.tsx file contents",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"node_modules": {},
|
|
"package.json": r#"{
|
|
"devDependencies": {
|
|
"prettier": "^3.0.3"
|
|
}
|
|
}"#
|
|
},
|
|
},
|
|
},
|
|
"package.json": r#"{
|
|
"workspaces": ["exercises/*/*", "examples/*"]
|
|
}"#,
|
|
},
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
match Prettier::locate_prettier_installation(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx")
|
|
)
|
|
.await {
|
|
Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"),
|
|
Err(e) => {
|
|
let message = e.to_string();
|
|
assert!(message.contains("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader"), "Error message should mention which project had prettier defined");
|
|
assert!(message.contains("/root/work/full-stack-foundations"), "Error message should mention potential candidates without prettier node_modules contents");
|
|
},
|
|
};
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_prettier_ignore_with_editor_prettier(cx: &mut gpui::TestAppContext) {
|
|
let fs = FakeFs::new(cx.executor());
|
|
fs.insert_tree(
|
|
"/root",
|
|
json!({
|
|
"project": {
|
|
"src": {
|
|
"index.js": "// index.js file contents",
|
|
"ignored.js": "// this file should be ignored",
|
|
},
|
|
".prettierignore": "ignored.js",
|
|
"package.json": r#"{
|
|
"name": "test-project"
|
|
}"#
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
Prettier::locate_prettier_ignore(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/project/src/index.js"),
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Continue(Some(PathBuf::from("/root/project"))),
|
|
"Should find prettierignore in project root"
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_prettier_ignore_in_monorepo_with_only_child_ignore(
|
|
cx: &mut gpui::TestAppContext,
|
|
) {
|
|
let fs = FakeFs::new(cx.executor());
|
|
fs.insert_tree(
|
|
"/root",
|
|
json!({
|
|
"monorepo": {
|
|
"node_modules": {
|
|
"prettier": {
|
|
"index.js": "// Dummy prettier package file",
|
|
}
|
|
},
|
|
"packages": {
|
|
"web": {
|
|
"src": {
|
|
"index.js": "// index.js contents",
|
|
"ignored.js": "// this should be ignored",
|
|
},
|
|
".prettierignore": "ignored.js",
|
|
"package.json": r#"{
|
|
"name": "web-package"
|
|
}"#
|
|
}
|
|
},
|
|
"package.json": r#"{
|
|
"workspaces": ["packages/*"],
|
|
"devDependencies": {
|
|
"prettier": "^2.0.0"
|
|
}
|
|
}"#
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
Prettier::locate_prettier_ignore(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/monorepo/packages/web/src/index.js"),
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
|
|
"Should find prettierignore in child package"
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_prettier_ignore_in_monorepo_with_root_and_child_ignores(
|
|
cx: &mut gpui::TestAppContext,
|
|
) {
|
|
let fs = FakeFs::new(cx.executor());
|
|
fs.insert_tree(
|
|
"/root",
|
|
json!({
|
|
"monorepo": {
|
|
"node_modules": {
|
|
"prettier": {
|
|
"index.js": "// Dummy prettier package file",
|
|
}
|
|
},
|
|
".prettierignore": "main.js",
|
|
"packages": {
|
|
"web": {
|
|
"src": {
|
|
"main.js": "// this should not be ignored",
|
|
"ignored.js": "// this should be ignored",
|
|
},
|
|
".prettierignore": "ignored.js",
|
|
"package.json": r#"{
|
|
"name": "web-package"
|
|
}"#
|
|
}
|
|
},
|
|
"package.json": r#"{
|
|
"workspaces": ["packages/*"],
|
|
"devDependencies": {
|
|
"prettier": "^2.0.0"
|
|
}
|
|
}"#
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
Prettier::locate_prettier_ignore(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/monorepo/packages/web/src/main.js"),
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
|
|
"Should find child package prettierignore first"
|
|
);
|
|
|
|
assert_eq!(
|
|
Prettier::locate_prettier_ignore(
|
|
fs.as_ref(),
|
|
&HashSet::default(),
|
|
Path::new("/root/monorepo/packages/web/src/ignored.js"),
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
|
|
"Should find child package prettierignore first"
|
|
);
|
|
}
|
|
}
|