1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-21 21:50:00 -05:00

feat(fmt): add support for configuration file (#11944)

This commit adds support for configuration file for "deno fmt"
subcommand. It is also respected by LSP when formatting
files.

Example configuration:
{
    "fmt": {
        "files": {
            "include": ["src/"],
            "exclude": ["src/testdata/"]
        },
        "options": {
            "useTabs": true,
            "lineWidth": 80,
            "indentWidth": 4,
            "singleQuote": true,
            "textWrap": "preserve"
        }
    }
}
This commit is contained in:
Bartek Iwańczuk 2021-09-13 20:19:10 +02:00 committed by GitHub
parent a655a0f3e4
commit 0dbeb774ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 687 additions and 92 deletions

View file

@ -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",

View file

@ -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<String>,
pub exclude: Vec<String>,
}
@ -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<bool>,
pub line_width: Option<u32>,
pub indent_width: Option<u8>,
pub single_quote: Option<bool>,
pub prose_wrap: Option<ProseWrap>,
}
#[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<Value>,
pub lint: Option<Value>,
pub fmt: Option<Value>,
}
#[derive(Clone, Debug)]
@ -374,6 +400,16 @@ impl ConfigFile {
Ok(None)
}
}
pub fn to_fmt_config(&self) -> Result<Option<FmtConfig>, 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]

View file

@ -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")
@ -1732,6 +1733,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(),
@ -2534,6 +2536,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]

View file

@ -336,15 +336,15 @@ impl Inner {
Ok(navigation_tree)
}
fn merge_user_tsconfig(
&mut self,
maybe_config: &Option<String>,
maybe_root_uri: &Option<Url>,
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<Option<(ConfigFile, Url)>, 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) {
@ -374,18 +374,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(())
}
@ -575,20 +591,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
@ -1015,14 +1026,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,
)
}
};

View file

@ -805,8 +805,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(
@ -814,6 +826,7 @@ async fn format_command(
fmt_flags.ignore,
fmt_flags.check,
flags.watch,
maybe_fmt_config,
)
.await?;
Ok(())

View file

@ -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,
});

View file

@ -3196,6 +3196,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");

15
cli/tests/testdata/fmt/deno.jsonc vendored Normal file
View file

@ -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"
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -0,0 +1 @@
Checked 2 files

View file

@ -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],
);
}
},
);

View file

@ -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;
},
);

View file

@ -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.

View file

@ -0,0 +1 @@
Checked 1 file

View file

@ -0,0 +1,4 @@
error: Failed to parse "fmt" configuration
Caused by:
unknown field `dont_know_this_field`, expected `options` or `files`

View file

@ -0,0 +1,4 @@
error: Failed to parse "fmt" configuration
Caused by:
unknown field `dont_know_this_field`, expected `include` or `exclude`

11
cli/tests/testdata/lsp/deno.fmt.jsonc vendored Normal file
View file

@ -0,0 +1,11 @@
{
"fmt": {
"options": {
"useTabs": true,
"lineWidth": 40,
"indentWidth": 8,
"singleQuote": true,
"proseWrap": "always"
}
}
}

View file

@ -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<PathBuf>,
check: bool,
watch: bool,
maybe_fmt_config: Option<FmtConfig>,
) -> 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::<Vec<PathBuf>>();
}
if exclude_files.is_empty() {
exclude_files = fmt_config
.files
.exclude
.iter()
.map(PathBuf::from)
.collect::<Vec<PathBuf>>();
}
}
let fmt_options = maybe_fmt_config.map(|c| c.options).unwrap_or_default();
let resolver = |changed: Option<Vec<PathBuf>>| {
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::<Vec<_>>()
} 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::<Vec<_>>()
} 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<PathBuf>| async move {
let operation = |(paths, fmt_options): (Vec<PathBuf>, 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 <https://github.com/dprint/dprint-plugin-markdown>) and its code blocks
/// (ts/tsx, js/jsx).
fn format_markdown(file_text: &str) -> Result<String, String> {
fn format_markdown(
file_text: &str,
fmt_options: &FmtOptionsConfig,
) -> Result<String, String> {
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<String, String> {
};
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<String, String> {
/// Formats JSON and JSONC using the rules provided by .deno()
/// of configuration builder of <https://github.com/dprint/dprint-plugin-json>.
/// See <https://git.io/Jt4ht> for configuration.
fn format_json(file_text: &str) -> Result<String, String> {
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<String, String> {
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<String, String> {
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<PathBuf>) -> Result<(), AnyError> {
async fn check_source_files(
paths: Vec<PathBuf>,
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<PathBuf>) -> 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<PathBuf>) -> Result<(), AnyError> {
}
}
async fn format_source_files(paths: Vec<PathBuf>) -> Result<(), AnyError> {
async fn format_source_files(
paths: Vec<PathBuf>,
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<PathBuf>) -> 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<PathBuf>) -> 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();
static ref MARKDOWN_CONFIG: dprint_plugin_markdown::configuration::Configuration = dprint_plugin_markdown::configuration::ConfigurationBuilder::new()
.deno()
.build();
if let Some(use_tabs) = options.use_tabs {
builder.use_tabs(use_tabs);
}
static ref JSON_CONFIG: dprint_plugin_json::configuration::Configuration = dprint_plugin_json::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);
}
builder.build()
}
struct FileContents {