diff --git a/Cargo.lock b/Cargo.lock index 1419961f77f4b..aced3b9d55072 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2499,6 +2499,7 @@ dependencies = [ "bpaf", "cow-utils", "editorconfig-parser", + "fast-glob", "ignore", "json-strip-comments", "mimalloc-safe", diff --git a/apps/oxfmt/Cargo.toml b/apps/oxfmt/Cargo.toml index 44b6bb7f93461..93ae717e05046 100644 --- a/apps/oxfmt/Cargo.toml +++ b/apps/oxfmt/Cargo.toml @@ -39,6 +39,7 @@ oxc_span = { workspace = true } bpaf = { workspace = true, features = ["autocomplete", "bright-color", "derive"] } cow-utils = { workspace = true } editorconfig-parser = { workspace = true } +fast-glob = { workspace = true } ignore = { workspace = true, features = ["simd-accel"] } json-strip-comments = { workspace = true } miette = { workspace = true } diff --git a/apps/oxfmt/src/core/config.rs b/apps/oxfmt/src/core/config.rs index 167b09636dd79..11b46be3fba65 100644 --- a/apps/oxfmt/src/core/config.rs +++ b/apps/oxfmt/src/core/config.rs @@ -4,6 +4,7 @@ use editorconfig_parser::{ EditorConfig, EditorConfigProperties, EditorConfigProperty, EndOfLine, IndentStyle, MaxLineLength, }; +use fast_glob::glob_match; use serde_json::Value; use tracing::instrument; @@ -12,7 +13,10 @@ use oxc_toml::Options as TomlFormatterOptions; use super::{ FormatFileStrategy, - oxfmtrc::{EndOfLineConfig, FormatConfig, OxfmtOptions, Oxfmtrc, populate_prettier_config}, + oxfmtrc::{ + EndOfLineConfig, FormatConfig, OxfmtOptions, OxfmtOverrideConfig, Oxfmtrc, + populate_prettier_config, + }, utils, }; @@ -68,29 +72,40 @@ pub enum ResolvedOptions { }, } -/// Configuration resolver that derives all config values from a single `serde_json::Value`. +/// Configuration resolver to handle `.oxfmtrc` and `.editorconfig` files. /// -/// Priority order: `Oxfmtrc::default()` → `.editorconfig` → user's `.oxfmtrc` +/// Priority order: `Oxfmtrc::default()` → user's `.oxfmtrc` base → `.oxfmtrc` overrides +/// `.editorconfig` is applied as fallback for unset fields only. +#[derive(Debug)] pub struct ConfigResolver { /// User's raw config as JSON value. /// It contains every possible field, even those not recognized by `Oxfmtrc`. /// e.g. `printWidth`: recognized by both `Oxfmtrc` and Prettier /// e.g. `vueIndentScriptAndStyle`: not recognized by `Oxfmtrc`, but used by Prettier - /// e.g. `svelteSortAttributes`: not recognized by Prettier by default + /// e.g. `svelteSortAttributes`: not recognized by Prettier and require plugins raw_config: Value, - /// Parsed `.editorconfig`, if any. - editorconfig: Option, + /// Directory containing the config file (for relative path resolution in overrides). + config_dir: Option, /// Cached parsed options after validation. - /// Used to avoid re-parsing during per-file resolution, if `.editorconfig` is not used. - /// NOTE: Currently, only `.editorconfig` provides per-file overrides, `.oxfmtrc` does not. + /// Used to avoid re-parsing during per-file resolution, if no per-file overrides exist. cached_options: Option<(OxfmtOptions, Value)>, + /// Resolved overrides from `.oxfmtrc` for file-specific matching. + oxfmtrc_overrides: Option, + /// Parsed `.editorconfig`, if any. + editorconfig: Option, } impl ConfigResolver { /// Create a new resolver from a raw JSON config value. #[cfg(feature = "napi")] pub fn from_value(raw_config: Value) -> Self { - Self { raw_config, editorconfig: None, cached_options: None } + Self { + raw_config, + config_dir: None, + cached_options: None, + oxfmtrc_overrides: None, + editorconfig: None, + } } /// Create a resolver by loading config from a file path. @@ -123,6 +138,8 @@ impl ConfigResolver { // Parse as raw JSON value let raw_config: Value = serde_json::from_str(&json_string) .map_err(|err| format!("Failed to parse config: {err}"))?; + // Store the config directory for override path resolution + let config_dir = oxfmtrc_path.and_then(|p| p.parent().map(Path::to_path_buf)); let editorconfig = match editorconfig_path { Some(path) => { @@ -136,13 +153,18 @@ impl ConfigResolver { None => None, }; - Ok(Self { raw_config, editorconfig, cached_options: None }) + Ok(Self { + raw_config, + config_dir, + cached_options: None, + oxfmtrc_overrides: None, + editorconfig, + }) } /// Validate config and return ignore patterns (= non-formatting option) for file walking. /// /// Validated options are cached for fast path resolution. - /// See also [`ConfigResolver::resolve_with_editorconfig_overrides`] for per-file overrides. /// /// # Errors /// Returns error if config deserialization fails. @@ -151,6 +173,11 @@ impl ConfigResolver { let oxfmtrc: Oxfmtrc = serde_json::from_value(self.raw_config.clone()) .map_err(|err| format!("Failed to deserialize Oxfmtrc: {err}"))?; + // Resolve `overrides` from `Oxfmtrc` for later per-file matching + let base_dir = self.config_dir.take(); + self.oxfmtrc_overrides = + oxfmtrc.overrides.map(|overrides| OxfmtrcOverrides::new(overrides, base_dir)); + let mut format_config = oxfmtrc.format_config; // If `.editorconfig` is used, apply its root section first @@ -185,19 +212,7 @@ impl ConfigResolver { /// Resolve format options for a specific file. #[instrument(level = "debug", name = "oxfmt::config::resolve", skip_all, fields(path = %strategy.path().display()))] pub fn resolve(&self, strategy: &FormatFileStrategy) -> ResolvedOptions { - let (oxfmt_options, external_options) = if let Some(editorconfig) = &self.editorconfig - && let Some(props) = get_editorconfig_overrides(editorconfig, strategy.path()) - { - self.resolve_with_editorconfig_overrides(&props) - } else { - // Fast path: no per-file overrides - // Either: - // - `.editorconfig` is NOT used - // - or used but per-file overrides do NOT exist for this file - self.cached_options - .clone() - .expect("`build_and_validate()` must be called before `resolve()`") - }; + let (oxfmt_options, external_options) = self.resolve_options(strategy.path()); #[cfg(feature = "napi")] let OxfmtOptions { format_options, toml_options, sort_package_json, insert_final_newline } = @@ -233,26 +248,41 @@ impl ConfigResolver { } } - /// Resolve format options for a specific file with `.editorconfig` overrides. - /// This is the slow path, for fast path, see [`ConfigResolver::build_and_validate`]. - /// Also main logics are the same as in `build_and_validate()`. - #[instrument(level = "debug", name = "oxfmt::config::resolve_with_overrides", skip_all)] - fn resolve_with_editorconfig_overrides( - &self, - props: &EditorConfigProperties, - ) -> (OxfmtOptions, Value) { - // NOTE: Deserialize `FormatConfig` from `raw_config` (not from cached options). - // If we base it on cached options, root section may be already applied, - // so `.is_some()` checks won't work and per-file overrides may not be applied. - // And `props` already has root section applied. + /// Resolve options for a specific file path. + /// Priority: oxfmtrc base → oxfmtrc overrides → editorconfig (fallback for unset fields) + fn resolve_options(&self, path: &Path) -> (OxfmtOptions, Value) { + let editorconfig_overrides = + self.editorconfig.as_ref().and_then(|ec| get_editorconfig_overrides(ec, path)); + let has_oxfmtrc_overrides = + self.oxfmtrc_overrides.as_ref().is_some_and(|o| o.has_match(path)); + + // Fast path: no per-file overrides + if editorconfig_overrides.is_none() && !has_oxfmtrc_overrides { + return self + .cached_options + .clone() + .expect("`build_and_validate()` must be called first"); + } + + // Slow path: reconstruct `FormatConfig` to apply overrides let mut format_config: FormatConfig = serde_json::from_value(self.raw_config.clone()) - .expect("`build_and_validate()` should catch this before `resolve()`"); + .expect("`build_and_validate()` should catch this before"); + + // Apply oxfmtrc overrides first (explicit settings) + if let Some(overrides) = &self.oxfmtrc_overrides { + for options in overrides.get_matching(path) { + format_config.merge(options); + } + } - apply_editorconfig(&mut format_config, props); + // Apply editorconfig as fallback (fills in unset fields only) + if let Some(props) = &editorconfig_overrides { + apply_editorconfig(&mut format_config, props); + } let oxfmt_options = format_config .into_oxfmt_options() - .expect("If this fails, there is an issue with editorconfig insertion above"); + .expect("If this fails, there is an issue with override values"); let mut external_options = self.raw_config.clone(); populate_prettier_config(&oxfmt_options.format_options, &mut external_options); @@ -263,6 +293,75 @@ impl ConfigResolver { // --- +/// Resolved overrides from `.oxfmtrc` for file-specific matching. +/// Similar to `EditorConfig`, this handles `FormatConfig` override resolution. +#[derive(Debug)] +struct OxfmtrcOverrides { + base_dir: Option, + entries: Vec, +} + +impl OxfmtrcOverrides { + fn new(overrides: Vec, base_dir: Option) -> Self { + // Normalize glob patterns by adding `**/` prefix to patterns without `/`. + // This matches ESLint/Prettier behavior. + let normalize_patterns = |patterns: Vec| { + patterns + .into_iter() + .map(|pat| if pat.contains('/') { pat } else { format!("**/{pat}") }) + .collect() + }; + + Self { + base_dir, + entries: overrides + .into_iter() + .map(|o| OxfmtrcOverrideEntry { + files: normalize_patterns(o.files), + exclude_files: o.exclude_files.map(normalize_patterns).unwrap_or_default(), + options: o.options, + }) + .collect(), + } + } + + /// Check if any overrides exist that match the given path. + fn has_match(&self, path: &Path) -> bool { + let relative = self.relative_path(path); + self.entries.iter().any(|e| Self::is_entry_match(e, &relative)) + } + + /// Get all matching override options for a given path. + fn get_matching(&self, path: &Path) -> impl Iterator + '_ { + let relative = self.relative_path(path); + self.entries.iter().filter(move |e| Self::is_entry_match(e, &relative)).map(|e| &e.options) + } + + fn relative_path(&self, path: &Path) -> String { + self.base_dir + .as_ref() + .and_then(|dir| path.strip_prefix(dir).ok()) + .unwrap_or(path) + .to_string_lossy() + .into_owned() + } + + fn is_entry_match(entry: &OxfmtrcOverrideEntry, relative: &str) -> bool { + entry.files.iter().any(|glob| glob_match(glob, relative)) + && !entry.exclude_files.iter().any(|glob| glob_match(glob, relative)) + } +} + +/// A single override entry with normalized glob patterns. +#[derive(Debug)] +struct OxfmtrcOverrideEntry { + files: Vec, + exclude_files: Vec, + options: FormatConfig, +} + +// --- + /// Check if `.editorconfig` has per-file overrides for this path. /// /// Returns `Some(props)` if the resolved properties differ from the root `[*]` section. @@ -315,9 +414,7 @@ fn get_editorconfig_overrides( /// Apply `.editorconfig` properties to `FormatConfig`. /// /// Only applies values that are not already set in the user's config. -/// Priority: `FormatConfig` default < `.editorconfig` < user's `FormatConfig` -/// -/// Only properties checked by [`get_editorconfig_overrides`] are applied here. +/// NOTE: Only properties checked by [`get_editorconfig_overrides`] are applied here. fn apply_editorconfig(config: &mut FormatConfig, props: &EditorConfigProperties) { #[expect(clippy::cast_possible_truncation)] if config.print_width.is_none() diff --git a/apps/oxfmt/src/core/oxfmtrc.rs b/apps/oxfmt/src/core/oxfmtrc.rs index 6eb5543da43fc..dea4ebf537fe6 100644 --- a/apps/oxfmt/src/core/oxfmtrc.rs +++ b/apps/oxfmt/src/core/oxfmtrc.rs @@ -19,7 +19,12 @@ use oxc_toml::Options as TomlFormatterOptions; pub struct Oxfmtrc { #[serde(flatten)] pub format_config: FormatConfig, - // TODO: `overrides` + /// File-specific overrides. + /// When a file matches multiple overrides, the later override takes precedence (array order matters). + /// + /// - Default: `[]` + #[serde(skip_serializing_if = "Option::is_none")] + pub overrides: Option>, /// Ignore files matching these glob patterns. /// Patterns are based on the location of the Oxfmt configuration file. /// @@ -30,6 +35,22 @@ pub struct Oxfmtrc { // --- +#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct OxfmtOverrideConfig { + /// Glob patterns to match files for this override. + /// All patterns are relative to the Oxfmt configuration file. + pub files: Vec, + /// Glob patterns to exclude from this override. + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude_files: Option>, + /// Format options to apply for matched files. + #[serde(default)] + pub options: FormatConfig, +} + +// --- + // NOTE: All fields are typed as `Option` to distinguish between user-specified values and defaults. #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", default)] @@ -1025,6 +1046,9 @@ pub fn populate_prettier_config(options: &FormatOptions, config: &mut Value) { }), ); + // Already handled by Oxfmt + obj.remove("overrides"); + // Below are our own extensions, just remove them obj.remove("ignorePatterns"); obj.remove("insertFinalNewline"); @@ -1284,6 +1308,58 @@ mod tests_populate_prettier_config { assert!(!obj.contains_key("ignorePatterns")); assert!(!obj.contains_key("experimentalSortImports")); } + + #[test] + fn test_overrides_parsing() { + let json = r#"{ + "tabWidth": 2, + "overrides": [ + { + "files": ["*.test.js"], + "options": { "tabWidth": 4 } + }, + { + "files": ["*.md", "*.html"], + "excludeFiles": ["*.min.js"], + "options": { "printWidth": 80 } + } + ] + }"#; + + let config: Oxfmtrc = serde_json::from_str(json).unwrap(); + assert!(config.overrides.is_some()); + + let overrides = config.overrides.unwrap(); + assert_eq!(overrides.len(), 2); + + // First override: single file pattern + assert_eq!(overrides[0].files, vec!["*.test.js"]); + assert!(overrides[0].exclude_files.is_none()); + assert_eq!(overrides[0].options.tab_width, Some(4)); + + // Second override: multiple file patterns with exclude + assert_eq!(overrides[1].files, vec!["*.md", "*.html"]); + assert_eq!(overrides[1].exclude_files, Some(vec!["*.min.js".to_string()])); + assert_eq!(overrides[1].options.print_width, Some(80)); + } + + #[test] + fn test_populate_prettier_config_removes_overrides() { + let json_string = r#"{ + "tabWidth": 2, + "overrides": [ + { "files": ["*.test.js"], "options": { "tabWidth": 4 } } + ] + }"#; + let mut raw_config: Value = serde_json::from_str(json_string).unwrap(); + let oxfmtrc: Oxfmtrc = serde_json::from_str(json_string).unwrap(); + let oxfmt_options = oxfmtrc.format_config.into_oxfmt_options().unwrap(); + + populate_prettier_config(&oxfmt_options.format_options, &mut raw_config); + + let obj = raw_config.as_object().unwrap(); + assert!(!obj.contains_key("overrides")); + } } #[cfg(test)] @@ -1309,7 +1385,7 @@ mod tests_json_deep_merge { json!({ "experimentalSortImports": { "order": "desc", "ignoreCase": true } }) ); - // Null resets to default (becomes null in JSON, then None in Option) + // Null overwrites value (but in practice, None is skipped during serialization) let base = json!({ "semi": false, "tabWidth": 4 }); let overlay = json!({ "semi": null }); let merged = json_deep_merge(base, overlay); diff --git a/npm/oxfmt/configuration_schema.json b/npm/oxfmt/configuration_schema.json index f13876bed2eee..693878cc7ce1c 100644 --- a/npm/oxfmt/configuration_schema.json +++ b/npm/oxfmt/configuration_schema.json @@ -143,6 +143,17 @@ ], "markdownDescription": "How to wrap object literals when they could fit on one line or span multiple lines.\n\nBy default, formats objects as multi-line if there is a newline prior to the first property.\nAuthors can use this heuristic to contextually improve readability, though it has some downsides.\n\n- Default: `\"preserve\"`" }, + "overrides": { + "description": "File-specific overrides.\nWhen a file matches multiple overrides, the later override takes precedence (array order matters).\n\n- Default: `[]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/OxfmtOverrideConfig" + }, + "markdownDescription": "File-specific overrides.\nWhen a file matches multiple overrides, the later override takes precedence (array order matters).\n\n- Default: `[]`" + }, "printWidth": { "description": "Specify the line length that the printer will wrap on.\n\nIf you don't want line wrapping when formatting Markdown, you can set the `proseWrap` option to disable it.\n\n- Default: `100`\n- Overrides `.editorconfig.max_line_length`", "type": [ @@ -285,6 +296,235 @@ "cr" ] }, + "FormatConfig": { + "type": "object", + "properties": { + "arrowParens": { + "description": "Include parentheses around a sole arrow function parameter.\n\n- Default: `\"always\"`", + "anyOf": [ + { + "$ref": "#/definitions/ArrowParensConfig" + }, + { + "type": "null" + } + ], + "markdownDescription": "Include parentheses around a sole arrow function parameter.\n\n- Default: `\"always\"`" + }, + "bracketSameLine": { + "description": "Put the `>` of a multi-line HTML (HTML, JSX, Vue, Angular) element at the end of the last line,\ninstead of being alone on the next line (does not apply to self closing elements).\n\n- Default: `false`", + "type": [ + "boolean", + "null" + ], + "markdownDescription": "Put the `>` of a multi-line HTML (HTML, JSX, Vue, Angular) element at the end of the last line,\ninstead of being alone on the next line (does not apply to self closing elements).\n\n- Default: `false`" + }, + "bracketSpacing": { + "description": "Print spaces between brackets in object literals.\n\n- Default: `true`", + "type": [ + "boolean", + "null" + ], + "markdownDescription": "Print spaces between brackets in object literals.\n\n- Default: `true`" + }, + "embeddedLanguageFormatting": { + "description": "Control whether to format embedded parts (For example, CSS-in-JS, or JS-in-Vue, etc.) in the file.\n\nNOTE: XXX-in-JS support is incomplete.\nJS-in-XXX is fully supported but still be handled by Prettier.\n\n- Default: `\"auto\"`", + "anyOf": [ + { + "$ref": "#/definitions/EmbeddedLanguageFormattingConfig" + }, + { + "type": "null" + } + ], + "markdownDescription": "Control whether to format embedded parts (For example, CSS-in-JS, or JS-in-Vue, etc.) in the file.\n\nNOTE: XXX-in-JS support is incomplete.\nJS-in-XXX is fully supported but still be handled by Prettier.\n\n- Default: `\"auto\"`" + }, + "endOfLine": { + "description": "Which end of line characters to apply.\n\nNOTE: `\"auto\"` is not supported.\n\n- Default: `\"lf\"`\n- Overrides `.editorconfig.end_of_line`", + "anyOf": [ + { + "$ref": "#/definitions/EndOfLineConfig" + }, + { + "type": "null" + } + ], + "markdownDescription": "Which end of line characters to apply.\n\nNOTE: `\"auto\"` is not supported.\n\n- Default: `\"lf\"`\n- Overrides `.editorconfig.end_of_line`" + }, + "experimentalSortImports": { + "description": "Experimental: Sort import statements.\n\nUsing the similar algorithm as [eslint-plugin-perfectionist/sort-imports](https://perfectionist.dev/rules/sort-imports).\nFor details, see each field's documentation.\n\n- Default: Disabled", + "anyOf": [ + { + "$ref": "#/definitions/SortImportsConfig" + }, + { + "type": "null" + } + ], + "markdownDescription": "Experimental: Sort import statements.\n\nUsing the similar algorithm as [eslint-plugin-perfectionist/sort-imports](https://perfectionist.dev/rules/sort-imports).\nFor details, see each field's documentation.\n\n- Default: Disabled" + }, + "experimentalSortPackageJson": { + "description": "Experimental: Sort `package.json` keys.\n\nThe algorithm is NOT compatible with [prettier-plugin-sort-packagejson](https://github.com/matzkoh/prettier-plugin-packagejson).\nBut we believe it is clearer and easier to navigate.\nFor details, see each field's documentation.\n\n- Default: `true`", + "anyOf": [ + { + "$ref": "#/definitions/SortPackageJsonUserConfig" + }, + { + "type": "null" + } + ], + "markdownDescription": "Experimental: Sort `package.json` keys.\n\nThe algorithm is NOT compatible with [prettier-plugin-sort-packagejson](https://github.com/matzkoh/prettier-plugin-packagejson).\nBut we believe it is clearer and easier to navigate.\nFor details, see each field's documentation.\n\n- Default: `true`" + }, + "experimentalTailwindcss": { + "description": "Experimental: Sort Tailwind CSS classes.\n\nUsing the same algorithm as [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss).\nOption names omit the `tailwind` prefix used in the original plugin (e.g., `config` instead of `tailwindConfig`).\nFor details, see each field's documentation.\n\n- Default: Disabled", + "anyOf": [ + { + "$ref": "#/definitions/TailwindcssConfig" + }, + { + "type": "null" + } + ], + "markdownDescription": "Experimental: Sort Tailwind CSS classes.\n\nUsing the same algorithm as [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss).\nOption names omit the `tailwind` prefix used in the original plugin (e.g., `config` instead of `tailwindConfig`).\nFor details, see each field's documentation.\n\n- Default: Disabled" + }, + "htmlWhitespaceSensitivity": { + "description": "Specify the global whitespace sensitivity for HTML, Vue, Angular, and Handlebars.\n\n- Default: `\"css\"`", + "anyOf": [ + { + "$ref": "#/definitions/HtmlWhitespaceSensitivityConfig" + }, + { + "type": "null" + } + ], + "markdownDescription": "Specify the global whitespace sensitivity for HTML, Vue, Angular, and Handlebars.\n\n- Default: `\"css\"`" + }, + "insertFinalNewline": { + "description": "Whether to insert a final newline at the end of the file.\n\n- Default: `true`\n- Overrides `.editorconfig.insert_final_newline`", + "type": [ + "boolean", + "null" + ], + "markdownDescription": "Whether to insert a final newline at the end of the file.\n\n- Default: `true`\n- Overrides `.editorconfig.insert_final_newline`" + }, + "jsxSingleQuote": { + "description": "Use single quotes instead of double quotes in JSX.\n\n- Default: `false`", + "type": [ + "boolean", + "null" + ], + "markdownDescription": "Use single quotes instead of double quotes in JSX.\n\n- Default: `false`" + }, + "objectWrap": { + "description": "How to wrap object literals when they could fit on one line or span multiple lines.\n\nBy default, formats objects as multi-line if there is a newline prior to the first property.\nAuthors can use this heuristic to contextually improve readability, though it has some downsides.\n\n- Default: `\"preserve\"`", + "anyOf": [ + { + "$ref": "#/definitions/ObjectWrapConfig" + }, + { + "type": "null" + } + ], + "markdownDescription": "How to wrap object literals when they could fit on one line or span multiple lines.\n\nBy default, formats objects as multi-line if there is a newline prior to the first property.\nAuthors can use this heuristic to contextually improve readability, though it has some downsides.\n\n- Default: `\"preserve\"`" + }, + "printWidth": { + "description": "Specify the line length that the printer will wrap on.\n\nIf you don't want line wrapping when formatting Markdown, you can set the `proseWrap` option to disable it.\n\n- Default: `100`\n- Overrides `.editorconfig.max_line_length`", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0, + "markdownDescription": "Specify the line length that the printer will wrap on.\n\nIf you don't want line wrapping when formatting Markdown, you can set the `proseWrap` option to disable it.\n\n- Default: `100`\n- Overrides `.editorconfig.max_line_length`" + }, + "proseWrap": { + "description": "How to wrap prose.\n\nBy default, formatter will not change wrapping in markdown text since some services use a linebreak-sensitive renderer, e.g. GitHub comments and BitBucket.\nTo wrap prose to the print width, change this option to \"always\".\nIf you want to force all prose blocks to be on a single line and rely on editor/viewer soft wrapping instead, you can use \"never\".\n\n- Default: `\"preserve\"`", + "anyOf": [ + { + "$ref": "#/definitions/ProseWrapConfig" + }, + { + "type": "null" + } + ], + "markdownDescription": "How to wrap prose.\n\nBy default, formatter will not change wrapping in markdown text since some services use a linebreak-sensitive renderer, e.g. GitHub comments and BitBucket.\nTo wrap prose to the print width, change this option to \"always\".\nIf you want to force all prose blocks to be on a single line and rely on editor/viewer soft wrapping instead, you can use \"never\".\n\n- Default: `\"preserve\"`" + }, + "quoteProps": { + "description": "Change when properties in objects are quoted.\n\n- Default: `\"as-needed\"`", + "anyOf": [ + { + "$ref": "#/definitions/QuotePropsConfig" + }, + { + "type": "null" + } + ], + "markdownDescription": "Change when properties in objects are quoted.\n\n- Default: `\"as-needed\"`" + }, + "semi": { + "description": "Print semicolons at the ends of statements.\n\n- Default: `true`", + "type": [ + "boolean", + "null" + ], + "markdownDescription": "Print semicolons at the ends of statements.\n\n- Default: `true`" + }, + "singleAttributePerLine": { + "description": "Enforce single attribute per line in HTML, Vue, and JSX.\n\n- Default: `false`", + "type": [ + "boolean", + "null" + ], + "markdownDescription": "Enforce single attribute per line in HTML, Vue, and JSX.\n\n- Default: `false`" + }, + "singleQuote": { + "description": "Use single quotes instead of double quotes.\n\nFor JSX, you can set the `jsxSingleQuote` option.\n\n- Default: `false`", + "type": [ + "boolean", + "null" + ], + "markdownDescription": "Use single quotes instead of double quotes.\n\nFor JSX, you can set the `jsxSingleQuote` option.\n\n- Default: `false`" + }, + "tabWidth": { + "description": "Specify the number of spaces per indentation-level.\n\n- Default: `2`\n- Overrides `.editorconfig.indent_size`", + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0, + "markdownDescription": "Specify the number of spaces per indentation-level.\n\n- Default: `2`\n- Overrides `.editorconfig.indent_size`" + }, + "trailingComma": { + "description": "Print trailing commas wherever possible in multi-line comma-separated syntactic structures.\n\nA single-line array, for example, never gets trailing commas.\n\n- Default: `\"all\"`", + "anyOf": [ + { + "$ref": "#/definitions/TrailingCommaConfig" + }, + { + "type": "null" + } + ], + "markdownDescription": "Print trailing commas wherever possible in multi-line comma-separated syntactic structures.\n\nA single-line array, for example, never gets trailing commas.\n\n- Default: `\"all\"`" + }, + "useTabs": { + "description": "Indent lines with tabs instead of spaces.\n\n- Default: `false`\n- Overrides `.editorconfig.indent_style`", + "type": [ + "boolean", + "null" + ], + "markdownDescription": "Indent lines with tabs instead of spaces.\n\n- Default: `false`\n- Overrides `.editorconfig.indent_style`" + }, + "vueIndentScriptAndStyle": { + "description": "Whether or not to indent the code inside `