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
42 changes: 40 additions & 2 deletions src/formatting/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@ impl<'a> Formatter<'a> {
self.write_comments_before(first_elem.expr.span.start);
}

// Pipelines inside indented blocks (e.g. def bodies) need
// explicit indentation when they start on a fresh line.
if self.at_line_start {
self.write_indent();
}

self.format_pipeline(pipeline);

if let Some(last_elem) = pipeline.elements.last() {
let end_pos = self.get_element_end_pos(last_elem);
self.write_inline_comment(end_pos);
self.write_inline_comment_bounded(end_pos, self.inline_comment_upper_bound);
self.last_pos = end_pos;
}

Expand Down Expand Up @@ -58,10 +64,34 @@ impl<'a> Formatter<'a> {
}

/// Format a pipeline (elements joined by `|`).
///
/// Preserves multi-line pipeline layout: if the original source has line
/// breaks between pipeline elements, the formatted output keeps each
/// stage on its own line with `| ` prefix.
pub(super) fn format_pipeline(&mut self, pipeline: &nu_protocol::ast::Pipeline) {
if pipeline.elements.len() <= 1 {
if let Some(element) = pipeline.elements.first() {
self.format_pipeline_element(element);
}
return;
}

// Detect whether the source places pipeline elements on separate lines.
let is_multiline = pipeline.elements.windows(2).any(|pair| {
let prev_end = self.get_element_end_pos(&pair[0]);
let next_start = pair[1].expr.span.start;
prev_end < next_start && self.source[prev_end..next_start].contains(&b'\n')
});

for (i, element) in pipeline.elements.iter().enumerate() {
if i > 0 {
self.write(" | ");
if is_multiline {
self.newline();
self.write_indent();
self.write("| ");
} else {
self.write(" | ");
}
}
self.format_pipeline_element(element);
}
Expand Down Expand Up @@ -133,6 +163,12 @@ impl<'a> Formatter<'a> {
self.write("{");
}

// Reset conditional_context_depth inside block bodies so that
// subexpressions nested inside `if` / `try` blocks keep their
// explicit parentheses (fixes issue #131).
let saved_conditional_depth = self.conditional_context_depth;
self.conditional_context_depth = 0;

let is_simple = block.pipelines.len() == 1
&& block.pipelines[0].elements.len() == 1
&& !self.block_has_nested_structures(block)
Expand Down Expand Up @@ -164,6 +200,8 @@ impl<'a> Formatter<'a> {
if with_braces {
self.write("}");
}

self.conditional_context_depth = saved_conditional_depth;
}

/// Detect whether a braced block looks like a compact record literal
Expand Down
68 changes: 68 additions & 0 deletions src/formatting/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ impl<'a> Formatter<'a> {
return;
}

if decl_name == "for" {
self.format_for_call(call);
return;
}

for arg in &call.arguments {
self.format_call_argument(arg, &cmd_type);
}
Expand Down Expand Up @@ -110,6 +115,69 @@ impl<'a> Formatter<'a> {
}
}

/// Format `for` loop calls, preserving explicit type annotations on the
/// loop variable (e.g. `for h: int in [1 2 3] { ... }`).
pub(super) fn format_for_call(&mut self, call: &nu_protocol::ast::Call) {
// Find the VarDecl positional and the Keyword("in") argument.
let var_decl = call.arguments.iter().find_map(|arg| match arg {
Argument::Positional(expr) | Argument::Unknown(expr)
if matches!(expr.expr, Expr::VarDecl(_)) =>
{
Some(expr)
}
_ => None,
});

let keyword_in = call.arguments.iter().find_map(|arg| match arg {
Argument::Positional(expr) | Argument::Unknown(expr)
if matches!(expr.expr, Expr::Keyword(_)) =>
{
Some(expr)
}
_ => None,
});

if let (Some(var_decl), Some(kw_in)) = (var_decl, keyword_in) {
self.space();
self.format_expression(var_decl);

// Preserve a type annotation (e.g. `: int`) between the loop
// variable and the `in` keyword.
if var_decl.span.end < kw_in.span.start {
let between = &self.source[var_decl.span.end..kw_in.span.start];
let annotation = between.trim_ascii();
if annotation.starts_with(b":") {
self.write_bytes(annotation);
}
}

self.space();
self.format_expression(kw_in);
} else {
// Fallback — format all arguments normally
for arg in &call.arguments {
self.format_call_argument(arg, &CommandType::Block);
}
return;
}

// Format remaining arguments (the body block)
for arg in &call.arguments {
match arg {
Argument::Positional(expr) | Argument::Unknown(expr) => {
if matches!(expr.expr, Expr::VarDecl(_) | Expr::Keyword(_)) {
continue;
}
self.space();
self.format_block_or_expr(expr);
}
_ => {
self.format_call_argument(arg, &CommandType::Block);
}
}
}
}

