|
| 1 | +/* |
| 2 | + * Copyright 2025 the original author or authors. |
| 3 | + * <p> |
| 4 | + * Licensed under the Moderne Source Available License (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * <p> |
| 8 | + * https://docs.moderne.io/licensing/moderne-source-available-license |
| 9 | + * <p> |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | +import {isJavaScript, JS, JSX} from "./tree"; |
| 17 | +import {JavaScriptVisitor} from "./visitor"; |
| 18 | +import {isJava, J, lastWhitespace, replaceLastWhitespace} from "../java"; |
| 19 | +import {produce} from "immer"; |
| 20 | +import {Cursor, isScope, Tree} from "../tree"; |
| 21 | +import {TabsAndIndentsStyle} from "./style"; |
| 22 | + |
| 23 | +type IndentKind = 'block' | 'continuation' | 'align'; |
| 24 | +export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> { |
| 25 | + private readonly singleIndent: string; |
| 26 | + |
| 27 | + constructor(private readonly tabsAndIndentsStyle: TabsAndIndentsStyle, private stopAfter?: Tree) { |
| 28 | + super(); |
| 29 | + |
| 30 | + if (this.tabsAndIndentsStyle.useTabCharacter) { |
| 31 | + this.singleIndent = "\t"; |
| 32 | + } else { |
| 33 | + this.singleIndent = " ".repeat(this.tabsAndIndentsStyle.indentSize); |
| 34 | + } |
| 35 | + } |
| 36 | + |
| 37 | + protected async preVisit(tree: J, _p: P): Promise<J | undefined> { |
| 38 | + this.setupCursorMessagesForTree(this.cursor, tree); |
| 39 | + return tree; |
| 40 | + } |
| 41 | + |
| 42 | + private setupCursorMessagesForTree(cursor: Cursor, tree: J): void { |
| 43 | + const [parentMyIndent, parentIndentKind] = this.getParentIndentContext(cursor); |
| 44 | + const myIndent = this.computeMyIndent(tree, parentMyIndent, parentIndentKind); |
| 45 | + cursor.messages.set("myIndent", myIndent); |
| 46 | + cursor.messages.set("indentKind", this.computeIndentKind(tree)); |
| 47 | + } |
| 48 | + |
| 49 | + private getParentIndentContext(cursor: Cursor): [string, IndentKind] { |
| 50 | + for (let c = cursor.parent; c != null; c = c.parent) { |
| 51 | + const indent = c.messages.get("myIndent") as string | undefined; |
| 52 | + if (indent !== undefined) { |
| 53 | + const kind = c.messages.get("indentKind") as IndentKind ?? 'continuation'; |
| 54 | + return [indent, kind]; |
| 55 | + } |
| 56 | + } |
| 57 | + return ["", 'continuation']; |
| 58 | + } |
| 59 | + |
| 60 | + private computeMyIndent(tree: J, parentMyIndent: string, parentIndentKind: IndentKind): string { |
| 61 | + if (tree.kind === J.Kind.IfElse || parentIndentKind === 'align') { |
| 62 | + return parentMyIndent; |
| 63 | + } |
| 64 | + if (parentIndentKind === 'block') { |
| 65 | + return parentMyIndent + this.singleIndent; |
| 66 | + } |
| 67 | + const hasNewline = tree.prefix?.whitespace?.includes("\n") || |
| 68 | + tree.prefix?.comments?.some(c => c.suffix.includes("\n")); |
| 69 | + return hasNewline ? parentMyIndent + this.singleIndent : parentMyIndent; |
| 70 | + } |
| 71 | + |
| 72 | + private computeIndentKind(tree: J): IndentKind { |
| 73 | + switch (tree.kind) { |
| 74 | + case J.Kind.Block: |
| 75 | + case J.Kind.Case: |
| 76 | + return 'block'; |
| 77 | + case JS.Kind.CompilationUnit: |
| 78 | + return 'align'; |
| 79 | + default: |
| 80 | + return 'continuation'; |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + override async postVisit(tree: J, _p: P): Promise<J | undefined> { |
| 85 | + if (this.stopAfter != null && isScope(this.stopAfter, tree)) { |
| 86 | + this.cursor?.root.messages.set("stop", true); |
| 87 | + } |
| 88 | + |
| 89 | + const myIndent = this.cursor.messages.get("myIndent") as string | undefined; |
| 90 | + if (myIndent === undefined) { |
| 91 | + return tree; |
| 92 | + } |
| 93 | + |
| 94 | + let result = tree; |
| 95 | + if (result.prefix?.whitespace?.includes("\n")) { |
| 96 | + result = produce(result, draft => { |
| 97 | + draft.prefix!.whitespace = this.combineIndent(draft.prefix!.whitespace, myIndent); |
| 98 | + }); |
| 99 | + } |
| 100 | + |
| 101 | + if (result.kind === J.Kind.Block) { |
| 102 | + result = this.normalizeBlockEnd(result as J.Block, myIndent); |
| 103 | + } else if (result.kind === JS.Kind.JsxTag) { |
| 104 | + result = this.normalizeJsxTagEnd(result as JSX.Tag, myIndent); |
| 105 | + } |
| 106 | + |
| 107 | + return result; |
| 108 | + } |
| 109 | + |
| 110 | + private normalizeBlockEnd(block: J.Block, myIndent: string): J.Block { |
| 111 | + const effectiveLastWs = lastWhitespace(block.end); |
| 112 | + if (!effectiveLastWs.includes("\n")) { |
| 113 | + return block; |
| 114 | + } |
| 115 | + return produce(block, draft => { |
| 116 | + draft.end = replaceLastWhitespace(draft.end, ws => this.combineIndent(ws, myIndent)); |
| 117 | + }); |
| 118 | + } |
| 119 | + |
| 120 | + private normalizeJsxTagEnd(tag: JSX.Tag, myIndent: string): JSX.Tag { |
| 121 | + if (!tag.children || tag.children.length === 0) { |
| 122 | + return tag; |
| 123 | + } |
| 124 | + const lastChild = tag.children[tag.children.length - 1]; |
| 125 | + if (lastChild.kind !== J.Kind.Literal || !lastChild.prefix.whitespace.includes("\n")) { |
| 126 | + return tag; |
| 127 | + } |
| 128 | + return produce(tag, draft => { |
| 129 | + const lastChildDraft = draft.children![draft.children!.length - 1]; |
| 130 | + lastChildDraft.prefix.whitespace = this.combineIndent(lastChildDraft.prefix.whitespace, myIndent); |
| 131 | + }); |
| 132 | + } |
| 133 | + |
| 134 | + public async visitContainer<T extends J>(container: J.Container<T>, p: P): Promise<J.Container<T>> { |
| 135 | + const parentIndent = this.cursor.messages.get("myIndent") as string ?? ""; |
| 136 | + const elementsIndent = container.before.whitespace.includes("\n") |
| 137 | + ? parentIndent + this.singleIndent |
| 138 | + : parentIndent; |
| 139 | + |
| 140 | + const savedMyIndent = this.cursor.messages.get("myIndent"); |
| 141 | + this.cursor.messages.set("myIndent", elementsIndent); |
| 142 | + let ret = await super.visitContainer(container, p); |
| 143 | + if (savedMyIndent !== undefined) { |
| 144 | + this.cursor.messages.set("myIndent", savedMyIndent); |
| 145 | + } |
| 146 | + |
| 147 | + if (ret.before.whitespace.includes("\n")) { |
| 148 | + ret = produce(ret, draft => { |
| 149 | + draft.before.whitespace = this.combineIndent(draft.before.whitespace, elementsIndent); |
| 150 | + }); |
| 151 | + } |
| 152 | + |
| 153 | + if (ret.elements.length > 0) { |
| 154 | + const effectiveLastWs = lastWhitespace(ret.elements[ret.elements.length - 1].after); |
| 155 | + if (effectiveLastWs.includes("\n")) { |
| 156 | + ret = produce(ret, draft => { |
| 157 | + const lastDraft = draft.elements[draft.elements.length - 1]; |
| 158 | + lastDraft.after = replaceLastWhitespace(lastDraft.after, ws => this.combineIndent(ws, parentIndent)); |
| 159 | + }); |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + return ret; |
| 164 | + } |
| 165 | + |
| 166 | + public async visitLeftPadded<T extends J | J.Space | number | string | boolean>( |
| 167 | + left: J.LeftPadded<T>, |
| 168 | + p: P |
| 169 | + ): Promise<J.LeftPadded<T> | undefined> { |
| 170 | + const ret = await super.visitLeftPadded(left, p); |
| 171 | + if (ret === undefined || !ret.before.whitespace.includes("\n")) { |
| 172 | + return ret; |
| 173 | + } |
| 174 | + const parentIndent = this.cursor.messages.get("myIndent") as string ?? ""; |
| 175 | + return produce(ret, draft => { |
| 176 | + draft.before.whitespace = this.combineIndent(draft.before.whitespace, parentIndent + this.singleIndent); |
| 177 | + }); |
| 178 | + } |
| 179 | + |
| 180 | + async visit<R extends J>(tree: Tree, p: P, parent?: Cursor): Promise<R | undefined> { |
| 181 | + if (this.cursor?.getNearestMessage("stop") != null) { |
| 182 | + return tree as R; |
| 183 | + } |
| 184 | + |
| 185 | + if (parent) { |
| 186 | + this.cursor = new Cursor(tree, parent); |
| 187 | + this.setupAncestorIndents(); |
| 188 | + } |
| 189 | + |
| 190 | + return await super.visit(tree, p) as R; |
| 191 | + } |
| 192 | + |
| 193 | + private setupAncestorIndents(): void { |
| 194 | + const path: Cursor[] = []; |
| 195 | + let anchorCursor: Cursor | undefined; |
| 196 | + let anchorIndent = ""; |
| 197 | + |
| 198 | + for (let c = this.cursor.parent; c; c = c.parent) { |
| 199 | + path.push(c); |
| 200 | + const v = c.value; |
| 201 | + |
| 202 | + if (this.isActualJNode(v) && !anchorCursor && v.prefix) { |
| 203 | + const ws = lastWhitespace(v.prefix); |
| 204 | + const idx = ws.lastIndexOf('\n'); |
| 205 | + if (idx !== -1) { |
| 206 | + anchorCursor = c; |
| 207 | + anchorIndent = ws.substring(idx + 1); |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + if (v.kind === JS.Kind.CompilationUnit) { |
| 212 | + if (!anchorCursor) { |
| 213 | + anchorCursor = c; |
| 214 | + anchorIndent = ""; |
| 215 | + } |
| 216 | + break; |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + if (path.length === 0) return; |
| 221 | + path.reverse(); |
| 222 | + |
| 223 | + for (const c of path) { |
| 224 | + const v = c.value; |
| 225 | + if (!this.isActualJNode(v)) continue; |
| 226 | + |
| 227 | + const savedCursor = this.cursor; |
| 228 | + this.cursor = c; |
| 229 | + if (c === anchorCursor) { |
| 230 | + c.messages.set("myIndent", anchorIndent); |
| 231 | + c.messages.set("indentKind", this.computeIndentKind(v)); |
| 232 | + } else { |
| 233 | + this.setupCursorMessagesForTree(c, v); |
| 234 | + } |
| 235 | + this.cursor = savedCursor; |
| 236 | + } |
| 237 | + } |
| 238 | + |
| 239 | + private isActualJNode(v: any): v is J { |
| 240 | + return (isJava(v) || isJavaScript(v)) && |
| 241 | + v.kind !== J.Kind.Container && |
| 242 | + v.kind !== J.Kind.LeftPadded && |
| 243 | + v.kind !== J.Kind.RightPadded; |
| 244 | + } |
| 245 | + |
| 246 | + private combineIndent(oldWs: string, newIndent: string): string { |
| 247 | + const lastNewline = oldWs.lastIndexOf("\n"); |
| 248 | + return oldWs.substring(0, lastNewline + 1) + newIndent; |
| 249 | + } |
| 250 | +} |
0 commit comments