diff --git a/crates/oxc_language_server/fixtures/formatter/ignore-file/.oxfmtrc.json b/crates/oxc_language_server/fixtures/formatter/ignore-file/.oxfmtrc.json new file mode 100644 index 0000000000000..cce9d3c080177 --- /dev/null +++ b/crates/oxc_language_server/fixtures/formatter/ignore-file/.oxfmtrc.json @@ -0,0 +1,3 @@ +{ + "semi": false +} diff --git a/crates/oxc_language_server/fixtures/formatter/ignore-file/.prettierignore b/crates/oxc_language_server/fixtures/formatter/ignore-file/.prettierignore new file mode 100644 index 0000000000000..6461deecd1c60 --- /dev/null +++ b/crates/oxc_language_server/fixtures/formatter/ignore-file/.prettierignore @@ -0,0 +1 @@ +*.ts diff --git a/crates/oxc_language_server/fixtures/formatter/ignore-file/ignored.ts b/crates/oxc_language_server/fixtures/formatter/ignore-file/ignored.ts new file mode 100644 index 0000000000000..0c276cc2f93c3 --- /dev/null +++ b/crates/oxc_language_server/fixtures/formatter/ignore-file/ignored.ts @@ -0,0 +1,2 @@ +// semicolon is on character 8-9, which will be removed +debugger; diff --git a/crates/oxc_language_server/fixtures/formatter/ignore-file/not-ignored.js b/crates/oxc_language_server/fixtures/formatter/ignore-file/not-ignored.js new file mode 100644 index 0000000000000..0c276cc2f93c3 --- /dev/null +++ b/crates/oxc_language_server/fixtures/formatter/ignore-file/not-ignored.js @@ -0,0 +1,2 @@ +// semicolon is on character 8-9, which will be removed +debugger; diff --git a/crates/oxc_language_server/fixtures/formatter/ignore-pattern/.oxfmtrc.json b/crates/oxc_language_server/fixtures/formatter/ignore-pattern/.oxfmtrc.json new file mode 100644 index 0000000000000..e081b18a36580 --- /dev/null +++ b/crates/oxc_language_server/fixtures/formatter/ignore-pattern/.oxfmtrc.json @@ -0,0 +1,4 @@ +{ + "semi": false, + "ignorePatterns": ["*.ts"] +} diff --git a/crates/oxc_language_server/fixtures/formatter/ignore-pattern/ignored.ts b/crates/oxc_language_server/fixtures/formatter/ignore-pattern/ignored.ts new file mode 100644 index 0000000000000..0c276cc2f93c3 --- /dev/null +++ b/crates/oxc_language_server/fixtures/formatter/ignore-pattern/ignored.ts @@ -0,0 +1,2 @@ +// semicolon is on character 8-9, which will be removed +debugger; diff --git a/crates/oxc_language_server/fixtures/formatter/ignore-pattern/not-ignored.js b/crates/oxc_language_server/fixtures/formatter/ignore-pattern/not-ignored.js new file mode 100644 index 0000000000000..0c276cc2f93c3 --- /dev/null +++ b/crates/oxc_language_server/fixtures/formatter/ignore-pattern/not-ignored.js @@ -0,0 +1,2 @@ +// semicolon is on character 8-9, which will be removed +debugger; diff --git a/crates/oxc_language_server/src/formatter/server_formatter.rs b/crates/oxc_language_server/src/formatter/server_formatter.rs index a3da1377c0eb1..fd5f4963fbcb9 100644 --- a/crates/oxc_language_server/src/formatter/server_formatter.rs +++ b/crates/oxc_language_server/src/formatter/server_formatter.rs @@ -1,5 +1,6 @@ use std::path::{Path, PathBuf}; +use ignore::gitignore::{Gitignore, GitignoreBuilder}; use log::{debug, warn}; use oxc_allocator::Allocator; use oxc_data_structures::rope::{Rope, get_line_column}; @@ -38,10 +39,29 @@ impl ServerFormatterBuilder { debug!("experimental formatter enabled"); } let root_path = root_uri.to_file_path().unwrap(); + let oxfmtrc = Self::get_config(&root_path, options.config_path.as_ref()); + + let gitignore_glob = if options.experimental { + match Self::create_ignore_globs( + &root_path, + oxfmtrc.ignore_patterns.as_deref().unwrap_or(&[]), + ) { + Ok(glob) => Some(glob), + Err(err) => { + warn!( + "Failed to create gitignore globs: {err}, proceeding without ignore globs" + ); + None + } + } + } else { + None + }; ServerFormatter::new( - Self::get_format_options(&root_path, options.config_path.as_ref()), + Self::get_format_options(oxfmtrc), options.experimental, + gitignore_glob, ) } } @@ -53,8 +73,8 @@ impl ToolBuilder for ServerFormatterBuilder { } impl ServerFormatterBuilder { - fn get_format_options(root_path: &Path, config_path: Option<&String>) -> FormatOptions { - let oxfmtrc = if let Some(config) = Self::search_config_file(root_path, config_path) { + fn get_config(root_path: &Path, config_path: Option<&String>) -> Oxfmtrc { + if let Some(config) = Self::search_config_file(root_path, config_path) { if let Ok(oxfmtrc) = Oxfmtrc::from_file(&config) { oxfmtrc } else { @@ -67,8 +87,9 @@ impl ServerFormatterBuilder { config_path.unwrap_or(&FORMAT_CONFIG_FILES.join(", ")) ); Oxfmtrc::default() - }; - + } + } + fn get_format_options(oxfmtrc: Oxfmtrc) -> FormatOptions { match oxfmtrc.into_format_options() { Ok(options) => options, Err(err) => { @@ -97,10 +118,30 @@ impl ServerFormatterBuilder { config.try_exists().is_ok_and(|exists| exists).then_some(config) }) } + + fn create_ignore_globs( + root_path: &Path, + ignore_patterns: &[String], + ) -> Result { + let mut builder = GitignoreBuilder::new(root_path); + for ignore_path in &load_ignore_paths(root_path) { + if builder.add(ignore_path).is_some() { + return Err(format!("Failed to add ignore file: {}", ignore_path.display())); + } + } + for pattern in ignore_patterns { + builder + .add_line(None, pattern) + .map_err(|e| format!("Invalid ignore pattern: {pattern}: {e}"))?; + } + + builder.build().map_err(|_| "Failed to build ignore globs".to_string()) + } } pub struct ServerFormatter { options: FormatOptions, should_run: bool, + gitignore_glob: Option, } impl Tool for ServerFormatter { @@ -209,6 +250,12 @@ impl Tool for ServerFormatter { } let path = uri.to_file_path()?; + + if self.is_ignored(&path) { + debug!("File is ignored: {}", path.display()); + return None; + } + let source_type = get_supported_source_type(&path).map(enable_jsx_source_type)?; // Declaring Variable to satisfy borrow checker let file_content; @@ -260,8 +307,24 @@ impl Tool for ServerFormatter { } impl ServerFormatter { - pub fn new(options: FormatOptions, should_run: bool) -> Self { - Self { options, should_run } + pub fn new( + options: FormatOptions, + should_run: bool, + gitignore_glob: Option, + ) -> Self { + Self { options, should_run, gitignore_glob } + } + + fn is_ignored(&self, path: &Path) -> bool { + if let Some(glob) = &self.gitignore_glob { + if !path.starts_with(glob.path()) { + return false; + } + + glob.matched_path_or_any_parents(path, path.is_dir()).is_ignore() + } else { + false + } } } @@ -306,6 +369,17 @@ fn compute_minimal_text_edit<'a>( (start, end, replacement) } +// Almost the same as `oxfmt::walk::load_ignore_paths`, but does not handle custom ignore files. +fn load_ignore_paths(cwd: &Path) -> Vec { + [".gitignore", ".prettierignore"] + .iter() + .filter_map(|file_name| { + let path = cwd.join(file_name); + if path.exists() { Some(path) } else { None } + }) + .collect::>() +} + #[cfg(test)] mod tests { use serde_json::json; @@ -427,4 +501,26 @@ mod tests { ) .format_and_snapshot_single_file("semicolons-as-needed.ts"); } + + #[test] + fn test_ignore_files() { + Tester::new( + "fixtures/formatter/ignore-file", + json!({ + "fmt.experimental": true + }), + ) + .format_and_snapshot_multiple_file(&["ignored.ts", "not-ignored.js"]); + } + + #[test] + fn test_ignore_pattern() { + Tester::new( + "fixtures/formatter/ignore-pattern", + json!({ + "fmt.experimental": true + }), + ) + .format_and_snapshot_multiple_file(&["ignored.ts", "not-ignored.js"]); + } } diff --git a/crates/oxc_language_server/src/formatter/snapshots/fixtures_formatter_ignore-file@ignored.ts_not-ignored.js.snap b/crates/oxc_language_server/src/formatter/snapshots/fixtures_formatter_ignore-file@ignored.ts_not-ignored.js.snap new file mode 100644 index 0000000000000..2efdf397ea62d --- /dev/null +++ b/crates/oxc_language_server/src/formatter/snapshots/fixtures_formatter_ignore-file@ignored.ts_not-ignored.js.snap @@ -0,0 +1,20 @@ +--- +source: crates/oxc_language_server/src/formatter/tester.rs +--- +======================================== +File: fixtures/formatter/ignore-file/ignored.ts +======================================== +File is ignored +======================================== +File: fixtures/formatter/ignore-file/not-ignored.js +======================================== +Range: Range { + start: Position { + line: 1, + character: 8, + }, + end: Position { + line: 1, + character: 9, + }, +} diff --git a/crates/oxc_language_server/src/formatter/snapshots/fixtures_formatter_ignore-pattern@ignored.ts_not-ignored.js.snap b/crates/oxc_language_server/src/formatter/snapshots/fixtures_formatter_ignore-pattern@ignored.ts_not-ignored.js.snap new file mode 100644 index 0000000000000..1f8e1625ff94c --- /dev/null +++ b/crates/oxc_language_server/src/formatter/snapshots/fixtures_formatter_ignore-pattern@ignored.ts_not-ignored.js.snap @@ -0,0 +1,20 @@ +--- +source: crates/oxc_language_server/src/formatter/tester.rs +--- +======================================== +File: fixtures/formatter/ignore-pattern/ignored.ts +======================================== +File is ignored +======================================== +File: fixtures/formatter/ignore-pattern/not-ignored.js +======================================== +Range: Range { + start: Position { + line: 1, + character: 8, + }, + end: Position { + line: 1, + character: 9, + }, +}