Skip to content

Commit

Permalink
GitHub Issue #9 - support Rule 3 indentation
Browse files Browse the repository at this point in the history
  • Loading branch information
oakmac authored Mar 9, 2024
1 parent c9a2edd commit d221663
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 8 deletions.
85 changes: 80 additions & 5 deletions lib/clojurefmt.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@
return a.length
}

// returns the last item in an Array
// returns null if the Array has no items
function arrayLast (a) {
const s = arraySize(a)
if (s === 0) {
return null
} else {
return a[dec(s)]
}
}

function strConcat (s1, s2) {
return '' + s1 + s2
}
Expand Down Expand Up @@ -557,6 +568,10 @@

// TODO: some of this information should be calculated when parsing

function isNonEmptyTextNode (node) {
return node && isString(node.text) && node.text !== ''
}

function isNewlineNode (n) {
return n && isString(n.text) && strIncludes(n.text, '\n')
}
Expand Down Expand Up @@ -667,24 +682,31 @@
if (!wrappingOpener) {
return 0
} else {
const nextNode = wrappingOpener._nextWithText
const nextNodeAfterOpener = wrappingOpener._nextWithText
const openerTextLength = strLen(wrappingOpener.text)
const directlyUnderneathOpener = wrappingOpener._colIdx + openerTextLength

if (isReaderConditionalOpener(wrappingOpener)) {
return directlyUnderneathOpener
// TODO: pretty sure one or both of these conditions can be removed or combined
} else if (nextNode && isParenOpener(nextNode)) {
} else if (nextNodeAfterOpener && isParenOpener(nextNodeAfterOpener)) {
return inc(wrappingOpener._colIdx)
} else if (isOneSpaceOpener(wrappingOpener)) {
return inc(wrappingOpener._colIdx)
} else {
// else indent two spaces from the wrapping opener
// else indent two spaces from the wrapping opener
return inc(inc(wrappingOpener._colIdx))
}
}
}

function numSpacesAfterNewline (newlineNode) {
// TODO: make this language neutral
const x = newlineNode.text.split('\n')
const lastX = arrayLast(x)
return strLen(lastX)
}

// ---------------------------------------------------------------------------
// Formatter

Expand All @@ -703,6 +725,7 @@
let idx = 0
let outTxt = ''
let outputTxtContainsChars = false
let lineIdx = 0

// FIXME: need a running paren stack of openers (who are not closed)
// use this on a newline to determine indentation level
Expand All @@ -727,6 +750,12 @@
const nodeWithExtraInfo = node
nodeWithExtraInfo._colIdx = colIdx
nodeWithExtraInfo._nextWithText = nextTextNode
nodeWithExtraInfo._parenOpenerLineIdx = lineIdx
// an array of tokens on the first line of this parenStack
// used to determine if Rule 3 indentation applies
nodeWithExtraInfo._openingLineTokens = []
nodeWithExtraInfo._rule3Active = false
nodeWithExtraInfo._rule3NumSpaces = 0

parenStack.push(nodeWithExtraInfo) // TODO: abstract to language neutral

Expand All @@ -741,6 +770,17 @@
parenStack.pop() // TODO: abstract to language neutral
}

// add token nodes to the top of the parenStack if we are on the opening line
const topOfTheParenStack = stackPeek(parenStack, 0)
if (topOfTheParenStack && isTokenNode(node)) {
const onOpeningLineOfParenStack = lineIdx === topOfTheParenStack._parenOpenerLineIdx
if (onOpeningLineOfParenStack) {
node._colIdx = colIdx
node._lineIdx = lineIdx
topOfTheParenStack._openingLineTokens.push(node)
}
}

// remove whitespace before a closer (remove-surrounding-whitespace?)
if (currentNodeIsWhitespace && !isNewlineNode(node) && isParenCloser(nextTextNode)) {
skipPrintingThisNode = true
Expand Down Expand Up @@ -781,15 +821,50 @@
// if we are not at the end of the nodes array
if (inc(idx) < numNodes) {
const topOfTheParenStack = stackPeek(parenStack, 0)
const numSpaces = numSpacesForIndentation(topOfTheParenStack)
const numSpacesOnNextLine = numSpacesAfterNewline(node)

// are we inside of a parenStack that crosses into the next line?
if (topOfTheParenStack && topOfTheParenStack._parenOpenerLineIdx === lineIdx) {
// NOTE: we start this index at 1 in order to skip the first token on the paren opener line
// ie: we want to look at tokens 2 or later for Rule 3 alignment
let openingLineTokensIdx = 1
const numOpeningLineTokens = arraySize(topOfTheParenStack._openingLineTokens)
while (openingLineTokensIdx < numOpeningLineTokens) {
const openingLineToken = topOfTheParenStack._openingLineTokens[openingLineTokensIdx]
if (openingLineToken._colIdx === numSpacesOnNextLine) {
// the first token on this line is vertically aligned with a first-line token:
// Rule 3 is activated 👍
topOfTheParenStack._rule3Active = true
topOfTheParenStack._rule3NumSpaces = numSpacesOnNextLine

// we can exit the loop now
openingLineTokensIdx = inc(numOpeningLineTokens)
}

openingLineTokensIdx = inc(openingLineTokensIdx)
}
}

let numSpaces = 0
if (topOfTheParenStack && topOfTheParenStack._rule3Active) {
numSpaces = topOfTheParenStack._rule3NumSpaces
} else {
numSpaces = numSpacesForIndentation(topOfTheParenStack)
}

const indentationStr = repeatString(' ', numSpaces)
outTxt = strConcat(outTxt, strConcat(newlineStr, indentationStr))

// reset the colIdx
colIdx = strLen(indentationStr)

// increment the lineIdx
lineIdx = inc(lineIdx)
if (isDoubleNewline) {
lineIdx = inc(lineIdx)
}
}
} else if (isString(node.text) && node.text !== '') {
} else if (isNonEmptyTextNode(node)) {
const isTokenFollowedByOpener = isTokenNode(node) && nextTextNode && isParenOpener(nextTextNode)
const isParenCloserFollowedByText = isParenCloser(node) && nextTextNode && (isTokenNode(nextTextNode) || isParenOpener(nextTextNode))
const addSpaceAfterThisNode = isTokenFollowedByOpener || isParenCloserFollowedByText
Expand Down
4 changes: 2 additions & 2 deletions test/format.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ test('All test_format/ cases should have unique names', () => {
// only those cases will run
const onlyRunCertainTests = false
const certainTests = new Set()
certainTests.add('Rule 3 Indentation')
certainTests.add('Rule 3 Indentation 1')

const ignoreSomeTests = true
const ignoreSomeTests = false
const ignoreTests = new Set()
ignoreTests.add('Rule 3 Indentation')

Expand Down
37 changes: 36 additions & 1 deletion test_format/format.eno
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@
baz)
--Expected

# Rule 3 Indentation
# Rule 3 Indentation 1

--Input
;; https://github.com/clj-commons/formatter/issues/9#issuecomment-446167649
Expand Down Expand Up @@ -291,3 +291,38 @@
baz
qux)
--Expected


# Rule 3 Indentation 2

--Input
(foo bar
baz
qux)

(zap bar baz
qux
wizzle
gee)

(yes tam
bif
bag
hop)
--Input

--Expected
(foo bar
baz
qux)

(zap bar baz
qux
wizzle
gee)

(yes tam
bif
bag
hop)
--Expected

0 comments on commit d221663

Please sign in to comment.