Skip to content

Commit

Permalink
feat(fmt): add support for configuration file (#11944)
Browse files Browse the repository at this point in the history
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"
        }
    }
}
  • Loading branch information
bartlomieju authored Sep 13, 2021
1 parent a655a0f commit 0dbeb77
Show file tree
Hide file tree
Showing 23 changed files with 687 additions and 92 deletions.
1 change: 1 addition & 0 deletions .dprint.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
64 changes: 62 additions & 2 deletions cli/config_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}
Expand All @@ -286,14 +286,40 @@ 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)]
#[serde(rename_all = "camelCase")]
pub struct ConfigFileJson {
pub compiler_options: Option<Value>,
pub lint: Option<Value>,
pub fmt: Option<Value>,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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]
Expand Down
40 changes: 40 additions & 0 deletions cli/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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]
Expand Down
104 changes: 69 additions & 35 deletions cli/lsp/language_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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(())
}

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

Expand Down
15 changes: 14 additions & 1 deletion cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -805,15 +805,28 @@ 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(
fmt_flags.files,
fmt_flags.ignore,
fmt_flags.check,
flags.watch,
maybe_fmt_config,
)
.await?;
Ok(())
Expand Down
Loading

0 comments on commit 0dbeb77

Please sign in to comment.