diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 92db62e6c6a52bad563d10f9d4afa58fc8f1afb2..d4c1654d9257772e6be3dc966770d16db510c1ef 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -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, + locate_from: &Path, + ) -> anyhow::Result>> { + let mut path_to_check = locate_from + .components() + .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules") + .collect::(); + 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_path: Option, + ignore_dir: Option, cx: &mut AsyncAppContext, ) -> anyhow::Result { 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, prettier_options: Option>, + ignore_path: Option, } #[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" + ); + } } diff --git a/crates/prettier/src/prettier_server.js b/crates/prettier/src/prettier_server.js index d19c557f8e05e1fcfc160c472f4c2fa32aaff262..abf8435b993fc66009e422a1da2884af46a6cf04 100644 --- a/crates/prettier/src/prettier_server.js +++ b/crates/prettier/src/prettier_server.js @@ -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 diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index c7ac0ffd0bbf6ce9cb980dcabe8f930b61e426df..e707f9e9bc2d20d906bd69f01efdb7586a0a59cd 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -36,6 +36,7 @@ pub struct PrettierStore { worktree_store: Model, default_prettier: DefaultPrettier, prettiers_per_worktree: HashMap>>, + prettier_ignores_per_worktree: HashMap>, prettier_instances: HashMap, } @@ -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.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, + cx: &mut ModelContext, + ) -> Task> { + 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));