/// Classify a command name into a [`CommandType`] for formatting purposes.
pub(super) fn classify_command(name: &str) -> CommandType {
if DEF_COMMANDS.contains(&name) {
Expand Down
10 changes: 8 additions & 2 deletions src/formatting/collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ impl<'a> Formatter<'a> {
| Expr::List(_)
| Expr::Closure(_)
| Expr::Block(_)
| Expr::Var(_)
| Expr::FullCellPath(_)
| Expr::StringInterpolation(_)
| Expr::Subexpression(_)
),
RecordItem::Spread(_, _) => false,
});
Expand Down Expand Up @@ -153,6 +153,12 @@ impl<'a> Formatter<'a> {
for item in items {
self.write_indent();
self.format_record_item(item, preserve_compact);
// Capture inline comments on the same line as the record item.
let item_end = match item {
RecordItem::Pair(_, v) => v.span.end,
RecordItem::Spread(_, expr) => expr.span.end,
};
self.write_inline_comment_bounded(item_end, None);
self.newline();
}
self.indent_level -= 1;
Expand Down
39 changes: 33 additions & 6 deletions src/formatting/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,26 @@ impl<'a> Formatter<'a> {

comments_to_write.sort_by_key(|(_, start, _)| *start);

for (idx, _, content) in comments_to_write {
self.written_comments[idx] = true;
let mut prev_comment_end: Option<usize> = None;
for (idx, start, content) in &comments_to_write {
self.written_comments[*idx] = true;

// Preserve blank lines between comment groups by checking
// whether the original source has a blank line between the
// previous comment and this one.
if let Some(prev_end) = prev_comment_end {
let between = &self.source[prev_end..*start];
let newline_count = between.iter().filter(|&&b| b == b'\n').count();
if newline_count >= 2 {
// There was at least one blank line in the source
// between the two comments — emit one now.
if !self.at_line_start {
self.newline();
}
self.newline();
}
}

if !self.at_line_start {
if let Some(&last) = self.output.last() {
if last != b'\n' {
Expand All @@ -85,24 +103,33 @@ impl<'a> Formatter<'a> {
}
}
self.write_indent();
self.output.extend(&content);
self.output.extend(content);
self.newline();

prev_comment_end = Some(start + content.len());
}
}

/// Emit an inline comment (on the same line) that appears after `after_pos`.
pub(super) fn write_inline_comment(&mut self, after_pos: usize) {
/// Emit an inline comment (on the same line) that appears after `after_pos`,
/// optionally bounded by an upper position limit.
///
/// When `upper` is `Some(pos)`, comments starting at or after `pos` are
/// ignored. This prevents capturing comments that belong outside a
/// surrounding delimiter (e.g. after a closing parenthesis).
pub(super) fn write_inline_comment_bounded(&mut self, after_pos: usize, upper: Option<usize>) {
let line_end = self.source[after_pos..]
.iter()
.position(|&b| b == b'\n')
.map_or(self.source.len(), |p| after_pos + p);

let effective_end = upper.map_or(line_end, |u| u.min(line_end));

let found = self
.comments
.iter()
.enumerate()
.find(|(i, (span, _))| {
!self.written_comments[*i] && span.start >= after_pos && span.start < line_end
!self.written_comments[*i] && span.start >= after_pos && span.start < effective_end
})
.map(|(i, (span, content))| (i, *span, content.clone()));

Expand Down
25 changes: 24 additions & 1 deletion src/formatting/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ impl<'a> Formatter<'a> {
| Expr::Binary(_)
| Expr::Filepath(_, _)
| Expr::Directory(_, _)
| Expr::GlobPattern(_, _)
| Expr::Var(_)
| Expr::VarDecl(_)
| Expr::Operator(_)
Expand All @@ -36,6 +35,22 @@ impl<'a> Formatter<'a> {
self.write_expr_span(expr);
}

// Glob patterns — normalise empty-brace globs like `{ }` to `{}`
// (the parser treats `{ }` in unknown-command args as a glob).
Expr::GlobPattern(_, _) => {
let content = &self.source[expr.span.start..expr.span.end];
if content.starts_with(b"{")
&& content.ends_with(b"}")
&& content[1..content.len() - 1]
.iter()
.all(|b| b.is_ascii_whitespace())
{
self.write("{}");
} else {
self.write_expr_span(expr);
}
}

Expr::Signature(sig) => {
if self.has_comments_in_span(expr.span.start, expr.span.end) {
self.write_expr_span(expr);
Expand Down Expand Up @@ -253,6 +268,12 @@ impl<'a> Formatter<'a> {
}
}

// Set inline comment boundary at the closing paren so that comments
// appearing after `)` on the same line are not captured inside (issue #133).
let saved_bound = self.inline_comment_upper_bound;
let closing_paren_pos = span.end.saturating_sub(1);
self.inline_comment_upper_bound = Some(closing_paren_pos);

self.write("(");
let is_simple = !source_has_newline
&& block.pipelines.len() == 1
Expand All @@ -269,6 +290,8 @@ impl<'a> Formatter<'a> {
self.write_indent();
}
self.write(")");

self.inline_comment_upper_bound = saved_bound;
}

/// Format an expression that could be a block or a regular expression.
Expand Down
15 changes: 12 additions & 3 deletions src/formatting/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ pub(crate) struct Formatter<'a> {
pub(crate) preserve_subexpr_parens_depth: usize,
/// Allow compact inline record style used for repaired malformed records.
pub(crate) allow_compact_recovered_record_style: bool,
/// Optional upper boundary for inline comment capture inside
/// delimited contexts (e.g. subexpressions).
pub(crate) inline_comment_upper_bound: Option<usize>,
}

/// Command types for formatting purposes.
Expand Down Expand Up @@ -101,6 +104,7 @@ impl<'a> Formatter<'a> {
conditional_context_depth: 0,
preserve_subexpr_parens_depth: 0,
allow_compact_recovered_record_style,
inline_comment_upper_bound: None,
}
}

Expand Down Expand Up @@ -151,15 +155,20 @@ impl<'a> Formatter<'a> {
// Span and source helpers
// ─────────────────────────────────────────────────────────────────────────

/// Get the source content for a span.
/// Copy the source bytes for a span into a new `Vec`.
///
/// Returns owned data so that callers can slice into it while still
/// holding `&mut self` for output writes (splitting borrows on `self`
/// through a method return is not possible with a `&[u8]` reference).
pub(crate) fn get_span_content(&self, span: Span) -> Vec<u8> {
self.source[span.start..span.end].to_vec()
}

/// Write the original source content for a span.
pub(crate) fn write_span(&mut self, span: Span) {
let content = self.source[span.start..span.end].to_vec();
self.write_bytes(&content);
self.write_indent();
self.output
.extend_from_slice(&self.source[span.start..span.end]);
}

/// Write the original source content for an expression's span.
Expand Down
5 changes: 1 addition & 4 deletions tests/fixtures/expected/error_make.nu
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ error make {
}
error make {
msg: "error"
label: {
text: "here"
span: $span
}
label: {text: "here", span: $span}
}
error make {msg: "test error"}
error make {
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/expected/issue128.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# module level doc

# next line comment
use a.nu
3 changes: 3 additions & 0 deletions tests/fixtures/expected/issue129.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def create-item [val_a: string, val_b: string] {
{field_a: $val_a, field_b: $val_b}
}
2 changes: 2 additions & 0 deletions tests/fixtures/expected/issue130.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
($json_input.tool_response? | default {})
($json_input.tool_response? | default {})
3 changes: 3 additions & 0 deletions tests/fixtures/expected/issue131.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if "a" != "b" {
return (call true)
}
3 changes: 3 additions & 0 deletions tests/fixtures/expected/issue132.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
for x: int in [1 2 3] {
print $x
}
5 changes: 5 additions & 0 deletions tests/fixtures/expected/issue133.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export def colors [] {
{
warn: (base16 "base0A" "yellow") # warning
}
}
6 changes: 6 additions & 0 deletions tests/fixtures/expected/issue134.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
let state = if $is_new {
init-state
| upsert APP_DRIVER (get-field $item "APP_DRIVER")
| upsert APP_STAGE (get-field $item "APP_STAGE")
| upsert APP_TOOL (get-field $item "APP_TOOL")
}
4 changes: 4 additions & 0 deletions tests/fixtures/input/issue128.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# module level doc

# next line comment
use a.nu
3 changes: 3 additions & 0 deletions tests/fixtures/input/issue129.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def create-item [val_a: string, val_b: string] {
{ field_a: $val_a, field_b: $val_b }
}
Loading
Loading