diff --git a/lib/clojurefmt.js b/lib/clojurefmt.js index 392fd8b..584d487 100644 --- a/lib/clojurefmt.js +++ b/lib/clojurefmt.js @@ -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 } @@ -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') } @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/test/format.test.js b/test/format.test.js index 70ca282..7ddb7fa 100644 --- a/test/format.test.js +++ b/test/format.test.js @@ -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') diff --git a/test_format/format.eno b/test_format/format.eno index 75dc252..ff74544 100644 --- a/test_format/format.eno +++ b/test_format/format.eno @@ -262,7 +262,7 @@ baz) --Expected -# Rule 3 Indentation +# Rule 3 Indentation 1 --Input ;; https://github.com/clj-commons/formatter/issues/9#issuecomment-446167649 @@ -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