Skip to content
198 changes: 198 additions & 0 deletions src/Formatter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Comment thread
TwitchBronBron marked this conversation as resolved.
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', () => {
Expand Down
154 changes: 154 additions & 0 deletions src/formatters/IndentFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -151,6 +152,10 @@ export class IndentFormatter {
}

nextLineOffset--;
if (OutdentSpacerTokenKinds.includes(token.kind)) {
outdentCount++;
}

if (foundIndentorThisLine === false) {
currentLineOffset--;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand Down