Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/mq-formatter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ mq-fmt --indent-width 4 file.mq

# Sort imports and functions
mq-fmt --sort-imports --sort-functions file.mq

# Wrap long `|`-chained pipelines at 80 columns
mq-fmt --max-width 80 file.mq
```

### Via mq
Expand All @@ -67,6 +70,7 @@ mq fmt --check file.mq
| Option | Short | Description | Default |
| ------------------ | ----- | ---------------------------------------- | ------- |
| `--indent-width` | `-i` | Number of spaces for indentation | `2` |
| `--max-width` | `-w` | Wrap `\|`-chained pipelines that exceed this line width | unset (no wrapping) |
| `--check` | `-c` | Check formatting without modifying files | `false` |
| `--sort-imports` | | Sort import statements | `false` |
| `--sort-functions` | | Sort function definitions | `false` |
Expand Down
1 change: 1 addition & 0 deletions crates/mq-formatter/benches/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ macro m(): test;"#;
sort_imports: true,
sort_functions: true,
sort_fields: true,
max_width: None,
};
let mut formatter = Formatter::new(Some(config));
formatter.format(code).unwrap();
Expand Down
67 changes: 65 additions & 2 deletions crates/mq-formatter/src/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub struct FormatterConfig {
pub sort_imports: bool,
pub sort_functions: bool,
pub sort_fields: bool,
pub max_width: Option<usize>,
}

impl Default for FormatterConfig {
Expand All @@ -39,6 +40,7 @@ impl Default for FormatterConfig {
sort_imports: false,
sort_functions: false,
sort_fields: false,
max_width: None,
}
}
}
Expand Down Expand Up @@ -212,7 +214,7 @@ impl Formatter {
}

fn format_node(&mut self, node: mq_lang::Shared<mq_lang::CstNode>, indent_level: usize) {
let has_leading_new_line = node.has_new_line();
let has_leading_new_line = node.has_new_line() || (node.is_pipe() && self.should_wrap_pipe());
let indent_level_consider_new_line = if has_leading_new_line { indent_level } else { 0 };

if !matches!(
Expand Down Expand Up @@ -1570,8 +1572,15 @@ impl Formatter {
self.output.push(' ');
}
mq_lang::TokenKind::Pipe => {
if node.has_new_line() {
let force_wrap = !node.has_new_line() && self.should_wrap_pipe();

if node.has_new_line() || force_wrap {
self.append_leading_trivia(node, indent_level);

if force_wrap {
self.append_newline();
}

self.append_indent(indent_level);
self.output.push_str(&token.to_string());
self.output.push(' ');
Expand Down Expand Up @@ -1619,6 +1628,19 @@ impl Formatter {
last_line.chars().take_while(|c| *c == ' ').count() / self.config.indent_width
}

#[inline(always)]
fn current_line_width(&self) -> usize {
let start = self.output.rfind('\n').map_or(0, |pos| pos + 1);
self.output[start..].chars().count()
}

#[inline(always)]
fn should_wrap_pipe(&self) -> bool {
self.config
.max_width
.is_some_and(|max_width| self.current_line_width() >= max_width)
}

#[inline(always)]
pub fn is_last_line_pipe(&self) -> bool {
let output = self.output.trim_end_matches('\n');
Expand Down Expand Up @@ -3079,8 +3101,49 @@ macro mac_x(): test;"#,
sort_imports: true,
sort_functions: true,
sort_fields: true,
max_width: None,
};
let result = Formatter::new(Some(config)).format(code);
assert_eq!(result.unwrap(), expected);
}

#[rstest]
#[case::wraps_top_level_pipeline(
"select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\",\"b\") | join(\",\")",
40,
"select(\"h1\") | upcase() | add_class(\"title\")\n| trim() | to_text() | replace(\"a\", \"b\")\n| join(\",\")\n"
)]
#[case::wraps_pipeline_inside_def(
"def foo(x):\n select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\",\"b\") | join(\",\");",
40,
"def foo(x):\n select(\"h1\") | upcase() | add_class(\"title\")\n | trim() | to_text() | replace(\"a\", \"b\")\n | join(\",\");\n"
)]
#[case::keeps_short_pipeline_inline("select(\"h1\") | upcase()", 80, "select(\"h1\") | upcase()")]
fn test_format_with_max_width(#[case] code: &str, #[case] max_width: usize, #[case] expected: &str) {
let config = FormatterConfig {
max_width: Some(max_width),
..Default::default()
};
let result = Formatter::new(Some(config)).format(code);
assert_eq!(result.unwrap(), expected);
}

#[test]
fn test_format_with_max_width_is_idempotent() {
let code = "select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\",\"b\") | join(\",\")";
let config = || FormatterConfig {
max_width: Some(40),
..Default::default()
};
let once = Formatter::new(Some(config())).format(code).unwrap();
let twice = Formatter::new(Some(config())).format(&once).unwrap();
assert_eq!(once, twice);
}

#[test]
fn test_format_without_max_width_does_not_wrap() {
let code = "select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\", \"b\") | join(\",\")";
let result = Formatter::default().format(code);
assert_eq!(result.unwrap(), code);
}
}
9 changes: 8 additions & 1 deletion crates/mq-formatter/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ use std::{fs, path::PathBuf};
## To check if files are formatted without modifying them:\n\
mq-fmt --check file.mq\n\n\
## To format with custom indent width:\n\
mq-fmt --indent-width 4 file.mq")]
mq-fmt --indent-width 4 file.mq\n\n\
## To wrap long pipelines at a maximum line width:\n\
mq-fmt --max-width 80 file.mq")]
struct Cli {
/// Number of spaces for indentation
#[arg(short, long, default_value_t = 2)]
indent_width: usize,

/// Maximum line width before long `|`-chained pipelines are wrapped onto multiple lines
#[arg(short = 'w', long)]
max_width: Option<usize>,

/// Check if files are formatted without modifying them
#[arg(short, long)]
check: bool,
Expand Down Expand Up @@ -49,6 +55,7 @@ fn main() -> miette::Result<()> {
sort_imports: cli.sort_imports,
sort_fields: cli.sort_fields,
sort_functions: cli.sort_functions,
max_width: cli.max_width,
}));

let files = match cli.files {
Expand Down
4 changes: 4 additions & 0 deletions crates/mq-web-api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ pub struct FormatApiRequest {
pub sort_imports: Option<bool>,
pub sort_functions: Option<bool>,
pub sort_fields: Option<bool>,
pub max_width: Option<usize>,
}

/// Response body for `POST /api/format`.
Expand Down Expand Up @@ -260,6 +261,7 @@ pub fn format_query(request: FormatApiRequest) -> miette::Result<FormatApiRespon
sort_imports: request.sort_imports.unwrap_or(false),
sort_functions: request.sort_functions.unwrap_or(false),
sort_fields: request.sort_fields.unwrap_or(false),
max_width: request.max_width,
};
let formatted = Formatter::new(Some(config))
.format(&request.query)
Expand Down Expand Up @@ -704,6 +706,7 @@ mod tests {
sort_imports: None,
sort_functions: None,
sort_fields: None,
max_width: None,
};
let result = format_query(req);
assert!(result.is_ok());
Expand All @@ -718,6 +721,7 @@ mod tests {
sort_imports: Some(false),
sort_functions: Some(false),
sort_fields: Some(false),
max_width: None,
};
let result = format_query(req);
assert!(result.is_ok());
Expand Down