Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle double-indent followed by double-outdent #89

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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', () => {
TwitchBronBron marked this conversation as resolved.
Show resolved Hide 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
Loading