diff --git a/src/Formatter.spec.ts b/src/Formatter.spec.ts index 90746c1..a7de5eb 100644 --- a/src/Formatter.spec.ts +++ b/src/Formatter.spec.ts @@ -1133,6 +1133,167 @@ end sub`; it('works for arrays with objects in them on separate lines', () => { formatEqual(`theVar = [\n {\n name = "bob"\n }\n]`); }); + + // https://github.com/rokucommunity/brighterscript-formatter/issues/85 + // When `{` and `function` appear on the same line, each independently triggers an indent, + // causing the body to be triple-indented instead of double-indented. + // The fix uses lookahead: if a line opens a bracket scope and a function/sub on the same + // line, and both close together on a later line, only 1 additional indent level is applied. + it('does not double-indent when { and function are on the same line', () => { + expect(formatter.format(undent` + sub main() + key_9 = { runtimeCheck: function() as boolean + return true + end function } + end sub + `, { formatMultiLineObjectsAndArrays: false })).to.equal(undent` + sub main() + key_9 = { runtimeCheck: function() as boolean + return true + end function } + end sub + `); + }); + + it('does not double-indent when [ and function are on the same line', () => { + expect(formatter.format(undent` + sub main() + items = [function() as boolean + return true + end function] + end sub + `, { formatMultiLineObjectsAndArrays: false })).to.equal(undent` + sub main() + items = [function() as boolean + return true + end function] + end sub + `); + }); + + it('does not double-indent inline AA with function value nested inside outer AA (issue #85)', () => { + expect(formatter.format(undent` + sub main() + config = { + key_1: "value1" + key_9: { env: ["any"], runtimeCheck: function() as boolean + return true + end function } + } + end sub + `, { formatMultiLineObjectsAndArrays: false })).to.equal(undent` + sub main() + config = { + key_1: "value1" + key_9: { env: ["any"], runtimeCheck: function() as boolean + return true + end function } + } + end sub + `); + }); + + it('does not double-indent multiple inline function values in the same AA (issue #85)', () => { + expect(formatter.format(undent` + sub main() + config = { + key_9: { runtimeCheck: function() as boolean + return true + end function } + key_10: { runtimeCheck: function() as boolean + return false + end function } + } + end sub + `, { formatMultiLineObjectsAndArrays: false })).to.equal(undent` + sub main() + config = { + key_9: { runtimeCheck: function() as boolean + return true + end function } + key_10: { runtimeCheck: function() as boolean + return false + end function } + } + end sub + `); + }); + + it('does not double-indent when deeply nested (issue #85)', () => { + expect(formatter.format(undent` + namespace tests + class TestStuff + function test() + m.call({ + key_9: { runtimeCheck: function() as boolean + return true + end function } + }) + end function + end class + end namespace + `, { formatMultiLineObjectsAndArrays: false })).to.equal(undent` + namespace tests + class TestStuff + function test() + m.call({ + key_9: { runtimeCheck: function() as boolean + return true + end function } + }) + end function + end class + end namespace + `); + }); + + it('empty lines inside [{ }] do not break surrounding indentation', () => { + formatEqualTrim(` + sub test() + array = [{ + + }] + if true then + print true + end if + end sub + `); + }); + + // https://github.com/rokucommunity/brighterscript-formatter/issues/85 + it('does not double-indent when { and sub are on the same line (issue #85)', () => { + expect(formatter.format(undent` + sub main() + m.foo = { callback: sub() + doThing() + end sub } + end sub + `, { formatMultiLineObjectsAndArrays: false })).to.equal(undent` + sub main() + m.foo = { callback: sub() + doThing() + end sub } + end sub + `); + }); + + it('does not apply double-indent fix when bracket closer has non-end-function token before it (issue #85)', () => { + // When `}` has something other than `end function`/`end sub` immediately before it + // on the same line (e.g. a second AA entry), the fix intentionally does not apply. + expect(formatter.format(undent` + sub main() + m.foo = { cb: function() + return true + end function, other: 1 } + end sub + `, { formatMultiLineObjectsAndArrays: false })).to.equal(undent` + sub main() + m.foo = { cb: function() + return true + end function, other: 1 } + end sub + `); + }); }); describe('indentSpaceCount', () => { @@ -1226,6 +1387,45 @@ end sub`; let program = `if (request.AsyncGetToString())\n scope.immediatelyFailed = false\nelse\n scope.immediatelyFailed = true\nend if`; expect(formatter.format(program)).to.equal(program); }); + + // https://github.com/rokucommunity/brighterscript-formatter/issues/82 + it('does not de-indent when ".catch" is used as a method call (issue #82)', () => { + formatEqual(undent` + sub main() + if true then + m.promise.catch(sub(err) + print err + end sub) + end if + end sub + `); + }); + + it('does not de-indent surrounding if blocks when chaining lambdas with typecast args (issue #82)', () => { + formatEqual(undent` + sub main() + if true then + m.list.filter(function(item as object) as boolean + return item.active = true + end function).forEach(sub(item as object) + print item + end sub) + end if + end sub + `); + }); + + it('does not de-indent when lambda has a typed parameter in a method chain (issue #82)', () => { + formatEqual(undent` + sub main() + if true then + someObject.next(function(val as integer) as string + return val.toStr() + end function) + end if + end sub + `); + }); }); describe('typeCase', () => { diff --git a/src/formatters/IndentFormatter.ts b/src/formatters/IndentFormatter.ts index f5d6919..f96f4d6 100644 --- a/src/formatters/IndentFormatter.ts +++ b/src/formatters/IndentFormatter.ts @@ -21,12 +21,18 @@ export class IndentFormatter { let parentIndentTokenKinds: TokenKind[] = []; + // Tracks bracket-closer tokens (e.g. `}` or `]`) that should be skipped when + // computing indentation offsets. Used to prevent double-indenting when a bracket + // opener and a function/sub keyword appear on the same line and both close together + // (e.g. `{ function() as boolean\n return true\nend function }`). + const skipOutdentTokens = new Set(); + //the list of output tokens let result: Token[] = []; //set the loop to run for a max of double the number of tokens we found so we don't end up with an infinite loop for (let lineTokens of this.splitTokensByLine(tokens)) { - const { currentLineOffset, nextLineOffset } = this.processLine(lineTokens, tokens, ifStatements, parentIndentTokenKinds); + const { currentLineOffset, nextLineOffset } = this.processLine(lineTokens, tokens, ifStatements, parentIndentTokenKinds, skipOutdentTokens); //uncomment the next line to debug indent/outdent issues // console.log(currentLineOffset.toString().padStart(3, ' '), nextLineOffset.toString().padStart(3, ' '), lineTokens.map(x => x.text).join('').replace(/\r?\n/, '').replace(/^\s*/, '')); @@ -50,7 +56,8 @@ export class IndentFormatter { lineTokens: Token[], tokens: Token[], ifStatements: Map, - parentIndentTokenKinds: TokenKind[] + parentIndentTokenKinds: TokenKind[], + skipOutdentTokens: Set ): { currentLineOffset: number; nextLineOffset: number } { const getParentIndentTokenKind = () => { const parentIndentTokenKind = parentIndentTokenKinds.length > 0 ? parentIndentTokenKinds[parentIndentTokenKinds.length - 1] : undefined; @@ -61,6 +68,10 @@ export class IndentFormatter { let nextLineOffset = 0; let foundIndentorThisLine = false; let firstNonWhitespaceToken: Token | null = null; + // Tracks the last multi-line bracket opener (`{` or `[`) and its closer pushed as an indent + // on this line. Storing the closer avoids a redundant getClosingToken call during compound + // indent detection. + let lastBracketIndentInfo: { opener: Token; closer: Token } | null = null; for (let i = 0; i < lineTokens.length; i++) { let token = lineTokens[i]; @@ -157,6 +168,8 @@ export class IndentFormatter { parentIndentTokenKinds.push(token.kind); //don't double indent if this is `[[...\n...]]` or `[{...\n...}]` + // Note: this block is before the lastBracketIndentTokenOnLine update below so + // that skipped `{`/`[` tokens don't overwrite the tracked bracket. if ( //is open square token.kind === TokenKind.LeftSquareBracket && @@ -175,6 +188,37 @@ export class IndentFormatter { i++; } } + + // Track the last multi-line bracket opener on this line for compound-indent detection below. + // Only track brackets whose closer is on a different line — single-line brackets like + // `["any"]` open and close on the same line and don't affect body indentation. + if (token.kind === TokenKind.LeftCurlyBrace || token.kind === TokenKind.LeftSquareBracket) { + const bCloseKind = token.kind === TokenKind.LeftCurlyBrace ? TokenKind.RightCurlyBrace : TokenKind.RightSquareBracket; + const bCloser = util.getClosingToken(tokens, tokens.indexOf(token), token.kind, bCloseKind); + if (bCloser && bCloser.range.start.line !== token.range.start.line) { + lastBracketIndentInfo = { opener: token, closer: bCloser }; + } + } + + // Don't double-indent when a bracket opener (`{` or `[`) and a function/sub keyword + // both appear on the same line and both close together on a later line, e.g.: + // key: { runtimeCheck: function() as boolean + // return true <-- should be 1 level deeper, not 2 + // end function } + if ( + foundIndentorThisLine && + CallableKeywordTokenKinds.includes(token.kind) && + lastBracketIndentInfo !== null + ) { + const { closer: bracketCloser } = lastBracketIndentInfo; + const tokenBeforeCloser = util.getPreviousNonWhitespaceToken(tokens, tokens.indexOf(bracketCloser), true); + if (tokenBeforeCloser && (tokenBeforeCloser.kind === TokenKind.EndFunction || tokenBeforeCloser.kind === TokenKind.EndSub)) { + // The function closes inside the bracket — undo the extra indent level and + // mark the bracket closer to be skipped on the closing line + nextLineOffset--; + skipOutdentTokens.add(bracketCloser); + } + } } else if (this.isOutdentToken(token, nextNonWhitespaceToken)) { //do not un-indent if this is a `next` or `endclass` token preceeded by a period if ( @@ -184,6 +228,14 @@ export class IndentFormatter { continue; } + // This bracket closer was marked to be skipped by the compound-indent fix above. + // Pop the stack to keep it balanced but don't adjust offsets. + if (skipOutdentTokens.has(token)) { + skipOutdentTokens.delete(token); + parentIndentTokenKinds.pop(); + continue; + } + nextLineOffset--; if (foundIndentorThisLine === false) { currentLineOffset--;