Commit ec3e4cc14b

87flowers <178735591+87flowers@users.noreply.github.com>
2024-10-16 20:15:19
std/zig/render: Initial implementation of indentation
1 parent 1b0584f
Changed files (1)
lib
std
lib/std/zig/render.zig
@@ -82,6 +82,7 @@ const Render = struct {
 pub fn renderTree(buffer: *std.ArrayList(u8), tree: Ast, fixups: Fixups) Error!void {
     assert(tree.errors.len == 0); // Cannot render an invalid tree.
     var auto_indenting_stream = Ais.init(buffer, indent_delta);
+    defer auto_indenting_stream.deinit();
     var r: Render = .{
         .gpa = buffer.allocator,
         .ais = &auto_indenting_stream,
@@ -195,7 +196,7 @@ fn renderMember(
             try renderExpression(r, fn_proto, .space);
             const body_node = datas[decl].rhs;
             if (r.fixups.gut_functions.contains(decl)) {
-                ais.pushIndent();
+                try ais.pushIndent(.normal);
                 const lbrace = tree.nodes.items(.main_token)[body_node];
                 try renderToken(r, lbrace, .newline);
                 try discardAllParams(r, fn_proto);
@@ -204,7 +205,7 @@ fn renderMember(
                 try ais.insertNewline();
                 try renderToken(r, tree.lastToken(body_node), space); // rbrace
             } else if (r.fixups.unused_var_decls.count() != 0) {
-                ais.pushIndentNextLine();
+                try ais.pushIndent(.normal);
                 const lbrace = tree.nodes.items(.main_token)[body_node];
                 try renderToken(r, lbrace, .newline);
 
@@ -358,13 +359,16 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
         => return renderToken(r, main_tokens[node], space),
 
         .multiline_string_literal => {
-            var locked_indents = ais.lockOneShotIndent();
             try ais.maybeInsertNewline();
 
             var i = datas[node].lhs;
             while (i <= datas[node].rhs) : (i += 1) try renderToken(r, i, .newline);
 
-            while (locked_indents > 0) : (locked_indents -= 1) ais.popIndent();
+            // dedent the next thing that comes after a multiline string literal
+            if (!ais.indentStackEmpty()) {
+                ais.popIndent();
+                try ais.pushIndent(.normal);
+            }
 
             switch (space) {
                 .none, .space, .newline, .skip => {},
@@ -442,6 +446,7 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
 
             try renderExpression(r, datas[node].lhs, .space); // target
 
+            try ais.pushIndent(.normal);
             if (token_tags[fallback_first - 1] == .pipe) {
                 try renderToken(r, main_token, .space); // catch keyword
                 try renderToken(r, main_token + 1, .none); // pipe
@@ -451,38 +456,27 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
                 assert(token_tags[fallback_first - 1] == .keyword_catch);
                 try renderToken(r, main_token, after_op_space); // catch keyword
             }
-
-            ais.pushIndentOneShot();
             try renderExpression(r, datas[node].rhs, space); // fallback
+            ais.popIndent();
         },
 
         .field_access => {
             const main_token = main_tokens[node];
             const field_access = datas[node];
 
+            try ais.pushIndent(.normal);
             try renderExpression(r, field_access.lhs, .none);
 
             // Allow a line break between the lhs and the dot if the lhs and rhs
             // are on different lines.
             const lhs_last_token = tree.lastToken(field_access.lhs);
             const same_line = tree.tokensOnSameLine(lhs_last_token, main_token + 1);
-            if (!same_line) {
-                if (!hasComment(tree, lhs_last_token, main_token)) try ais.insertNewline();
-                ais.pushIndentOneShot();
-            }
+            if (!same_line and !hasComment(tree, lhs_last_token, main_token)) try ais.insertNewline();
 
             try renderToken(r, main_token, .none); // .
 
-            // This check ensures that zag() is indented in the following example:
-            // const x = foo
-            //     .bar()
-            //     . // comment
-            //     zag();
-            if (!same_line and hasComment(tree, main_token, main_token + 1)) {
-                ais.pushIndentOneShot();
-            }
-
-            return renderIdentifier(r, field_access.rhs, space, .eagerly_unquote); // field
+            try renderIdentifier(r, field_access.rhs, space, .eagerly_unquote); // field
+            ais.popIndent();
         },
 
         .error_union,
@@ -555,15 +549,14 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
             const infix = datas[node];
             try renderExpression(r, infix.lhs, .space);
             const op_token = main_tokens[node];
+            try ais.pushIndent(.normal);
             if (tree.tokensOnSameLine(op_token, op_token + 1)) {
                 try renderToken(r, op_token, .space);
             } else {
-                ais.pushIndent();
                 try renderToken(r, op_token, .newline);
-                ais.popIndent();
             }
-            ais.pushIndentOneShot();
-            return renderExpression(r, infix.rhs, space);
+            try renderExpression(r, infix.rhs, space);
+            ais.popIndent();
         },
 
         .assign_destructure => {
@@ -585,15 +578,14 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
                     else => try renderExpression(r, variable_node, variable_space),
                 }
             }
+            try ais.pushIndent(.normal);
             if (tree.tokensOnSameLine(full.ast.equal_token, full.ast.equal_token + 1)) {
                 try renderToken(r, full.ast.equal_token, .space);
             } else {
-                ais.pushIndent();
                 try renderToken(r, full.ast.equal_token, .newline);
-                ais.popIndent();
             }
-            ais.pushIndentOneShot();
-            return renderExpression(r, full.ast.value_expr, space);
+            try renderExpression(r, full.ast.value_expr, space);
+            ais.popIndent();
         },
 
         .bit_not,
@@ -671,7 +663,7 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
             const one_line = tree.tokensOnSameLine(lbracket, rbracket);
             const inner_space = if (one_line) Space.none else Space.newline;
             try renderExpression(r, suffix.lhs, .none);
-            ais.pushIndentNextLine();
+            try ais.pushIndent(.normal);
             try renderToken(r, lbracket, inner_space); // [
             try renderExpression(r, suffix.rhs, inner_space);
             ais.popIndent();
@@ -723,8 +715,9 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
 
         .grouped_expression => {
             try renderToken(r, main_tokens[node], .none); // lparen
-            ais.pushIndentOneShot();
+            try ais.pushIndent(.normal);
             try renderExpression(r, datas[node].lhs, .none);
+            ais.popIndent();
             return renderToken(r, datas[node].rhs, space); // rparen
         },
 
@@ -764,7 +757,7 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
                 return renderToken(r, rbrace, space);
             } else if (token_tags[rbrace - 1] == .comma) {
                 // There is a trailing comma so render each member on a new line.
-                ais.pushIndentNextLine();
+                try ais.pushIndent(.normal);
                 try renderToken(r, lbrace, .newline);
                 var i = lbrace + 1;
                 while (i < rbrace) : (i += 1) {
@@ -845,7 +838,7 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
             try renderExpression(r, full.ast.condition, .none); // condition expression
             try renderToken(r, rparen, .space); // )
 
-            ais.pushIndentNextLine();
+            try ais.pushIndent(.normal);
             if (full.ast.cases.len == 0) {
                 try renderToken(r, rparen + 1, .none); // {
             } else {
@@ -920,7 +913,7 @@ fn renderArrayType(
     const rbracket = tree.firstToken(array_type.ast.elem_type) - 1;
     const one_line = tree.tokensOnSameLine(array_type.ast.lbracket, rbracket);
     const inner_space = if (one_line) Space.none else Space.newline;
-    ais.pushIndentNextLine();
+    try ais.pushIndent(.normal);
     try renderToken(r, array_type.ast.lbracket, inner_space); // lbracket
     try renderExpression(r, array_type.ast.elem_count, inner_space);
     if (array_type.ast.sentinel != 0) {
@@ -1236,13 +1229,10 @@ fn renderVarDeclWithoutFixups(
 
     const eq_token = tree.firstToken(var_decl.ast.init_node) - 1;
     const eq_space: Space = if (tree.tokensOnSameLine(eq_token, eq_token + 1)) .space else .newline;
-    {
-        ais.pushIndent();
-        try renderToken(r, eq_token, eq_space); // =
-        ais.popIndent();
-    }
-    ais.pushIndentOneShot();
-    return renderExpression(r, var_decl.ast.init_node, space); // ;
+    try ais.pushIndent(.normal);
+    try renderToken(r, eq_token, eq_space); // =
+    try renderExpression(r, var_decl.ast.init_node, space); // ;
+    ais.popIndent();
 }
 
 fn renderIf(r: *Render, if_node: Ast.full.If, space: Space) Error!void {
@@ -1342,23 +1332,28 @@ fn renderThenElse(
     const then_expr_is_block = nodeIsBlock(node_tags[then_expr]);
     const indent_then_expr = !then_expr_is_block and
         !tree.tokensOnSameLine(last_prefix_token, tree.firstToken(then_expr));
-    if (indent_then_expr or (then_expr_is_block and ais.isLineOverIndented())) {
-        ais.pushIndentNextLine();
+
+    if (indent_then_expr) try ais.pushIndent(.normal);
+
+    if (then_expr_is_block and ais.isLineOverIndented()) {
+        ais.disableIndentCommitting();
+        try renderToken(r, last_prefix_token, .newline);
+        ais.enableIndentCommitting();
+    } else if (indent_then_expr) {
         try renderToken(r, last_prefix_token, .newline);
-        ais.popIndent();
     } else {
         try renderToken(r, last_prefix_token, .space);
     }
 
     if (else_expr != 0) {
         if (indent_then_expr) {
-            ais.pushIndent();
             try renderExpression(r, then_expr, .newline);
-            ais.popIndent();
         } else {
             try renderExpression(r, then_expr, .space);
         }
 
+        if (indent_then_expr) ais.popIndent();
+
         var last_else_token = else_token;
 
         if (maybe_error_token) |error_token| {
@@ -1372,20 +1367,17 @@ fn renderThenElse(
             !nodeIsBlock(node_tags[else_expr]) and
             !nodeIsIfForWhileSwitch(node_tags[else_expr]);
         if (indent_else_expr) {
-            ais.pushIndentNextLine();
+            try ais.pushIndent(.normal);
             try renderToken(r, last_else_token, .newline);
+            try renderExpression(r, else_expr, space);
             ais.popIndent();
-            try renderExpressionIndented(r, else_expr, space);
         } else {
             try renderToken(r, last_else_token, .space);
             try renderExpression(r, else_expr, space);
         }
     } else {
-        if (indent_then_expr) {
-            try renderExpressionIndented(r, then_expr, space);
-        } else {
-            try renderExpression(r, then_expr, space);
-        }
+        try renderExpression(r, then_expr, space);
+        if (indent_then_expr) ais.popIndent();
     }
 }
 
@@ -1411,7 +1403,7 @@ fn renderFor(r: *Render, for_node: Ast.full.For, space: Space) Error!void {
     var cur = for_node.payload_token;
     const pipe = std.mem.indexOfScalarPos(std.zig.Token.Tag, token_tags, cur, .pipe).?;
     if (token_tags[pipe - 1] == .comma) {
-        ais.pushIndentNextLine();
+        try ais.pushIndent(.normal);
         try renderToken(r, cur - 1, .newline); // |
         while (true) {
             if (token_tags[cur] == .asterisk) {
@@ -1540,7 +1532,7 @@ fn renderContainerField(
     const eq_token = tree.firstToken(field.ast.value_expr) - 1;
     const eq_space: Space = if (tree.tokensOnSameLine(eq_token, eq_token + 1)) .space else .newline;
     {
-        ais.pushIndent();
+        try ais.pushIndent(.normal);
         try renderToken(r, eq_token, eq_space); // =
         ais.popIndent();
     }
@@ -1552,12 +1544,12 @@ fn renderContainerField(
     const maybe_comma = tree.lastToken(field.ast.value_expr) + 1;
 
     if (token_tags[maybe_comma] == .comma) {
-        ais.pushIndent();
+        try ais.pushIndent(.normal);
         try renderExpression(r, field.ast.value_expr, .none); // value
         ais.popIndent();
         try renderToken(r, maybe_comma, .newline);
     } else {
-        ais.pushIndent();
+        try ais.pushIndent(.normal);
         try renderExpression(r, field.ast.value_expr, space); // value
         ais.popIndent();
     }
@@ -1614,9 +1606,12 @@ fn renderBuiltinCall(
             if (token_tags[first_param_token] == .multiline_string_literal_line or
                 hasSameLineComment(tree, first_param_token - 1))
             {
-                ais.pushIndentOneShot();
+                try ais.pushIndent(.normal);
+                try renderExpression(r, param_node, .none);
+                ais.popIndent();
+            } else {
+                try renderExpression(r, param_node, .none);
             }
-            try renderExpression(r, param_node, .none);
 
             if (i + 1 < params.len) {
                 const comma_token = tree.lastToken(param_node) + 1;
@@ -1626,7 +1621,7 @@ fn renderBuiltinCall(
         return renderToken(r, after_last_param_token, space); // )
     } else {
         // Render one param per line.
-        ais.pushIndent();
+        try ais.pushIndent(.normal);
         try renderToken(r, builtin_token + 1, Space.newline); // (
 
         for (params) |param_node| {
@@ -1752,7 +1747,7 @@ fn renderFnProto(r: *Render, fn_proto: Ast.full.FnProto, space: Space) Error!voi
         }
     } else {
         // One param per line.
-        ais.pushIndent();
+        try ais.pushIndent(.normal);
         try renderToken(r, lparen, .newline); // (
 
         var param_i: usize = 0;
@@ -1933,7 +1928,7 @@ fn renderBlock(
         try renderIdentifier(r, lbrace - 2, .none, .eagerly_unquote); // identifier
         try renderToken(r, lbrace - 1, .space); // :
     }
-    ais.pushIndentNextLine();
+    try ais.pushIndent(.normal);
     if (statements.len == 0) {
         try renderToken(r, lbrace, .none);
         ais.popIndent();
@@ -1986,7 +1981,7 @@ fn renderStructInit(
         try renderExpression(r, struct_init.ast.type_expr, .none); // T
     }
     if (struct_init.ast.fields.len == 0) {
-        ais.pushIndentNextLine();
+        try ais.pushIndent(.normal);
         try renderToken(r, struct_init.ast.lbrace, .none); // lbrace
         ais.popIndent();
         return renderToken(r, struct_init.ast.lbrace + 1, space); // rbrace
@@ -1996,7 +1991,7 @@ fn renderStructInit(
     const trailing_comma = token_tags[rbrace - 1] == .comma;
     if (trailing_comma or hasComment(tree, struct_init.ast.lbrace, rbrace)) {
         // Render one field init per line.
-        ais.pushIndentNextLine();
+        try ais.pushIndent(.normal);
         try renderToken(r, struct_init.ast.lbrace, .newline);
 
         try renderToken(r, struct_init.ast.lbrace + 1, .none); // .
@@ -2054,7 +2049,7 @@ fn renderArrayInit(
     }
 
     if (array_init.ast.elements.len == 0) {
-        ais.pushIndentNextLine();
+        try ais.pushIndent(.normal);
         try renderToken(r, array_init.ast.lbrace, .none); // lbrace
         ais.popIndent();
         return renderToken(r, array_init.ast.lbrace + 1, space); // rbrace
@@ -2096,7 +2091,7 @@ fn renderArrayInit(
         return renderToken(r, last_elem_token + 1, space); // rbrace
     }
 
-    ais.pushIndentNextLine();
+    try ais.pushIndent(.normal);
     try renderToken(r, array_init.ast.lbrace, .newline);
 
     var expr_index: usize = 0;
@@ -2149,7 +2144,8 @@ fn renderArrayInit(
         const sub_expr_buffer_starts = try gpa.alloc(usize, section_exprs.len + 1);
         defer gpa.free(sub_expr_buffer_starts);
 
-        var auto_indenting_stream = Ais.init(sub_expr_buffer, indent_delta);
+        var auto_indenting_stream = Ais.init(&sub_expr_buffer, indent_delta);
+        defer auto_indenting_stream.deinit();
         var sub_render: Render = .{
             .gpa = r.gpa,
             .ais = &auto_indenting_stream,
@@ -2312,7 +2308,7 @@ fn renderContainerDecl(
 
     const rbrace = tree.lastToken(container_decl_node);
     if (container_decl.ast.members.len == 0) {
-        ais.pushIndentNextLine();
+        try ais.pushIndent(.normal);
         if (token_tags[lbrace + 1] == .container_doc_comment) {
             try renderToken(r, lbrace, .newline); // lbrace
             try renderContainerDocComments(r, lbrace + 1);
@@ -2354,7 +2350,7 @@ fn renderContainerDecl(
     }
 
     // One member per line.
-    ais.pushIndentNextLine();
+    try ais.pushIndent(.normal);
     try renderToken(r, lbrace, .newline); // lbrace
     if (token_tags[lbrace + 1] == .container_doc_comment) {
         try renderContainerDocComments(r, lbrace + 1);
@@ -2395,7 +2391,7 @@ fn renderAsm(
     }
 
     if (asm_node.ast.items.len == 0) {
-        ais.pushIndent();
+        try ais.pushIndent(.normal);
         if (asm_node.first_clobber) |first_clobber| {
             // asm ("foo" ::: "a", "b")
             // asm ("foo" ::: "a", "b",)
@@ -2433,7 +2429,7 @@ fn renderAsm(
         }
     }
 
-    ais.pushIndent();
+    try ais.pushIndent(.normal);
     try renderExpression(r, asm_node.ast.template, .newline);
     ais.setIndentDelta(asm_indent_delta);
     const colon1 = tree.lastToken(asm_node.ast.template) + 1;
@@ -2444,7 +2440,7 @@ fn renderAsm(
     } else colon2: {
         try renderToken(r, colon1, .space); // :
 
-        ais.pushIndent();
+        try ais.pushIndent(.normal);
         for (asm_node.outputs, 0..) |asm_output, i| {
             if (i + 1 < asm_node.outputs.len) {
                 const next_asm_output = asm_node.outputs[i + 1];
@@ -2476,7 +2472,7 @@ fn renderAsm(
         break :colon3 colon2 + 1;
     } else colon3: {
         try renderToken(r, colon2, .space); // :
-        ais.pushIndent();
+        try ais.pushIndent(.normal);
         for (asm_node.inputs, 0..) |asm_input, i| {
             if (i + 1 < asm_node.inputs.len) {
                 const next_asm_input = asm_node.inputs[i + 1];
@@ -2558,7 +2554,7 @@ fn renderParamList(
     const token_tags = tree.tokens.items(.tag);
 
     if (params.len == 0) {
-        ais.pushIndentNextLine();
+        try ais.pushIndent(.normal);
         try renderToken(r, lparen, .none);
         ais.popIndent();
         return renderToken(r, lparen + 1, space); // )
@@ -2567,22 +2563,15 @@ fn renderParamList(
     const last_param = params[params.len - 1];
     const after_last_param_tok = tree.lastToken(last_param) + 1;
     if (token_tags[after_last_param_tok] == .comma) {
-        ais.pushIndentNextLine();
+        try ais.pushIndent(.normal);
         try renderToken(r, lparen, .newline); // (
         for (params, 0..) |param_node, i| {
             if (i + 1 < params.len) {
                 try renderExpression(r, param_node, .none);
 
-                // Unindent the comma for multiline string literals.
-                const is_multiline_string =
-                    token_tags[tree.firstToken(param_node)] == .multiline_string_literal_line;
-                if (is_multiline_string) ais.popIndent();
-
                 const comma = tree.lastToken(param_node) + 1;
                 try renderToken(r, comma, .newline); // ,
 
-                if (is_multiline_string) ais.pushIndent();
-
                 try renderExtraNewline(r, params[i + 1]);
             } else {
                 try renderExpression(r, param_node, .comma);
@@ -2599,9 +2588,12 @@ fn renderParamList(
         if (token_tags[first_param_token] == .multiline_string_literal_line or
             hasSameLineComment(tree, first_param_token - 1))
         {
-            ais.pushIndentOneShot();
+            try ais.pushIndent(.normal);
+            try renderExpression(r, param_node, .none);
+            ais.popIndent();
+        } else {
+            try renderExpression(r, param_node, .none);
         }
-        try renderExpression(r, param_node, .none);
 
         if (i + 1 < params.len) {
             const comma = tree.lastToken(param_node) + 1;
@@ -2615,66 +2607,6 @@ fn renderParamList(
     return renderToken(r, after_last_param_tok, space); // )
 }
 
-/// Renders the given expression indented, popping the indent before rendering
-/// any following line comments
-fn renderExpressionIndented(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
-    const tree = r.tree;
-    const ais = r.ais;
-    const token_starts = tree.tokens.items(.start);
-    const token_tags = tree.tokens.items(.tag);
-
-    ais.pushIndent();
-
-    var last_token = tree.lastToken(node);
-    const punctuation = switch (space) {
-        .none, .space, .newline, .skip => false,
-        .comma => true,
-        .comma_space => token_tags[last_token + 1] == .comma,
-        .semicolon => token_tags[last_token + 1] == .semicolon,
-    };
-
-    try renderExpression(r, node, if (punctuation) .none else .skip);
-
-    switch (space) {
-        .none, .space, .newline, .skip => {},
-        .comma => {
-            if (token_tags[last_token + 1] == .comma) {
-                try renderToken(r, last_token + 1, .skip);
-                last_token += 1;
-            } else {
-                try ais.writer().writeByte(',');
-            }
-        },
-        .comma_space => if (token_tags[last_token + 1] == .comma) {
-            try renderToken(r, last_token + 1, .skip);
-            last_token += 1;
-        },
-        .semicolon => if (token_tags[last_token + 1] == .semicolon) {
-            try renderToken(r, last_token + 1, .skip);
-            last_token += 1;
-        },
-    }
-
-    ais.popIndent();
-
-    if (space == .skip) return;
-
-    const comment_start = token_starts[last_token] + tokenSliceForRender(tree, last_token).len;
-    const comment = try renderComments(r, comment_start, token_starts[last_token + 1]);
-
-    if (!comment) switch (space) {
-        .none => {},
-        .space,
-        .comma_space,
-        => try ais.writer().writeByte(' '),
-        .newline,
-        .comma,
-        .semicolon,
-        => try ais.insertNewline(),
-        .skip => unreachable,
-    };
-}
-
 /// Render an expression, and the comma that follows it, if it is present in the source.
 /// If a comma is present, and `space` is `Space.comma`, render only a single comma.
 fn renderExpressionComma(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
@@ -3315,6 +3247,14 @@ fn AutoIndentingStream(comptime UnderlyingWriter: type) type {
         pub const WriteError = UnderlyingWriter.Error;
         pub const Writer = std.io.Writer(*Self, WriteError, write);
 
+        pub const IndentType = enum {
+            normal,
+        };
+        const StackElem = struct {
+            indent_type: IndentType,
+            realized: bool,
+        };
+
         underlying_writer: UnderlyingWriter,
 
         /// Offset into the source at which formatting has been disabled with
@@ -3327,21 +3267,24 @@ fn AutoIndentingStream(comptime UnderlyingWriter: type) type {
 
         indent_count: usize = 0,
         indent_delta: usize,
+        indent_stack: std.ArrayList(StackElem),
+        disable_indent_committing: usize = 0,
         current_line_empty: bool = true,
-        /// automatically popped when applied
-        indent_one_shot_count: usize = 0,
         /// the most recently applied indent
         applied_indent: usize = 0,
-        /// not used until the next line
-        indent_next_line: usize = 0,
 
-        pub fn init(buffer: *std.ArrayList(u8), indent_delta: usize) Self {
+        pub fn init(buffer: *std.ArrayList(u8), indent_delta_: usize) Self {
             return .{
                 .underlying_writer = buffer.writer(),
-                .indent_delta = indent_delta,
+                .indent_delta = indent_delta_,
+                .indent_stack = std.ArrayList(StackElem).init(buffer.allocator),
             };
         }
 
+        pub fn deinit(self: *Self) void {
+            self.indent_stack.deinit();
+        }
+
         pub fn writer(self: *Self) Writer {
             return .{ .context = self };
         }
@@ -3385,7 +3328,23 @@ fn AutoIndentingStream(comptime UnderlyingWriter: type) type {
 
         fn resetLine(self: *Self) void {
             self.current_line_empty = true;
-            self.indent_next_line = 0;
+            if (self.disable_indent_committing > 0) return;
+            if (self.indent_stack.items.len > 0) {
+                // Only realize last pushed indent
+                if (!self.indent_stack.items[self.indent_stack.items.len - 1].realized) {
+                    self.indent_stack.items[self.indent_stack.items.len - 1].realized = true;
+                    self.indent_count += 1;
+                }
+            }
+        }
+
+        pub fn disableIndentCommitting(self: *Self) void {
+            self.disable_indent_committing += 1;
+        }
+
+        pub fn enableIndentCommitting(self: *Self) void {
+            assert(self.disable_indent_committing > 0);
+            self.disable_indent_committing -= 1;
         }
 
         /// Insert a newline unless the current line is blank
@@ -3397,36 +3356,19 @@ fn AutoIndentingStream(comptime UnderlyingWriter: type) type {
         /// Push default indentation
         /// Doesn't actually write any indentation.
         /// Just primes the stream to be able to write the correct indentation if it needs to.
-        pub fn pushIndent(self: *Self) void {
-            self.indent_count += 1;
-        }
-
-        /// Push an indent that is automatically popped after being applied
-        pub fn pushIndentOneShot(self: *Self) void {
-            self.indent_one_shot_count += 1;
-            self.pushIndent();
-        }
-
-        /// Turns all one-shot indents into regular indents
-        /// Returns number of indents that must now be manually popped
-        pub fn lockOneShotIndent(self: *Self) usize {
-            const locked_count = self.indent_one_shot_count;
-            self.indent_one_shot_count = 0;
-            return locked_count;
-        }
-
-        /// Push an indent that should not take effect until the next line
-        pub fn pushIndentNextLine(self: *Self) void {
-            self.indent_next_line += 1;
-            self.pushIndent();
+        pub fn pushIndent(self: *Self, indent_type: IndentType) !void {
+            try self.indent_stack.append(.{ .indent_type = indent_type, .realized = false });
         }
 
         pub fn popIndent(self: *Self) void {
-            assert(self.indent_count != 0);
-            self.indent_count -= 1;
+            if (self.indent_stack.pop().realized) {
+                assert(self.indent_count > 0);
+                self.indent_count -= 1;
+            }
+        }
 
-            if (self.indent_next_line > 0)
-                self.indent_next_line -= 1;
+        pub fn indentStackEmpty(self: *Self) bool {
+            return self.indent_stack.items.len == 0;
         }
 
         /// Writes ' ' bytes if the current line is empty
@@ -3438,9 +3380,6 @@ fn AutoIndentingStream(comptime UnderlyingWriter: type) type {
                 }
                 self.applied_indent = current_indent;
             }
-
-            self.indent_count -= self.indent_one_shot_count;
-            self.indent_one_shot_count = 0;
             self.current_line_empty = false;
         }
 
@@ -3451,12 +3390,7 @@ fn AutoIndentingStream(comptime UnderlyingWriter: type) type {
         }
 
         fn currentIndent(self: *Self) usize {
-            var indent_current: usize = 0;
-            if (self.indent_count > 0) {
-                const indent_count = self.indent_count - self.indent_next_line;
-                indent_current = indent_count * self.indent_delta;
-            }
-            return indent_current;
+            return self.indent_count * self.indent_delta;
         }
     };
 }