diff --git a/.dprint.json b/.dprint.json index fcba86c7b9745e..56397dfd93a7f6 100644 --- a/.dprint.json +++ b/.dprint.json @@ -24,6 +24,7 @@ "cli/tests/testdata/badly_formatted.md", "cli/tests/testdata/badly_formatted.json", "cli/tests/testdata/byte_order_mark.ts", + "cli/tests/testdata/fmt/*", "cli/tsc/*typescript.js", "test_util/std", "test_util/wpt", diff --git a/cli/config_file.rs b/cli/config_file.rs index 94373334a24372..dd002ca97ab8a7 100644 --- a/cli/config_file.rs +++ b/cli/config_file.rs @@ -277,7 +277,7 @@ pub struct LintRulesConfig { #[derive(Clone, Debug, Default, Deserialize)] #[serde(default, deny_unknown_fields)] -pub struct LintFilesConfig { +pub struct FilesConfig { pub include: Vec, pub exclude: Vec, } @@ -286,7 +286,32 @@ pub struct LintFilesConfig { #[serde(default, deny_unknown_fields)] pub struct LintConfig { pub rules: LintRulesConfig, - pub files: LintFilesConfig, + pub files: FilesConfig, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub enum ProseWrap { + Always, + Never, + Preserve, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(default, deny_unknown_fields, rename_all = "camelCase")] +pub struct FmtOptionsConfig { + pub use_tabs: Option, + pub line_width: Option, + pub indent_width: Option, + pub single_quote: Option, + pub prose_wrap: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct FmtConfig { + pub options: FmtOptionsConfig, + pub files: FilesConfig, } #[derive(Clone, Debug, Deserialize)] @@ -294,6 +319,7 @@ pub struct LintConfig { pub struct ConfigFileJson { pub compiler_options: Option, pub lint: Option, + pub fmt: Option, } #[derive(Clone, Debug)] @@ -374,6 +400,16 @@ impl ConfigFile { Ok(None) } } + + pub fn to_fmt_config(&self) -> Result, AnyError> { + if let Some(config) = self.json.fmt.clone() { + let fmt_config: FmtConfig = serde_json::from_value(config) + .context("Failed to parse \"fmt\" configuration")?; + Ok(Some(fmt_config)) + } else { + Ok(None) + } + } } #[cfg(test)] @@ -441,6 +477,19 @@ mod tests { "tags": ["recommended"], "include": ["ban-untagged-todo"] } + }, + "fmt": { + "files": { + "include": ["src/"], + "exclude": ["src/testdata/"] + }, + "options": { + "useTabs": true, + "lineWidth": 80, + "indentWidth": 4, + "singleQuote": true, + "proseWrap": "preserve" + } } }"#; let config_path = PathBuf::from("/deno/tsconfig.json"); @@ -474,6 +523,17 @@ mod tests { Some(vec!["recommended".to_string()]) ); assert!(lint_config.rules.exclude.is_none()); + + let fmt_config = config_file + .to_fmt_config() + .expect("error parsing fmt object") + .expect("fmt object should be defined"); + assert_eq!(fmt_config.files.include, vec!["src/"]); + assert_eq!(fmt_config.files.exclude, vec!["src/testdata/"]); + assert_eq!(fmt_config.options.use_tabs, Some(true)); + assert_eq!(fmt_config.options.line_width, Some(80)); + assert_eq!(fmt_config.options.indent_width, Some(4)); + assert_eq!(fmt_config.options.single_quote, Some(true)); } #[test] diff --git a/cli/flags.rs b/cli/flags.rs index 8bfcbb3807f12e..3a84c9c5a72fba 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -815,6 +815,7 @@ Ignore formatting a file by adding an ignore comment at the top of the file: // deno-fmt-ignore-file", ) + .arg(config_arg()) .arg( Arg::with_name("check") .long("check") @@ -1733,6 +1734,7 @@ fn eval_parse(flags: &mut Flags, matches: &clap::ArgMatches) { } fn fmt_parse(flags: &mut Flags, matches: &clap::ArgMatches) { + config_arg_parse(flags, matches); flags.watch = matches.is_present("watch"); let files = match matches.values_of("files") { Some(f) => f.map(PathBuf::from).collect(), @@ -2535,6 +2537,44 @@ mod tests { ..Flags::default() } ); + + let r = flags_from_vec(svec!["deno", "fmt", "--config", "deno.jsonc"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Fmt(FmtFlags { + ignore: vec![], + check: false, + files: vec![], + ext: "ts".to_string() + }), + config_path: Some("deno.jsonc".to_string()), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec![ + "deno", + "fmt", + "--config", + "deno.jsonc", + "--watch", + "foo.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Fmt(FmtFlags { + ignore: vec![], + check: false, + files: vec![PathBuf::from("foo.ts")], + ext: "ts".to_string() + }), + config_path: Some("deno.jsonc".to_string()), + watch: true, + ..Flags::default() + } + ); } #[test] diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index f2b03a0aaf6ff1..f1ff61cff05a46 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -335,15 +335,15 @@ impl Inner { Ok(navigation_tree) } - fn merge_user_tsconfig( - &mut self, - maybe_config: &Option, - maybe_root_uri: &Option, - tsconfig: &mut TsConfig, - ) -> Result<(), AnyError> { - self.maybe_config_file = None; - self.maybe_config_uri = None; - if let Some(config_str) = maybe_config { + /// Returns a tuple with parsed `ConfigFile` and `Url` pointing to that file. + /// If there's no config file specified in settings returns `None`. + fn get_config_file_and_url( + &self, + ) -> Result, AnyError> { + let workspace_settings = self.config.get_workspace_settings(); + let maybe_root_uri = self.config.root_uri.clone(); + let maybe_config = workspace_settings.config; + if let Some(config_str) = &maybe_config { if !config_str.is_empty() { info!("Setting TypeScript configuration from: \"{}\"", config_str); let config_url = if let Ok(url) = Url::from_file_path(config_str) { @@ -373,18 +373,34 @@ impl Inner { .ok_or_else(|| anyhow!("Bad uri: \"{}\"", config_url))?; ConfigFile::read(path)? }; - let (value, maybe_ignored_options) = - config_file.to_compiler_options()?; - tsconfig.merge(&value); - self.maybe_config_file = Some(config_file); - self.maybe_config_uri = Some(config_url); - if let Some(ignored_options) = maybe_ignored_options { - // TODO(@kitsonk) turn these into diagnostics that can be sent to the - // client - warn!("{}", ignored_options); - } + return Ok(Some((config_file, config_url))); + } + } + + Ok(None) + } + + fn merge_user_tsconfig( + &mut self, + tsconfig: &mut TsConfig, + ) -> Result<(), AnyError> { + self.maybe_config_file = None; + self.maybe_config_uri = None; + + let maybe_file_and_url = self.get_config_file_and_url()?; + + if let Some((config_file, config_url)) = maybe_file_and_url { + let (value, maybe_ignored_options) = config_file.to_compiler_options()?; + tsconfig.merge(&value); + self.maybe_config_file = Some(config_file); + self.maybe_config_uri = Some(config_url); + if let Some(ignored_options) = maybe_ignored_options { + // TODO(@kitsonk) turn these into diagnostics that can be sent to the + // client + warn!("{}", ignored_options); } } + Ok(()) } @@ -566,20 +582,15 @@ impl Inner { // TODO(@kitsonk) remove for Deno 1.15 "useUnknownInCatchVariables": false, })); - let (maybe_config, maybe_root_uri) = { - let config = &self.config; - let workspace_settings = config.get_workspace_settings(); - if workspace_settings.unstable { - let unstable_libs = json!({ - "lib": ["deno.ns", "deno.window", "deno.unstable"] - }); - tsconfig.merge(&unstable_libs); - } - (workspace_settings.config, config.root_uri.clone()) - }; - if let Err(err) = - self.merge_user_tsconfig(&maybe_config, &maybe_root_uri, &mut tsconfig) - { + let config = &self.config; + let workspace_settings = config.get_workspace_settings(); + if workspace_settings.unstable { + let unstable_libs = json!({ + "lib": ["deno.ns", "deno.window", "deno.unstable"] + }); + tsconfig.merge(&unstable_libs); + } + if let Err(err) = self.merge_user_tsconfig(&mut tsconfig) { self.client.show_message(MessageType::Warning, err).await; } let _ok: bool = self @@ -1006,14 +1017,37 @@ impl Inner { PathBuf::from(params.text_document.uri.path()) }; + let maybe_file_and_url = self.get_config_file_and_url().map_err(|err| { + error!("Unable to parse configuration file: {}", err); + LspError::internal_error() + })?; + + let fmt_options = if let Some((config_file, _)) = maybe_file_and_url { + config_file + .to_fmt_config() + .map_err(|err| { + error!("Unable to parse fmt configuration: {}", err); + LspError::internal_error() + })? + .unwrap_or_default() + } else { + Default::default() + }; + let source = document_data.source().clone(); let text_edits = tokio::task::spawn_blocking(move || { let format_result = match source.module() { - Some(Ok(parsed_module)) => Ok(format_parsed_module(parsed_module)), + Some(Ok(parsed_module)) => { + Ok(format_parsed_module(parsed_module, fmt_options.options)) + } Some(Err(err)) => Err(err.to_string()), None => { // it's not a js/ts file, so attempt to format its contents - format_file(&file_path, source.text_info().text_str()) + format_file( + &file_path, + source.text_info().text_str(), + fmt_options.options, + ) } }; diff --git a/cli/main.rs b/cli/main.rs index 985bf68dd019fd..8d7ab86ced144c 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -806,8 +806,20 @@ async fn format_command( flags: Flags, fmt_flags: FmtFlags, ) -> Result<(), AnyError> { + let program_state = ProgramState::build(flags.clone()).await?; + let maybe_fmt_config = + if let Some(config_file) = &program_state.maybe_config_file { + config_file.to_fmt_config()? + } else { + None + }; + if fmt_flags.files.len() == 1 && fmt_flags.files[0].to_string_lossy() == "-" { - return tools::fmt::format_stdin(fmt_flags.check, fmt_flags.ext); + return tools::fmt::format_stdin( + fmt_flags.check, + fmt_flags.ext, + maybe_fmt_config.map(|c| c.options).unwrap_or_default(), + ); } tools::fmt::format( @@ -815,6 +827,7 @@ async fn format_command( fmt_flags.ignore, fmt_flags.check, flags.watch, + maybe_fmt_config, ) .await?; Ok(()) diff --git a/cli/tests/integration/fmt_tests.rs b/cli/tests/integration/fmt_tests.rs index 00565a5d04d611..2d7451694b4965 100644 --- a/cli/tests/integration/fmt_tests.rs +++ b/cli/tests/integration/fmt_tests.rs @@ -129,25 +129,25 @@ fn fmt_ignore_unexplicit_files() { } itest!(fmt_check_tests_dir { - args: "fmt --check ./ --ignore=.test_coverage", + args: "fmt --check ./ --ignore=.test_coverage,fmt/fmt_with_config/", output: "fmt/expected_fmt_check_tests_dir.out", exit_code: 1, }); itest!(fmt_quiet_check_fmt_dir { - args: "fmt --check --quiet fmt/", + args: "fmt --check --quiet fmt/regular/", output_str: Some(""), exit_code: 0, }); itest!(fmt_check_formatted_files { - args: "fmt --check fmt/formatted1.js fmt/formatted2.ts fmt/formatted3.md fmt/formatted4.jsonc", + args: "fmt --check fmt/regular/formatted1.js fmt/regular/formatted2.ts fmt/regular/formatted3.md fmt/regular/formatted4.jsonc", output: "fmt/expected_fmt_check_formatted_files.out", exit_code: 0, }); itest!(fmt_check_ignore { - args: "fmt --check --ignore=fmt/formatted1.js fmt/", + args: "fmt --check --ignore=fmt/regular/formatted1.js fmt/regular/", output: "fmt/expected_fmt_check_ignore.out", exit_code: 0, }); @@ -181,3 +181,26 @@ itest!(fmt_stdin_check_not_formatted { input: Some("const a = 1\n"), output_str: Some("Not formatted stdin\n"), }); + +itest!(fmt_with_config { + args: "fmt --config fmt/deno.jsonc fmt/fmt_with_config/", + output: "fmt/fmt_with_config.out", +}); + +// Check if CLI flags take precedence +itest!(fmt_with_config_and_flags { + args: "fmt --config fmt/deno.jsonc --ignore=fmt/fmt_with_config/a.ts,fmt/fmt_with_config/b.ts", + output: "fmt/fmt_with_config_and_flags.out", +}); + +itest!(fmt_with_malformed_config { + args: "fmt --config fmt/deno.malformed.jsonc", + output: "fmt/fmt_with_malformed_config.out", + exit_code: 1, +}); + +itest!(fmt_with_malformed_config2 { + args: "fmt --config fmt/deno.malformed2.jsonc", + output: "fmt/fmt_with_malformed_config2.out", + exit_code: 1, +}); diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index 762d8bb94ba403..6dce35c6e55e87 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -3170,6 +3170,169 @@ fn lsp_format_markdown() { shutdown(&mut client); } +#[test] +fn lsp_format_with_config() { + let temp_dir = TempDir::new().expect("could not create temp dir"); + let mut params: lsp::InitializeParams = + serde_json::from_value(load_fixture("initialize_params.json")).unwrap(); + let deno_fmt_jsonc = + serde_json::to_vec_pretty(&load_fixture("deno.fmt.jsonc")).unwrap(); + fs::write(temp_dir.path().join("deno.fmt.jsonc"), deno_fmt_jsonc).unwrap(); + + params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap()); + if let Some(Value::Object(mut map)) = params.initialization_options { + map.insert("config".to_string(), json!("./deno.fmt.jsonc")); + params.initialization_options = Some(Value::Object(map)); + } + + let deno_exe = deno_exe_path(); + let mut client = LspClient::new(&deno_exe).unwrap(); + client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "export async function someVeryLongFunctionName() {\nconst response = fetch(\"http://localhost:4545/some/non/existent/path.json\");\nconsole.log(response.text());\nconsole.log(\"finished!\")\n}" + } + }), + ) + .unwrap(); + + // The options below should be ignored in favor of configuration from config file. + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "options": { + "tabSize": 2, + "insertSpaces": true + } + }), + ) + .unwrap(); + + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!([{ + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "\t" + }, + { + "range": { + "start": { + "line": 1, + "character": 23 + }, + "end": { + "line": 1, + "character": 24 + } + }, + "newText": "\n\t\t'" + }, + { + "range": { + "start": { + "line": 1, + "character": 73 + }, + "end": { + "line": 1, + "character": 74 + } + }, + "newText": "',\n\t" + }, + { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 0 + } + }, + "newText": "\t" + }, + { + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 3, + "character": 0 + } + }, + "newText": "\t" + }, + { + "range": { + "start": { + "line": 3, + "character": 12 + }, + "end": { + "line": 3, + "character": 13 + } + }, + "newText": "'" + }, + { + "range": { + "start": { + "line": 3, + "character": 22 + }, + "end": { + "line": 3, + "character": 24 + } + }, + "newText": "');" + }, + { + "range": { + "start": { + "line": 4, + "character": 1 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "newText": "\n" + }] + )) + ); + shutdown(&mut client); +} + #[test] fn lsp_markdown_no_diagnostics() { let mut client = init("initialize_params.json"); diff --git a/cli/tests/testdata/fmt/deno.jsonc b/cli/tests/testdata/fmt/deno.jsonc new file mode 100644 index 00000000000000..0e682cf3635fdb --- /dev/null +++ b/cli/tests/testdata/fmt/deno.jsonc @@ -0,0 +1,15 @@ +{ + "fmt": { + "files": { + "include": ["fmt/fmt_with_config/"], + "exclude": ["fmt/fmt_with_config/b.ts"] + }, + "options": { + "useTabs": true, + "lineWidth": 40, + "indentWidth": 8, + "singleQuote": true, + "proseWrap": "always" + } + } +} diff --git a/cli/tests/testdata/fmt/deno.malformed.jsonc b/cli/tests/testdata/fmt/deno.malformed.jsonc new file mode 100644 index 00000000000000..667480bdcfade1 --- /dev/null +++ b/cli/tests/testdata/fmt/deno.malformed.jsonc @@ -0,0 +1,12 @@ +{ + "fmt": { + "files": { + "include": ["fmt/fmt_with_config/"], + "exclude": ["fmt/fmt_with_config/b.ts"] + }, + "dont_know_this_field": {}, + "options": { + "useTabs": true + } + } +} diff --git a/cli/tests/testdata/fmt/deno.malformed2.jsonc b/cli/tests/testdata/fmt/deno.malformed2.jsonc new file mode 100644 index 00000000000000..2210dc0c753010 --- /dev/null +++ b/cli/tests/testdata/fmt/deno.malformed2.jsonc @@ -0,0 +1,12 @@ +{ + "fmt": { + "files": { + "include": ["fmt/fmt_with_config/"], + "exclude": ["fmt/fmt_with_config/b.ts"], + "dont_know_this_field": {} + }, + "options": { + "useTabs": true + } + } +} diff --git a/cli/tests/testdata/fmt/fmt_with_config.out b/cli/tests/testdata/fmt/fmt_with_config.out new file mode 100644 index 00000000000000..158c556c2968ff --- /dev/null +++ b/cli/tests/testdata/fmt/fmt_with_config.out @@ -0,0 +1 @@ +Checked 2 files diff --git a/cli/tests/testdata/fmt/fmt_with_config/a.ts b/cli/tests/testdata/fmt/fmt_with_config/a.ts new file mode 100644 index 00000000000000..e0f32647b62486 --- /dev/null +++ b/cli/tests/testdata/fmt/fmt_with_config/a.ts @@ -0,0 +1,46 @@ +unitTest( + { perms: { net: true } }, + async function responseClone() { + const response = + await fetch( + 'http://localhost:4545/fixture.json', + ); + const response1 = + response.clone(); + assert( + response !== + response1, + ); + assertEquals( + response.status, + response1 + .status, + ); + assertEquals( + response.statusText, + response1 + .statusText, + ); + const u8a = + new Uint8Array( + await response + .arrayBuffer(), + ); + const u8a1 = + new Uint8Array( + await response1 + .arrayBuffer(), + ); + for ( + let i = 0; + i < + u8a.byteLength; + i++ + ) { + assertEquals( + u8a[i], + u8a1[i], + ); + } + }, +); diff --git a/cli/tests/testdata/fmt/fmt_with_config/b.ts b/cli/tests/testdata/fmt/fmt_with_config/b.ts new file mode 100644 index 00000000000000..5c37d51d2877b3 --- /dev/null +++ b/cli/tests/testdata/fmt/fmt_with_config/b.ts @@ -0,0 +1,15 @@ +// This file should be excluded from formatting +unitTest( + { perms: { net: true } }, + async function fetchBodyUsedCancelStream() { + const response = await fetch( + "http://localhost:4545/fixture.json", + ); + assert(response.body !== null); + + assertEquals(response.bodyUsed, false); + const promise = response.body.cancel(); + assertEquals(response.bodyUsed, true); + await promise; + }, +); \ No newline at end of file diff --git a/cli/tests/testdata/fmt/fmt_with_config/c.md b/cli/tests/testdata/fmt/fmt_with_config/c.md new file mode 100644 index 00000000000000..012f7e3d492ef6 --- /dev/null +++ b/cli/tests/testdata/fmt/fmt_with_config/c.md @@ -0,0 +1,17 @@ +## Permissions + +Deno is secure by default. Therefore, +unless you specifically enable it, a +program run with Deno has no file, +network, or environment access. Access +to security sensitive functionality +requires that permisisons have been +granted to an executing script through +command line flags, or a runtime +permission prompt. + +For the following example `mod.ts` has +been granted read-only access to the +file system. It cannot write to the file +system, or perform any other security +sensitive functions. diff --git a/cli/tests/testdata/fmt/fmt_with_config_and_flags.out b/cli/tests/testdata/fmt/fmt_with_config_and_flags.out new file mode 100644 index 00000000000000..c05ac45a1e7e51 --- /dev/null +++ b/cli/tests/testdata/fmt/fmt_with_config_and_flags.out @@ -0,0 +1 @@ +Checked 1 file diff --git a/cli/tests/testdata/fmt/fmt_with_malformed_config.out b/cli/tests/testdata/fmt/fmt_with_malformed_config.out new file mode 100644 index 00000000000000..1a55613ef2a05a --- /dev/null +++ b/cli/tests/testdata/fmt/fmt_with_malformed_config.out @@ -0,0 +1,4 @@ +error: Failed to parse "fmt" configuration + +Caused by: + unknown field `dont_know_this_field`, expected `options` or `files` diff --git a/cli/tests/testdata/fmt/fmt_with_malformed_config2.out b/cli/tests/testdata/fmt/fmt_with_malformed_config2.out new file mode 100644 index 00000000000000..948b6b5b85e3bc --- /dev/null +++ b/cli/tests/testdata/fmt/fmt_with_malformed_config2.out @@ -0,0 +1,4 @@ +error: Failed to parse "fmt" configuration + +Caused by: + unknown field `dont_know_this_field`, expected `include` or `exclude` diff --git a/cli/tests/testdata/fmt/formatted1.js b/cli/tests/testdata/fmt/regular/formatted1.js similarity index 100% rename from cli/tests/testdata/fmt/formatted1.js rename to cli/tests/testdata/fmt/regular/formatted1.js diff --git a/cli/tests/testdata/fmt/formatted2.ts b/cli/tests/testdata/fmt/regular/formatted2.ts similarity index 100% rename from cli/tests/testdata/fmt/formatted2.ts rename to cli/tests/testdata/fmt/regular/formatted2.ts diff --git a/cli/tests/testdata/fmt/formatted3.md b/cli/tests/testdata/fmt/regular/formatted3.md similarity index 100% rename from cli/tests/testdata/fmt/formatted3.md rename to cli/tests/testdata/fmt/regular/formatted3.md diff --git a/cli/tests/testdata/fmt/formatted4.jsonc b/cli/tests/testdata/fmt/regular/formatted4.jsonc similarity index 100% rename from cli/tests/testdata/fmt/formatted4.jsonc rename to cli/tests/testdata/fmt/regular/formatted4.jsonc diff --git a/cli/tests/testdata/lsp/deno.fmt.jsonc b/cli/tests/testdata/lsp/deno.fmt.jsonc new file mode 100644 index 00000000000000..a0a8517318b7d2 --- /dev/null +++ b/cli/tests/testdata/lsp/deno.fmt.jsonc @@ -0,0 +1,11 @@ +{ + "fmt": { + "options": { + "useTabs": true, + "lineWidth": 40, + "indentWidth": 8, + "singleQuote": true, + "proseWrap": "always" + } + } +} diff --git a/cli/tools/fmt.rs b/cli/tools/fmt.rs index b3dfc86e88fc7d..5f1507d19d4d5e 100644 --- a/cli/tools/fmt.rs +++ b/cli/tools/fmt.rs @@ -8,6 +8,9 @@ //! the same functions as ops available in JS runtime. use crate::colors; +use crate::config_file::FmtConfig; +use crate::config_file::FmtOptionsConfig; +use crate::config_file::ProseWrap; use crate::diff::diff; use crate::file_watcher; use crate::file_watcher::ResolutionResult; @@ -35,24 +38,57 @@ pub async fn format( ignore: Vec, check: bool, watch: bool, + maybe_fmt_config: Option, ) -> Result<(), AnyError> { + // First, prepare final configuration. + // Collect included and ignored files. CLI flags take precendence + // over config file, ie. if there's `files.ignore` in config file + // and `--ignore` CLI flag, only the flag value is taken into account. + let mut include_files = args.clone(); + let mut exclude_files = ignore; + + if let Some(fmt_config) = maybe_fmt_config.as_ref() { + if include_files.is_empty() { + include_files = fmt_config + .files + .include + .iter() + .map(PathBuf::from) + .collect::>(); + } + + if exclude_files.is_empty() { + exclude_files = fmt_config + .files + .exclude + .iter() + .map(PathBuf::from) + .collect::>(); + } + } + + let fmt_options = maybe_fmt_config.map(|c| c.options).unwrap_or_default(); + let resolver = |changed: Option>| { let files_changed = changed.is_some(); let result = - collect_files(&args, &ignore, is_supported_ext_fmt).map(|files| { - if let Some(paths) = changed { - files - .into_iter() - .filter(|path| paths.contains(path)) - .collect::>() - } else { - files - } - }); - let paths_to_watch = args.clone(); + collect_files(&include_files, &exclude_files, is_supported_ext_fmt).map( + |files| { + let collected_files = if let Some(paths) = changed { + files + .into_iter() + .filter(|path| paths.contains(path)) + .collect::>() + } else { + files + }; + (collected_files, fmt_options.clone()) + }, + ); + let paths_to_watch = include_files.clone(); async move { if (files_changed || !watch) - && matches!(result, Ok(ref files) if files.is_empty()) + && matches!(result, Ok((ref files, _)) if files.is_empty()) { ResolutionResult::Ignore } else { @@ -63,11 +99,11 @@ pub async fn format( } } }; - let operation = |paths: Vec| async move { + let operation = |(paths, fmt_options): (Vec, FmtOptionsConfig)| async move { if check { - check_source_files(paths).await?; + check_source_files(paths, fmt_options).await?; } else { - format_source_files(paths).await?; + format_source_files(paths, fmt_options).await?; } Ok(()) }; @@ -75,13 +111,13 @@ pub async fn format( if watch { file_watcher::watch_func(resolver, operation, "Fmt").await?; } else { - let files = + let (files, fmt_options) = if let ResolutionResult::Restart { result, .. } = resolver(None).await { result? } else { return Err(generic_error("No target files found.")); }; - operation(files).await?; + operation((files, fmt_options)).await?; } Ok(()) @@ -89,10 +125,14 @@ pub async fn format( /// Formats markdown (using ) and its code blocks /// (ts/tsx, js/jsx). -fn format_markdown(file_text: &str) -> Result { +fn format_markdown( + file_text: &str, + fmt_options: &FmtOptionsConfig, +) -> Result { + let markdown_config = get_resolved_markdown_config(fmt_options); dprint_plugin_markdown::format_text( file_text, - &MARKDOWN_CONFIG, + &markdown_config, move |tag, text, line_width| { let tag = tag.to_lowercase(); if matches!( @@ -115,13 +155,14 @@ fn format_markdown(file_text: &str) -> Result { }; if matches!(extension, "json" | "jsonc") { - let mut json_config = JSON_CONFIG.clone(); + let mut json_config = get_resolved_json_config(fmt_options); json_config.line_width = line_width; dprint_plugin_json::format_text(text, &json_config) } else { let fake_filename = PathBuf::from(format!("deno_fmt_stdin.{}", extension)); - let mut codeblock_config = TYPESCRIPT_CONFIG.clone(); + let mut codeblock_config = + get_resolved_typescript_config(fmt_options); codeblock_config.line_width = line_width; dprint_plugin_typescript::format_text( &fake_filename, @@ -140,32 +181,36 @@ fn format_markdown(file_text: &str) -> Result { /// Formats JSON and JSONC using the rules provided by .deno() /// of configuration builder of . /// See for configuration. -fn format_json(file_text: &str) -> Result { - dprint_plugin_json::format_text(file_text, &JSON_CONFIG) - .map_err(|e| e.to_string()) +fn format_json( + file_text: &str, + fmt_options: &FmtOptionsConfig, +) -> Result { + let config = get_resolved_json_config(fmt_options); + dprint_plugin_json::format_text(file_text, &config).map_err(|e| e.to_string()) } /// Formats a single TS, TSX, JS, JSX, JSONC, JSON, or MD file. pub fn format_file( file_path: &Path, file_text: &str, + fmt_options: FmtOptionsConfig, ) -> Result { let ext = get_extension(file_path).unwrap_or_else(String::new); if ext == "md" { - format_markdown(file_text) + format_markdown(file_text, &fmt_options) } else if matches!(ext.as_str(), "json" | "jsonc") { - format_json(file_text) + format_json(file_text, &fmt_options) } else { - dprint_plugin_typescript::format_text( - file_path, - file_text, - &TYPESCRIPT_CONFIG, - ) - .map_err(|e| e.to_string()) + let config = get_resolved_typescript_config(&fmt_options); + dprint_plugin_typescript::format_text(file_path, file_text, &config) + .map_err(|e| e.to_string()) } } -pub fn format_parsed_module(parsed_source: &ParsedSource) -> String { +pub fn format_parsed_module( + parsed_source: &ParsedSource, + fmt_options: FmtOptionsConfig, +) -> String { dprint_plugin_typescript::format_parsed_file( &dprint_plugin_typescript::SourceFileInfo { is_jsx: matches!( @@ -178,11 +223,14 @@ pub fn format_parsed_module(parsed_source: &ParsedSource) -> String { module: parsed_source.module(), tokens: parsed_source.tokens(), }, - &TYPESCRIPT_CONFIG, + &get_resolved_typescript_config(&fmt_options), ) } -async fn check_source_files(paths: Vec) -> Result<(), AnyError> { +async fn check_source_files( + paths: Vec, + fmt_options: FmtOptionsConfig, +) -> Result<(), AnyError> { let not_formatted_files_count = Arc::new(AtomicUsize::new(0)); let checked_files_count = Arc::new(AtomicUsize::new(0)); @@ -196,7 +244,7 @@ async fn check_source_files(paths: Vec) -> Result<(), AnyError> { checked_files_count.fetch_add(1, Ordering::Relaxed); let file_text = read_file_contents(&file_path)?.text; - match format_file(&file_path, &file_text) { + match format_file(&file_path, &file_text, fmt_options.clone()) { Ok(formatted_text) => { if formatted_text != file_text { not_formatted_files_count.fetch_add(1, Ordering::Relaxed); @@ -235,7 +283,10 @@ async fn check_source_files(paths: Vec) -> Result<(), AnyError> { } } -async fn format_source_files(paths: Vec) -> Result<(), AnyError> { +async fn format_source_files( + paths: Vec, + fmt_options: FmtOptionsConfig, +) -> Result<(), AnyError> { let formatted_files_count = Arc::new(AtomicUsize::new(0)); let checked_files_count = Arc::new(AtomicUsize::new(0)); let output_lock = Arc::new(Mutex::new(0)); // prevent threads outputting at the same time @@ -247,7 +298,7 @@ async fn format_source_files(paths: Vec) -> Result<(), AnyError> { checked_files_count.fetch_add(1, Ordering::Relaxed); let file_contents = read_file_contents(&file_path)?; - match format_file(&file_path, &file_contents.text) { + match format_file(&file_path, &file_contents.text, fmt_options.clone()) { Ok(formatted_text) => { if formatted_text != file_contents.text { write_file_contents( @@ -293,14 +344,18 @@ async fn format_source_files(paths: Vec) -> Result<(), AnyError> { /// Format stdin and write result to stdout. /// Treats input as TypeScript or as set by `--ext` flag. /// Compatible with `--check` flag. -pub fn format_stdin(check: bool, ext: String) -> Result<(), AnyError> { +pub fn format_stdin( + check: bool, + ext: String, + fmt_options: FmtOptionsConfig, +) -> Result<(), AnyError> { let mut source = String::new(); if stdin().read_to_string(&mut source).is_err() { return Err(generic_error("Failed to read from stdin")); } let file_path = PathBuf::from(format!("_stdin.{}", ext)); - match format_file(&file_path, &source) { + match format_file(&file_path, &source, fmt_options) { Ok(formatted_text) => { if check { if formatted_text != source { @@ -325,18 +380,86 @@ fn files_str(len: usize) -> &'static str { } } -lazy_static::lazy_static! { - static ref TYPESCRIPT_CONFIG: dprint_plugin_typescript::configuration::Configuration = dprint_plugin_typescript::configuration::ConfigurationBuilder::new() - .deno() - .build(); +fn get_resolved_typescript_config( + options: &FmtOptionsConfig, +) -> dprint_plugin_typescript::configuration::Configuration { + let mut builder = + dprint_plugin_typescript::configuration::ConfigurationBuilder::new(); + builder.deno(); + + if let Some(use_tabs) = options.use_tabs { + builder.use_tabs(use_tabs); + } - static ref MARKDOWN_CONFIG: dprint_plugin_markdown::configuration::Configuration = dprint_plugin_markdown::configuration::ConfigurationBuilder::new() - .deno() - .build(); + if let Some(line_width) = options.line_width { + builder.line_width(line_width); + } + + if let Some(indent_width) = options.indent_width { + builder.indent_width(indent_width); + } + + if let Some(single_quote) = options.single_quote { + if single_quote { + builder.quote_style( + dprint_plugin_typescript::configuration::QuoteStyle::AlwaysSingle, + ); + } + } + + builder.build() +} + +fn get_resolved_markdown_config( + options: &FmtOptionsConfig, +) -> dprint_plugin_markdown::configuration::Configuration { + let mut builder = + dprint_plugin_markdown::configuration::ConfigurationBuilder::new(); + + builder.deno(); + + if let Some(line_width) = options.line_width { + builder.line_width(line_width); + } + + if let Some(prose_wrap) = options.prose_wrap { + builder.text_wrap(match prose_wrap { + ProseWrap::Always => { + dprint_plugin_markdown::configuration::TextWrap::Always + } + ProseWrap::Never => { + dprint_plugin_markdown::configuration::TextWrap::Never + } + ProseWrap::Preserve => { + dprint_plugin_markdown::configuration::TextWrap::Maintain + } + }); + } + + builder.build() +} + +fn get_resolved_json_config( + options: &FmtOptionsConfig, +) -> dprint_plugin_json::configuration::Configuration { + let mut builder = + dprint_plugin_json::configuration::ConfigurationBuilder::new(); + + builder.deno(); + + if let Some(use_tabs) = options.use_tabs { + builder.use_tabs(use_tabs); + } + + if let Some(line_width) = options.line_width { + builder.line_width(line_width); + } + + if let Some(indent_width) = options.indent_width { + builder.indent_width(indent_width); + } - static ref JSON_CONFIG: dprint_plugin_json::configuration::Configuration = dprint_plugin_json::configuration::ConfigurationBuilder::new() - .deno() - .build(); + builder.build() } struct FileContents {