diff --git a/src/Formatter.spec.ts b/src/Formatter.spec.ts index 6d6ed60..825d8c6 100644 --- a/src/Formatter.spec.ts +++ b/src/Formatter.spec.ts @@ -251,6 +251,204 @@ describe('Formatter', () => { end sub `); }); + + it('empty newlines in AA do not break indentation', () => { + formatEqual(undent` + sub test() + array = [{ + + }] + if true then + print true + end if + end sub + `); + }); + + it('formats outdents', () => { + expect(formatter.format(undent` + temp = { + key_9: { env: ["any"], themes: ["any"], runtimeCheck: function() as boolean + return true + end function } + } + `, { + formatMultiLineObjectsAndArrays: false + })).to.equal(undent` + temp = { + key_9: { env: ["any"], themes: ["any"], runtimeCheck: function() as boolean + return true + end function } + } + `); + + expect(formatter.format(undent` + function test() + temp = { + key_9: { env: ["any"], themes: ["any"], runtimeCheck: function() as boolean + return true + end function + } + } + end function + `, { + formatMultiLineObjectsAndArrays: false + })).to.equal(undent` + function test() + temp = { + key_9: { env: ["any"], themes: ["any"], runtimeCheck: function() as boolean + return true + end function + } + } + end function + `); + + expect(formatter.format(undent` + function test() + temp = { + key_9: { + env: ["any"], themes: ["any"], runtimeCheck: function() as boolean + return true + end function + } + } + end function + `, { + formatMultiLineObjectsAndArrays: false + })).to.equal(undent` + function test() + temp = { + key_9: { + env: ["any"], themes: ["any"], runtimeCheck: function() as boolean + return true + end function + } + } + end function + `); + + expect(formatter.format(undent` + namespace tests + class TestStuff + function _() + m.assertEqual(sanitize({ + key_0: { env: [], themes: ["any"] } + key_1: { env: ["any"], themes: ["test"] } + key_2: { env: ["prod"], themes: ["test"] } + key_3: { env: ["prod"], themes: ["test"] } + key_4: { env: ["prod", "qa"], themes: ["test", "test"] } + key_5: { env: ["dev", "qa"], themes: ["test"] } + key_6: { env: ["dev", "qa"], themes: ["test", "test"] } + key_7: { env: ["dev", "qa"], themes: ["test"] } + key_8: { env: [], themes: [] } + key_9: { env: ["any"], themes: ["any"], runtimeCheck: function() as boolean + return true + end function } + key_10: { env: ["any"], themes: ["any"], runtimeCheck: function() as boolean + return false + end function } + key_11: { env: ["dev"], themes: ["any"], runtimeCheck: function() as boolean + return true + end function } + key_12: { env: ["any"], themes: ["test"], runtimeCheck: function() as boolean + return true + end function } + key_13: { { env: ["any"], themes: ["test"], runtimeCheck: function() as boolean + return true + end function } } + }, "prod", "test"), { + enabled: ["key_1", "key_2", "key_4", "key_9"] + available: ["key_0", "key_1", "key_10", "key_11", "key_12", "key_2", "key_3", "key_4", "key_5", "key_6", "key_7", "key_8", "key_9"] + }) + end function + end class + end namespace + `, { + formatMultiLineObjectsAndArrays: false + })).to.equal(undent` + namespace tests + class TestStuff + function _() + m.assertEqual(sanitize({ + key_0: { env: [], themes: ["any"] } + key_1: { env: ["any"], themes: ["test"] } + key_2: { env: ["prod"], themes: ["test"] } + key_3: { env: ["prod"], themes: ["test"] } + key_4: { env: ["prod", "qa"], themes: ["test", "test"] } + key_5: { env: ["dev", "qa"], themes: ["test"] } + key_6: { env: ["dev", "qa"], themes: ["test", "test"] } + key_7: { env: ["dev", "qa"], themes: ["test"] } + key_8: { env: [], themes: [] } + key_9: { env: ["any"], themes: ["any"], runtimeCheck: function() as boolean + return true + end function } + key_10: { env: ["any"], themes: ["any"], runtimeCheck: function() as boolean + return false + end function } + key_11: { env: ["dev"], themes: ["any"], runtimeCheck: function() as boolean + return true + end function } + key_12: { env: ["any"], themes: ["test"], runtimeCheck: function() as boolean + return true + end function } + key_13: { { env: ["any"], themes: ["test"], runtimeCheck: function() as boolean + return true + end function } } + }, "prod", "test"), { + enabled: ["key_1", "key_2", "key_4", "key_9"] + available: ["key_0", "key_1", "key_10", "key_11", "key_12", "key_2", "key_3", "key_4", "key_5", "key_6", "key_7", "key_8", "key_9"] + }) + end function + end class + end namespace + `); + + formatEqual(undent` + sub createSections(navigationAction as object) + m.sections = [FormatJson({ + type: "test" + components: [{ + type: "test" + group_id: "1" + }] + slug: "test" + })] + end sub + `, undefined, { + formatMultiLineObjectsAndArrays: false + }); + + formatEqual(undent` + sub createSections(navigationAction as object) + m.sections = [parser.parseList({ + components: [{ + buttons: [{ + actions: { + on_click: [get.value(navigationAction, "_retryAction")] + } + }] + }] + })] + end sub + `, undefined, { + formatMultiLineObjectsAndArrays: false + }); + + formatEqual(undent` + namespace alpha + namespace beta + sub createSections() + m.sections = [ + FormatJson({ + })] + end sub + end namespace + end namespace + `, undefined, { + formatMultiLineObjectsAndArrays: false + }); + }); }); describe('formatMultiLineObjectsAndArrays', () => { diff --git a/src/formatters/IndentFormatter.ts b/src/formatters/IndentFormatter.ts index cc54164..d4fc18e 100644 --- a/src/formatters/IndentFormatter.ts +++ b/src/formatters/IndentFormatter.ts @@ -59,6 +59,7 @@ export class IndentFormatter { let currentLineOffset = 0; let nextLineOffset = 0; let foundIndentorThisLine = false; + let outdentCount = 0; for (let i = 0; i < lineTokens.length; i++) { let token = lineTokens[i]; @@ -151,6 +152,10 @@ export class IndentFormatter { } nextLineOffset--; + if (OutdentSpacerTokenKinds.includes(token.kind)) { + outdentCount++; + } + if (foundIndentorThisLine === false) { currentLineOffset--; } @@ -189,12 +194,141 @@ export class IndentFormatter { // tabCount--; // } } + + //check if next multiple indents are followed by multiple outdents and update indentation accordingly + if (nextLineOffset > 1) { + nextLineOffset = this.lookaheadSameLineMultiOutdents(tokens, lineTokens[lineTokens.length - 1], nextLineOffset, currentLineOffset); + } else if (outdentCount > 0) { + //if multiple outdents on same line then outdent only once + if (currentLineOffset < 0) { + currentLineOffset = -1; + } + + if (nextLineOffset < 0) { + //look back to get offset of last closing token pair + nextLineOffset = this.getNextLineOffset(tokens, lineTokens); + } + } + return { currentLineOffset: currentLineOffset, nextLineOffset: nextLineOffset }; } + /** + * Lookback to find the matching opening token and then calculates outdents + * @param tokens the array of tokens in a file + * @param lineTokens token of curent line + */ + private getNextLineOffset(tokens: Token[], lineTokens: Token[]): number { + let nextLineOffset = -1; + + let lineLastToken = util.getPreviousNonWhitespaceToken(lineTokens, lineTokens.length - 1); + let curLineLastToken = lineLastToken !== undefined ? lineLastToken : lineTokens[lineTokens.length - 1]; + + let closeKind = curLineLastToken.kind; + let openKind = this.getOpeningTokenKind(closeKind); + + if ((curLineLastToken.kind === TokenKind.RightCurlyBrace) || (curLineLastToken.kind === TokenKind.RightSquareBracket)) { + let openCount = 0; + let isWhiteSpaceToken = (lineTokens[0].kind === TokenKind.Whitespace) || (lineTokens[0].kind === TokenKind.Newline); + let lineFirstToken = isWhiteSpaceToken ? util.getNextNonWhitespaceToken(lineTokens, 0) : lineTokens[0]; + let curLineStart = lineFirstToken?.range.start.character ? lineFirstToken?.range.start.character : 0; + + let openerFound = false; + let currentIndex = lineTokens.indexOf(curLineLastToken); + + let lines = this.splitTokensByLine(tokens); + + for (let lineIndex = curLineLastToken.range.start.line; lineIndex >= 0; lineIndex--) { + let lineToken = lines[lineIndex]; + + if (lineToken.length === 1 && lineToken[0].kind === TokenKind.Newline) { + continue; + } + isWhiteSpaceToken = (lineToken[0].kind === TokenKind.Whitespace) || (lineToken[0].kind === TokenKind.Newline); + let firstToken = isWhiteSpaceToken ? util.getNextNonWhitespaceToken(lineToken, 0) : lineToken[0]; + let lineStartPosition = firstToken ? firstToken.range.start.character : 0; + + let lineTokenIndex = currentIndex > -1 ? currentIndex : lineToken.length - 1; + currentIndex = -1; + + for (let i = lineTokenIndex; i >= 0; i--) { + let token = lineToken[i]; + if (token.kind === TokenKind.Whitespace) { + continue; + } + + if (token.kind === openKind) { + openCount++; + } else if (token.kind === closeKind) { + openCount--; + } + + if (openCount === 0) { + openerFound = true; + break; + } + } + + if (lineStartPosition < curLineStart) { + nextLineOffset--; + curLineStart = lineStartPosition; + } + + if (openerFound) { + break; + } + } + } + return nextLineOffset; + } + + /** + * Lookahead if next line with oudents are same as indents on current line + * @param tokens the array of tokens in a file + * @param curLineToken token of curent line + * @param nextLineOffset the number of tabs to indent the next line + * @param currentLineOffset the number of tabs to indent the current line + */ + private lookaheadSameLineMultiOutdents(tokens: Token[], curLineToken: Token, nextLineOffset: number, currentLineOffset: number): number { + let outdentCount = 0; + let tokenLineNum = 0; + let currentLineTokenIndex = tokens.indexOf(curLineToken); + + for (let i = currentLineTokenIndex + 1; i < tokens.length; i++) { + let token = tokens[i]; + if (token.kind !== TokenKind.Whitespace) { + let openingTokenKind = this.getOpeningTokenKind(token.kind); + //next line with outdents + if (OutdentSpacerTokenKinds.includes(token.kind) && openingTokenKind) { + let opener = this.getOpeningToken(tokens, i, openingTokenKind, token.kind); + if (opener && opener.range.start.line === curLineToken.range.start.line) { + if (tokenLineNum === 0) { + tokenLineNum = token.range.start.line; + } + + if (token.range.start.line === tokenLineNum) { + outdentCount++; + } + } + } + if (tokenLineNum > 0 && token.range.start.line !== tokenLineNum) { + break; + } + } + } + + //if outdents on next line with outdents = indents on current line then indent next line by one tab only + if (outdentCount > 0) { + if (outdentCount === nextLineOffset) { + nextLineOffset = currentLineOffset + 1; + } + } + return nextLineOffset; + } + /** * Ensure the list of tokens contains the correct number of tabs * @param tokens the array of tokens to be modified in-place @@ -314,6 +448,26 @@ export class IndentFormatter { } } + /** + * Returns opening token kind of the tokenkind passed + */ + public getOpeningTokenKind(tokenKind: TokenKind) { + if (tokenKind === TokenKind.RightCurlyBrace) { + return TokenKind.LeftCurlyBrace; + } else if (tokenKind === TokenKind.RightParen) { + return TokenKind.LeftParen; + } else if (tokenKind === TokenKind.RightSquareBracket) { + return TokenKind.LeftSquareBracket; + } else if (tokenKind === TokenKind.EndIf) { + return TokenKind.If; + } else if (tokenKind === TokenKind.EndFunction) { + return TokenKind.Function; + } else if (tokenKind === TokenKind.EndSub) { + return TokenKind.Sub; + } + return undefined; + } + /** * Determines if this is an outdent token */