mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 20:32:22 +03:00
Add .prettierignore support (#21297)
Closes #11115 **Context**: Consider a monorepo setup like this: the root has Prettier installed, but the individual monorepos do not. In this case, only one Prettier instance is used, with its installation located at the root. The monorepos also use this same instance for formatting. However, monorepo can have its own `.prettierignore` file, which will take precedence over the `.prettierignore` file at the root level (if one exists) for files in that monorepo. <img src="https://github.com/user-attachments/assets/742f16ac-11ad-4d2f-a5a2-696e47a617b9" alt="prettier" width="200px" /> **Implementation**: From the context above, we should keep ignore dir decoupled from the Prettier instance. This means that even if the project has only one Prettier installation (and thus a single Prettier instance), there can still be multiple `.prettierignore` in play. This approach also allows us to respect `.prettierignore` even when the project does not have Prettier installed locally and instead relies on the editor’s Prettier instance. **Tests**: 1. No Prettier in project, using editor Prettier: Ensures `.prettierignore` is respected even without a local Prettier installation. 2. Monorepo with root Prettier and child `.prettierignore`: Confirms that the child project’s ignore file is correctly used. 3. Monorepo with root and child `.prettierignore` files: Verifies the child ignore file takes precedence over the root’s. Release Notes: - Added `.prettierignore` support to the Prettier integration.
This commit is contained in:
parent
8dd1c23b92
commit
6a37307302
@ -58,6 +58,7 @@ impl Prettier {
|
||||
"prettier.config.js",
|
||||
"prettier.config.cjs",
|
||||
".editorconfig",
|
||||
".prettierignore",
|
||||
];
|
||||
|
||||
pub async fn locate_prettier_installation(
|
||||
@ -134,6 +135,101 @@ impl Prettier {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@ -201,6 +297,7 @@ impl Prettier {
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_path: Option<PathBuf>,
|
||||
ignore_dir: Option<PathBuf>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> anyhow::Result<Diff> {
|
||||
match self {
|
||||
@ -315,11 +412,17 @@ impl Prettier {
|
||||
|
||||
}
|
||||
|
||||
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: {:?}",
|
||||
"Formatting file {:?} with prettier, plugins :{:?}, options: {:?}, ignore_path: {:?}",
|
||||
buffer.file().map(|f| f.full_path(cx)),
|
||||
plugins,
|
||||
prettier_options,
|
||||
ignore_path,
|
||||
);
|
||||
|
||||
anyhow::Ok(FormatParams {
|
||||
@ -329,6 +432,7 @@ impl Prettier {
|
||||
plugins,
|
||||
path: buffer_path,
|
||||
prettier_options,
|
||||
ignore_path,
|
||||
},
|
||||
})
|
||||
})?
|
||||
@ -449,6 +553,7 @@ struct FormatOptions {
|
||||
#[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)]
|
||||
@ -840,4 +945,150 @@ mod tests {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,9 @@ class Prettier {
|
||||
process.exit(1);
|
||||
}
|
||||
process.stderr.write(
|
||||
`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`,
|
||||
`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(
|
||||
config,
|
||||
)}\n`,
|
||||
);
|
||||
process.stdin.resume();
|
||||
handleBuffer(new Prettier(prettierPath, prettier, config));
|
||||
@ -68,7 +70,9 @@ async function handleBuffer(prettier) {
|
||||
sendResponse({
|
||||
id: message.id,
|
||||
...makeError(
|
||||
`error during message '${JSON.stringify(errorMessage)}' handling: ${e}`,
|
||||
`error during message '${JSON.stringify(
|
||||
errorMessage,
|
||||
)}' handling: ${e}`,
|
||||
),
|
||||
});
|
||||
});
|
||||
@ -189,6 +193,22 @@ async function handleMessage(message, prettier) {
|
||||
if (params.options.filepath) {
|
||||
resolvedConfig =
|
||||
(await prettier.prettier.resolveConfig(params.options.filepath)) || {};
|
||||
|
||||
if (params.options.ignorePath) {
|
||||
const fileInfo = await prettier.prettier.getFileInfo(
|
||||
params.options.filepath,
|
||||
{
|
||||
ignorePath: params.options.ignorePath,
|
||||
},
|
||||
);
|
||||
if (fileInfo.ignored) {
|
||||
process.stderr.write(
|
||||
`Ignoring file '${params.options.filepath}' based on rules in '${params.options.ignorePath}'\n`,
|
||||
);
|
||||
sendResponse({ id, result: { text: params.text } });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Marking the params.options.filepath as undefined makes
|
||||
|
@ -36,6 +36,7 @@ pub struct PrettierStore {
|
||||
worktree_store: Model<WorktreeStore>,
|
||||
default_prettier: DefaultPrettier,
|
||||
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
|
||||
prettier_ignores_per_worktree: HashMap<WorktreeId, HashSet<PathBuf>>,
|
||||
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
||||
}
|
||||
|
||||
@ -65,11 +66,13 @@ impl PrettierStore {
|
||||
worktree_store,
|
||||
default_prettier: DefaultPrettier::default(),
|
||||
prettiers_per_worktree: HashMap::default(),
|
||||
prettier_ignores_per_worktree: HashMap::default(),
|
||||
prettier_instances: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
|
||||
self.prettier_ignores_per_worktree.remove(&id_to_remove);
|
||||
let mut prettier_instances_to_clean = FuturesUnordered::new();
|
||||
if let Some(prettier_paths) = self.prettiers_per_worktree.remove(&id_to_remove) {
|
||||
for path in prettier_paths.iter().flatten() {
|
||||
@ -211,6 +214,65 @@ impl PrettierStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn prettier_ignore_for_buffer(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Option<PathBuf>> {
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_file = buffer.file();
|
||||
if buffer.language().is_none() {
|
||||
return Task::ready(None);
|
||||
}
|
||||
match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) {
|
||||
Some((worktree_id, buffer_path)) => {
|
||||
let fs = Arc::clone(&self.fs);
|
||||
let prettier_ignores = self
|
||||
.prettier_ignores_per_worktree
|
||||
.get(&worktree_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
cx.spawn(|lsp_store, mut cx| async move {
|
||||
match cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
Prettier::locate_prettier_ignore(
|
||||
fs.as_ref(),
|
||||
&prettier_ignores,
|
||||
&buffer_path,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(ControlFlow::Break(())) => None,
|
||||
Ok(ControlFlow::Continue(None)) => None,
|
||||
Ok(ControlFlow::Continue(Some(ignore_dir))) => {
|
||||
log::debug!("Found prettier ignore in {ignore_dir:?}");
|
||||
lsp_store
|
||||
.update(&mut cx, |store, _| {
|
||||
store
|
||||
.prettier_ignores_per_worktree
|
||||
.entry(worktree_id)
|
||||
.or_default()
|
||||
.insert(ignore_dir.clone());
|
||||
})
|
||||
.ok();
|
||||
Some(ignore_dir)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to determine prettier ignore path for buffer: {e:#}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
None => Task::ready(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_prettier(
|
||||
node: NodeRuntime,
|
||||
prettier_dir: PathBuf,
|
||||
@ -654,6 +716,13 @@ pub(super) async fn format_with_prettier(
|
||||
.ok()?
|
||||
.await;
|
||||
|
||||
let ignore_dir = prettier_store
|
||||
.update(cx, |prettier_store, cx| {
|
||||
prettier_store.prettier_ignore_for_buffer(buffer, cx)
|
||||
})
|
||||
.ok()?
|
||||
.await;
|
||||
|
||||
let (prettier_path, prettier_task) = prettier_instance?;
|
||||
|
||||
let prettier_description = match prettier_path.as_ref() {
|
||||
@ -671,7 +740,7 @@ pub(super) async fn format_with_prettier(
|
||||
.flatten();
|
||||
|
||||
let format_result = prettier
|
||||
.format(buffer, buffer_path, cx)
|
||||
.format(buffer, buffer_path, ignore_dir, cx)
|
||||
.await
|
||||
.map(crate::lsp_store::FormatOperation::Prettier)
|
||||
.with_context(|| format!("{} failed to format buffer", prettier_description));
|
||||
|
Loading…
Reference in New Issue
Block a user