2025-10-15 10:12:44 +03:00

1216 lines
46 KiB
Go

package format
import (
"context"
"math"
"slices"
"strings"
"unicode/utf8"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
)
/** find node that fully contains given text range */
func findEnclosingNode(r core.TextRange, sourceFile *ast.SourceFile) *ast.Node {
var find func(*ast.Node) *ast.Node
find = func(n *ast.Node) *ast.Node {
var candidate *ast.Node
n.ForEachChild(func(c *ast.Node) bool {
if r.ContainedBy(withTokenStart(c, sourceFile)) {
candidate = c
return true
}
return false
})
if candidate != nil {
result := find(candidate)
if result != nil {
return result
}
}
return n
}
return find(sourceFile.AsNode())
}
/**
* Start of the original range might fall inside the comment - scanner will not yield appropriate results
* This function will look for token that is located before the start of target range
* and return its end as start position for the scanner.
*/
func getScanStartPosition(enclosingNode *ast.Node, originalRange core.TextRange, sourceFile *ast.SourceFile) int {
adjusted := withTokenStart(enclosingNode, sourceFile)
start := adjusted.Pos()
if start == originalRange.Pos() && enclosingNode.End() == originalRange.End() {
return start
}
precedingToken := astnav.FindPrecedingToken(sourceFile, originalRange.Pos())
if precedingToken == nil {
// no preceding token found - start from the beginning of enclosing node
return enclosingNode.Pos()
}
// preceding token ends after the start of original range (i.e when originalRange.pos falls in the middle of literal)
// start from the beginning of enclosingNode to handle the entire 'originalRange'
if precedingToken.End() >= originalRange.Pos() {
return enclosingNode.Pos()
}
return precedingToken.End()
}
/*
* For cases like
* if (a ||
* b ||$
* c) {...}
* If we hit Enter at $ we want line ' b ||' to be indented.
* Formatting will be applied to the last two lines.
* Node that fully encloses these lines is binary expression 'a ||...'.
* Initial indentation for this node will be 0.
* Binary expressions don't introduce new indentation scopes, however it is possible
* that some parent node on the same line does - like if statement in this case.
* Note that we are considering parents only from the same line with initial node -
* if parent is on the different line - its delta was already contributed
* to the initial indentation.
*/
func getOwnOrInheritedDelta(n *ast.Node, options *FormatCodeSettings, sourceFile *ast.SourceFile) int {
previousLine := -1
var child *ast.Node
for n != nil {
line, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, withTokenStart(n, sourceFile).Pos())
if previousLine != -1 && line != previousLine {
break
}
if ShouldIndentChildNode(options, n, child, sourceFile) {
return options.IndentSize // !!! nil check???
}
previousLine = line
child = n
n = n.Parent
}
return 0
}
func rangeHasNoErrors(_ core.TextRange) bool {
return false
}
func prepareRangeContainsErrorFunction(errors []*ast.Diagnostic, originalRange core.TextRange) func(r core.TextRange) bool {
if len(errors) == 0 {
return rangeHasNoErrors
}
// pick only errors that fall in range
sorted := core.Filter(errors, func(d *ast.Diagnostic) bool {
return originalRange.Overlaps(d.Loc())
})
if len(sorted) == 0 {
return rangeHasNoErrors
}
slices.SortStableFunc(sorted, func(a *ast.Diagnostic, b *ast.Diagnostic) int { return a.Pos() - b.Pos() })
index := 0
return func(r core.TextRange) bool {
// in current implementation sequence of arguments [r1, r2...] is monotonically increasing.
// 'index' tracks the index of the most recent error that was checked.
for true {
if index >= len(sorted) {
// all errors in the range were already checked -> no error in specified range
return false
}
err := sorted[index]
if r.End() <= err.Pos() {
// specified range ends before the error referred by 'index' - no error in range
return false
}
if r.Overlaps(err.Loc()) {
// specified range overlaps with error range
return true
}
index++
}
return false // unreachable
}
}
type formatSpanWorker struct {
originalRange core.TextRange
enclosingNode *ast.Node
initialIndentation int
delta int
requestKind FormatRequestKind
rangeContainsError func(r core.TextRange) bool
sourceFile *ast.SourceFile
ctx context.Context
formattingScanner *formattingScanner
formattingContext *formattingContext
edits []core.TextChange
previousRange TextRangeWithKind
previousRangeTriviaEnd int
previousParent *ast.Node
previousRangeStartLine int
childContextNode *ast.Node
lastIndentedLine int
indentationOnLastIndentedLine int
visitor *ast.NodeVisitor
visitingNode *ast.Node
visitingIndenter *dynamicIndenter
visitingNodeStartLine int
visitingUndecoratedNodeStartLine int
currentRules []*ruleImpl
}
func newFormatSpanWorker(
ctx context.Context,
originalRange core.TextRange,
enclosingNode *ast.Node,
initialIndentation int,
delta int,
requestKind FormatRequestKind,
rangeContainsError func(r core.TextRange) bool,
sourceFile *ast.SourceFile,
) *formatSpanWorker {
return &formatSpanWorker{
ctx: ctx,
originalRange: originalRange,
enclosingNode: enclosingNode,
initialIndentation: initialIndentation,
delta: delta,
requestKind: requestKind,
rangeContainsError: rangeContainsError,
sourceFile: sourceFile,
currentRules: make([]*ruleImpl, 0, 32), // increaseInsertionIndex should assert there are no more than 32 rules in a given bucket
}
}
func getNonDecoratorTokenPosOfNode(node *ast.Node, file *ast.SourceFile) int {
var lastDecorator *ast.Node
if ast.HasDecorators(node) {
lastDecorator = core.FindLast(node.Modifiers().Nodes, ast.IsDecorator)
}
if file == nil {
file = ast.GetSourceFileOfNode(node)
}
if lastDecorator == nil {
return withTokenStart(node, file).Pos()
}
return scanner.SkipTrivia(file.Text(), lastDecorator.End())
}
func (w *formatSpanWorker) execute(s *formattingScanner) []core.TextChange {
w.formattingScanner = s
w.indentationOnLastIndentedLine = -1
opt := GetFormatCodeSettingsFromContext(w.ctx)
w.formattingContext = NewFormattingContext(w.sourceFile, w.requestKind, opt)
// formatting context is used by rules provider
w.visitor = ast.NewNodeVisitor(func(child *ast.Node) *ast.Node {
if child == nil {
return child
}
w.processChildNode(w.visitingNode, w.visitingIndenter, w.visitingNodeStartLine, w.visitingUndecoratedNodeStartLine, child, -1, w.visitingNode, w.visitingIndenter, w.visitingNodeStartLine, w.visitingUndecoratedNodeStartLine, false, false)
return child
}, &ast.NodeFactory{}, ast.NodeVisitorHooks{
VisitNodes: func(nodes *ast.NodeList, v *ast.NodeVisitor) *ast.NodeList {
if nodes == nil {
return nodes
}
w.processChildNodes(w.visitingNode, w.visitingIndenter, w.visitingNodeStartLine, w.visitingUndecoratedNodeStartLine, nodes, w.visitingNode, w.visitingNodeStartLine, w.visitingIndenter)
return nodes
},
})
w.formattingScanner.advance()
if w.formattingScanner.isOnToken() {
startLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, withTokenStart(w.enclosingNode, w.sourceFile).Pos())
undecoratedStartLine := startLine
if ast.HasDecorators(w.enclosingNode) {
undecoratedStartLine, _ = scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, getNonDecoratorTokenPosOfNode(w.enclosingNode, w.sourceFile))
}
w.processNode(w.enclosingNode, w.enclosingNode, startLine, undecoratedStartLine, w.initialIndentation, w.delta)
}
// Leading trivia items get attached to and processed with the token that proceeds them. If the
// range ends in the middle of some leading trivia, the token that proceeds them won't be in the
// range and thus won't get processed. So we process those remaining trivia items here.
remainingTrivia := w.formattingScanner.getCurrentLeadingTrivia()
if len(remainingTrivia) > 0 {
indentation := w.initialIndentation
if NodeWillIndentChild(w.formattingContext.Options, w.enclosingNode, nil, w.sourceFile, false) {
indentation += opt.IndentSize // !!! TODO: nil check???
}
w.indentTriviaItems(remainingTrivia, indentation, true, func(item TextRangeWithKind) {
startLine, startChar := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, item.Loc.Pos())
w.processRange(item, startLine, startChar, w.enclosingNode, w.enclosingNode, nil)
w.insertIndentation(item.Loc.Pos(), indentation, false)
})
if opt.TrimTrailingWhitespace != false {
w.trimTrailingWhitespacesForRemainingRange(remainingTrivia)
}
}
if w.previousRange != NewTextRangeWithKind(0, 0, 0) && w.formattingScanner.getTokenFullStart() >= w.originalRange.End() {
// Formatting edits happen by looking at pairs of contiguous tokens (see `processPair`),
// typically inserting or deleting whitespace between them. The recursive `processNode`
// logic above bails out as soon as it encounters a token that is beyond the end of the
// range we're supposed to format (or if we reach the end of the file). But this potentially
// leaves out an edit that would occur *inside* the requested range but cannot be discovered
// without looking at one token *beyond* the end of the range: consider the line `x = { }`
// with a selection from the beginning of the line to the space inside the curly braces,
// inclusive. We would expect a format-selection would delete the space (if rules apply),
// but in order to do that, we need to process the pair ["{", "}"], but we stopped processing
// just before getting there. This block handles this trailing edit.
var tokenInfo TextRangeWithKind
if w.formattingScanner.isOnEOF() {
tokenInfo = w.formattingScanner.readEOFTokenRange()
} else if w.formattingScanner.isOnToken() {
tokenInfo = w.formattingScanner.readTokenInfo(w.enclosingNode).token
}
if tokenInfo.Loc.Pos() == w.previousRangeTriviaEnd {
// We need to check that tokenInfo and previousRange are contiguous: the `originalRange`
// may have ended in the middle of a token, which means we will have stopped formatting
// on that token, leaving `previousRange` pointing to the token before it, but already
// having moved the formatting scanner (where we just got `tokenInfo`) to the next token.
// If this happens, our supposed pair [previousRange, tokenInfo] actually straddles the
// token that intersects the end of the range we're supposed to format, so the pair will
// produce bogus edits if we try to `processPair`. Recall that the point of this logic is
// to perform a trailing edit at the end of the selection range: but there can be no valid
// edit in the middle of a token where the range ended, so if we have a non-contiguous
// pair here, we're already done and we can ignore it.
parent := astnav.FindPrecedingToken(w.sourceFile, tokenInfo.Loc.End())
if parent == nil {
parent = w.previousParent
}
line, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, tokenInfo.Loc.Pos())
w.processPair(
tokenInfo,
line,
parent,
w.previousRange,
w.previousRangeStartLine,
w.previousParent,
parent,
nil,
)
}
}
return w.edits
}
func (w *formatSpanWorker) processChildNode(
node *ast.Node,
indenter *dynamicIndenter,
nodeStartLine int,
undecoratedNodeStartLine int,
child *ast.Node,
inheritedIndentation int,
parent *ast.Node,
parentDynamicIndentation *dynamicIndenter,
parentStartLine int,
undecoratedParentStartLine int,
isListItem bool,
isFirstListItem bool,
) int {
debug.Assert(!ast.NodeIsSynthesized(child))
if ast.NodeIsMissing(child) || isGrammarError(parent, child) {
return inheritedIndentation
}
childStartPos := scanner.GetTokenPosOfNode(child, w.sourceFile, false)
childStartLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, childStartPos)
undecoratedChildStartLine := childStartLine
if ast.HasDecorators(child) {
undecoratedChildStartLine, _ = scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, getNonDecoratorTokenPosOfNode(child, w.sourceFile))
}
// if child is a list item - try to get its indentation, only if parent is within the original range.
childIndentationAmount := -1
if isListItem && parent.Loc.ContainedBy(w.originalRange) {
childIndentationAmount = w.tryComputeIndentationForListItem(childStartPos, child.End(), parentStartLine, w.originalRange, inheritedIndentation)
if childIndentationAmount != -1 {
inheritedIndentation = childIndentationAmount
}
}
// child node is outside the target range - do not dive inside
if !w.originalRange.Overlaps(child.Loc) {
if child.End() < w.originalRange.Pos() {
w.formattingScanner.skipToEndOf(&child.Loc)
}
return inheritedIndentation
}
if child.Loc.Len() == 0 {
return inheritedIndentation
}
for w.formattingScanner.isOnToken() && w.formattingScanner.getTokenFullStart() < w.originalRange.End() {
// proceed any parent tokens that are located prior to child.getStart()
tokenInfo := w.formattingScanner.readTokenInfo(node)
if tokenInfo.token.Loc.End() > w.originalRange.End() {
return inheritedIndentation
}
if tokenInfo.token.Loc.End() > childStartPos {
if tokenInfo.token.Loc.Pos() > childStartPos {
w.formattingScanner.skipToStartOf(&child.Loc)
}
// stop when formatting scanner advances past the beginning of the child
break
}
w.consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, node, false)
}
if !w.formattingScanner.isOnToken() || w.formattingScanner.getTokenFullStart() >= w.originalRange.End() {
return inheritedIndentation
}
if ast.IsTokenKind(child.Kind) {
// if child node is a token, it does not impact indentation, proceed it using parent indentation scope rules
tokenInfo := w.formattingScanner.readTokenInfo(child)
// JSX text shouldn't affect indenting
if child.Kind != ast.KindJsxText {
debug.Assert(tokenInfo.token.Loc.End() == child.Loc.End(), "Token end is child end")
w.consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, child, false)
return inheritedIndentation
}
}
effectiveParentStartLine := undecoratedParentStartLine
if child.Kind == ast.KindDecorator {
effectiveParentStartLine = childStartLine
}
childIndentation, delta := w.computeIndentation(child, childStartLine, childIndentationAmount, node, parentDynamicIndentation, effectiveParentStartLine)
w.processNode(child, w.childContextNode, childStartLine, undecoratedChildStartLine, childIndentation, delta)
w.childContextNode = node
if isFirstListItem && parent.Kind == ast.KindArrayLiteralExpression && inheritedIndentation == -1 {
inheritedIndentation = childIndentation
}
return inheritedIndentation
}
func (w *formatSpanWorker) processChildNodes(
node *ast.Node,
indenter *dynamicIndenter,
nodeStartLine int,
undecoratedNodeStartLine int,
nodes *ast.NodeList,
parent *ast.Node,
parentStartLine int,
parentDynamicIndentation *dynamicIndenter,
) {
debug.AssertIsDefined(nodes)
debug.Assert(!ast.PositionIsSynthesized(nodes.Pos()))
debug.Assert(!ast.PositionIsSynthesized(nodes.End()))
listStartToken := getOpenTokenForList(parent, nodes)
listDynamicIndentation := parentDynamicIndentation
startLine := parentStartLine
// node range is outside the target range - do not dive inside
if !w.originalRange.Overlaps(nodes.Loc) {
if nodes.End() < w.originalRange.Pos() {
w.formattingScanner.skipToEndOf(&nodes.Loc)
}
return
}
if listStartToken != -1 {
// introduce a new indentation scope for lists (including list start and end tokens)
for w.formattingScanner.isOnToken() && w.formattingScanner.getTokenFullStart() < w.originalRange.End() {
tokenInfo := w.formattingScanner.readTokenInfo(parent)
if tokenInfo.token.Loc.End() > nodes.Pos() {
// stop when formatting scanner moves past the beginning of node list
break
} else if tokenInfo.token.Kind == listStartToken {
// consume list start token
startLine, _ = scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, tokenInfo.token.Loc.Pos())
w.consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent, false)
indentationOnListStartToken := 0
if w.indentationOnLastIndentedLine != -1 {
// scanner just processed list start token so consider last indentation as list indentation
// function foo(): { // last indentation was 0, list item will be indented based on this value
// foo: number;
// }: {};
indentationOnListStartToken = w.indentationOnLastIndentedLine
} else {
startLinePosition := GetLineStartPositionForPosition(tokenInfo.token.Loc.Pos(), w.sourceFile)
indentationOnListStartToken = FindFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.Loc.Pos(), w.sourceFile, w.formattingContext.Options)
}
listDynamicIndentation = w.getDynamicIndentation(parent, parentStartLine, indentationOnListStartToken, w.formattingContext.Options.IndentSize)
} else {
// consume any tokens that precede the list as child elements of 'node' using its indentation scope
w.consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent, false)
}
}
}
inheritedIndentation := -1
for i := range len(nodes.Nodes) {
child := nodes.Nodes[i]
inheritedIndentation = w.processChildNode(node, indenter, nodeStartLine, undecoratedNodeStartLine, child, inheritedIndentation, node, listDynamicIndentation, startLine, startLine, true, i == 0)
}
listEndToken := getCloseTokenForOpenToken(listStartToken)
if listEndToken != ast.KindUnknown && w.formattingScanner.isOnToken() && w.formattingScanner.getTokenFullStart() < w.originalRange.End() {
tokenInfo := w.formattingScanner.readTokenInfo(parent)
if tokenInfo.token.Kind == ast.KindCommaToken {
// consume the comma
w.consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent, false)
if w.formattingScanner.isOnToken() {
tokenInfo = w.formattingScanner.readTokenInfo(parent)
} else {
return
}
}
// consume the list end token only if it is still belong to the parent
// there might be the case when current token matches end token but does not considered as one
// function (x: function) <--
// without this check close paren will be interpreted as list end token for function expression which is wrong
if tokenInfo.token.Kind == listEndToken && tokenInfo.token.Loc.ContainedBy(parent.Loc) {
// consume list end token
w.consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent /*isListEndToken*/, true)
}
}
}
func (w *formatSpanWorker) executeProcessNodeVisitor(node *ast.Node, indenter *dynamicIndenter, nodeStartLine int, undecoratedNodeStartLine int) {
oldNode := w.visitingNode
oldIndenter := w.visitingIndenter
oldStart := w.visitingNodeStartLine
oldUndecoratedStart := w.visitingUndecoratedNodeStartLine
w.visitingNode = node
w.visitingIndenter = indenter
w.visitingNodeStartLine = nodeStartLine
w.visitingUndecoratedNodeStartLine = undecoratedNodeStartLine
node.VisitEachChild(w.visitor)
w.visitingNode = oldNode
w.visitingIndenter = oldIndenter
w.visitingNodeStartLine = oldStart
w.visitingUndecoratedNodeStartLine = oldUndecoratedStart
}
func (w *formatSpanWorker) computeIndentation(node *ast.Node, startLine int, inheritedIndentation int, parent *ast.Node, parentDynamicIndentation *dynamicIndenter, effectiveParentStartLine int) (indentation int, delta int) {
delta = 0
if ShouldIndentChildNode(w.formattingContext.Options, node, nil, nil) {
delta = w.formattingContext.Options.IndentSize
}
if effectiveParentStartLine == startLine {
// if node is located on the same line with the parent
// - inherit indentation from the parent
// - push children if either parent of node itself has non-zero delta
indentation = w.indentationOnLastIndentedLine
if startLine != w.lastIndentedLine {
indentation = parentDynamicIndentation.getIndentation()
}
delta = min(w.formattingContext.Options.IndentSize, parentDynamicIndentation.getDelta(node)+delta)
return indentation, delta
} else if inheritedIndentation == -1 {
if node.Kind == ast.KindOpenParenToken && startLine == w.lastIndentedLine {
// the is used for chaining methods formatting
// - we need to get the indentation on last line and the delta of parent
return w.indentationOnLastIndentedLine, parentDynamicIndentation.getDelta(node)
} else if childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, w.sourceFile) ||
childIsUnindentedBranchOfConditionalExpression(parent, node, startLine, w.sourceFile) ||
argumentStartsOnSameLineAsPreviousArgument(parent, node, startLine, w.sourceFile) {
return parentDynamicIndentation.getIndentation(), delta
} else {
i := parentDynamicIndentation.getIndentation()
if i == -1 {
return parentDynamicIndentation.getIndentation(), delta
}
return i + parentDynamicIndentation.getDelta(node), delta
}
}
return inheritedIndentation, delta
}
/** Tries to compute the indentation for a list element.
* If list element is not in range then
* function will pick its actual indentation
* so it can be pushed downstream as inherited indentation.
* If list element is in the range - its indentation will be equal
* to inherited indentation from its predecessors.
*/
func (w *formatSpanWorker) tryComputeIndentationForListItem(startPos int, endPos int, parentStartLine int, r core.TextRange, inheritedIndentation int) int {
r2 := core.NewTextRange(startPos, endPos)
if r.Overlaps(r2) || r2.ContainedBy(r) { /* Not to miss zero-range nodes e.g. JsxText */
if inheritedIndentation != -1 {
return inheritedIndentation
}
} else {
startLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, startPos)
startLinePosition := GetLineStartPositionForPosition(startPos, w.sourceFile)
column := FindFirstNonWhitespaceColumn(startLinePosition, startPos, w.sourceFile, w.formattingContext.Options)
if startLine != parentStartLine || startPos == column {
// Use the base indent size if it is greater than
// the indentation of the inherited predecessor.
baseIndentSize := w.formattingContext.Options.BaseIndentSize
if baseIndentSize > column {
return baseIndentSize
}
return column
}
}
return -1
}
func (w *formatSpanWorker) processNode(node *ast.Node, contextNode *ast.Node, nodeStartLine int, undecoratedNodeStartLine int, indentation int, delta int) {
if !w.originalRange.Overlaps(withTokenStart(node, w.sourceFile)) {
return
}
nodeDynamicIndentation := w.getDynamicIndentation(node, nodeStartLine, indentation, delta)
// a useful observations when tracking context node
// /
// [a]
// / | \
// [b] [c] [d]
// node 'a' is a context node for nodes 'b', 'c', 'd'
// except for the leftmost leaf token in [b] - in this case context node ('e') is located somewhere above 'a'
// this rule can be applied recursively to child nodes of 'a'.
//
// context node is set to parent node value after processing every child node
// context node is set to parent of the token after processing every token
w.childContextNode = contextNode
// if there are any tokens that logically belong to node and interleave child nodes
// such tokens will be consumed in processChildNode for the child that follows them
w.executeProcessNodeVisitor(node, nodeDynamicIndentation, nodeStartLine, undecoratedNodeStartLine)
// proceed any tokens in the node that are located after child nodes
for w.formattingScanner.isOnToken() && w.formattingScanner.getTokenFullStart() < w.originalRange.End() {
tokenInfo := w.formattingScanner.readTokenInfo(node)
if tokenInfo.token.Loc.End() > min(node.End(), w.originalRange.End()) {
break
}
w.consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node, false)
}
}
func (w *formatSpanWorker) processPair(currentItem TextRangeWithKind, currentStartLine int, currentParent *ast.Node, previousItem TextRangeWithKind, previousStartLine int, previousParent *ast.Node, contextNode *ast.Node, dynamicIndentation *dynamicIndenter) LineAction {
w.formattingContext.UpdateContext(previousItem, previousParent, currentItem, currentParent, contextNode)
w.currentRules = w.currentRules[:0]
w.currentRules = getRules(w.formattingContext, w.currentRules)
trimTrailingWhitespaces := w.formattingContext.Options.TrimTrailingWhitespace != false
lineAction := LineActionNone
if len(w.currentRules) > 0 {
// Apply rules in reverse order so that higher priority rules (which are first in the array)
// win in a conflict with lower priority rules.
for i := len(w.currentRules) - 1; i >= 0; i-- {
rule := w.currentRules[i]
lineAction = w.applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine)
if dynamicIndentation != nil {
switch lineAction {
case LineActionLineRemoved:
// Handle the case where the next line is moved to be the end of this line.
// In this case we don't indent the next line in the next pass.
if scanner.GetTokenPosOfNode(currentParent, w.sourceFile, false) == currentItem.Loc.Pos() {
dynamicIndentation.recomputeIndentation( /*lineAddedByFormatting*/ false, contextNode)
}
case LineActionLineAdded:
// Handle the case where token2 is moved to the new line.
// In this case we indent token2 in the next pass but we set
// sameLineIndent flag to notify the indenter that the indentation is within the line.
if scanner.GetTokenPosOfNode(currentParent, w.sourceFile, false) == currentItem.Loc.Pos() {
dynamicIndentation.recomputeIndentation( /*lineAddedByFormatting*/ true, contextNode)
}
default:
debug.Assert(lineAction == LineActionNone)
}
}
// We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line
trimTrailingWhitespaces = trimTrailingWhitespaces && (rule.Action()&ruleActionDeleteSpace == 0) && rule.Flags() != ruleFlagsCanDeleteNewLines
}
} else {
trimTrailingWhitespaces = trimTrailingWhitespaces && currentItem.Kind != ast.KindEndOfFile
}
if currentStartLine != previousStartLine && trimTrailingWhitespaces {
// We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line
w.trimTrailingWhitespacesForLines(previousStartLine, currentStartLine, previousItem)
}
return lineAction
}
func (w *formatSpanWorker) applyRuleEdits(rule *ruleImpl, previousRange TextRangeWithKind, previousStartLine int, currentRange TextRangeWithKind, currentStartLine int) LineAction {
onLaterLine := currentStartLine != previousStartLine
switch rule.Action() {
case ruleActionStopProcessingSpaceActions:
// no action required
return LineActionNone
case ruleActionDeleteSpace:
if previousRange.Loc.End() != currentRange.Loc.Pos() {
// delete characters starting from t1.end up to t2.pos exclusive
w.recordDelete(previousRange.Loc.End(), currentRange.Loc.Pos()-previousRange.Loc.End())
if onLaterLine {
return LineActionLineRemoved
}
return LineActionNone
}
case ruleActionDeleteToken:
w.recordDelete(previousRange.Loc.Pos(), previousRange.Loc.Len())
case ruleActionInsertNewLine:
// exit early if we on different lines and rule cannot change number of newlines
// if line1 and line2 are on subsequent lines then no edits are required - ok to exit
// if line1 and line2 are separated with more than one newline - ok to exit since we cannot delete extra new lines
if rule.Flags() != ruleFlagsCanDeleteNewLines && previousStartLine != currentStartLine {
return LineActionNone
}
// edit should not be applied if we have one line feed between elements
lineDelta := currentStartLine - previousStartLine
if lineDelta != 1 {
w.recordReplace(previousRange.Loc.End(), currentRange.Loc.Pos()-previousRange.Loc.End(), GetNewLineOrDefaultFromContext(w.ctx))
if onLaterLine {
return LineActionNone
}
return LineActionLineAdded
}
case ruleActionInsertSpace:
// exit early if we on different lines and rule cannot change number of newlines
if rule.Flags() != ruleFlagsCanDeleteNewLines && previousStartLine != currentStartLine {
return LineActionNone
}
posDelta := currentRange.Loc.Pos() - previousRange.Loc.End()
if posDelta != 1 || !strings.HasPrefix(w.sourceFile.Text()[previousRange.Loc.End():], " ") {
w.recordReplace(previousRange.Loc.End(), posDelta, " ")
if onLaterLine {
return LineActionLineRemoved
}
return LineActionNone
}
case ruleActionInsertTrailingSemicolon:
w.recordInsert(previousRange.Loc.End(), ";")
}
return LineActionNone
}
type LineAction int
const (
LineActionNone LineAction = iota
LineActionLineAdded
LineActionLineRemoved
)
func (w *formatSpanWorker) processRange(r TextRangeWithKind, rangeStartLine int, rangeStartCharacter int, parent *ast.Node, contextNode *ast.Node, dynamicIndentation *dynamicIndenter) LineAction {
rangeHasError := w.rangeContainsError(r.Loc)
lineAction := LineActionNone
if !rangeHasError {
if w.previousRange == NewTextRangeWithKind(0, 0, 0) {
// trim whitespaces starting from the beginning of the span up to the current line
originalStartLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, w.originalRange.Pos())
w.trimTrailingWhitespacesForLines(originalStartLine, rangeStartLine, NewTextRangeWithKind(0, 0, 0))
} else {
lineAction = w.processPair(r, rangeStartLine, parent, w.previousRange, w.previousRangeStartLine, w.previousParent, contextNode, dynamicIndentation)
}
}
w.previousRange = r
w.previousRangeTriviaEnd = r.Loc.End()
w.previousParent = parent
w.previousRangeStartLine = rangeStartLine
return lineAction
}
func (w *formatSpanWorker) processTrivia(trivia []TextRangeWithKind, parent *ast.Node, contextNode *ast.Node, dynamicIndentation *dynamicIndenter) {
for _, triviaItem := range trivia {
if isComment(triviaItem.Kind) && triviaItem.Loc.ContainedBy(w.originalRange) {
triviaItemStartLine, triviaItemStartCharacter := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, triviaItem.Loc.Pos())
w.processRange(triviaItem, triviaItemStartLine, triviaItemStartCharacter, parent, contextNode, dynamicIndentation)
}
}
}
/**
* Trimming will be done for lines after the previous range.
* Exclude comments as they had been previously processed.
*/
func (w *formatSpanWorker) trimTrailingWhitespacesForRemainingRange(trivias []TextRangeWithKind) {
startPos := w.originalRange.Pos()
if w.previousRange != NewTextRangeWithKind(0, 0, 0) {
startPos = w.previousRange.Loc.End()
}
for _, trivia := range trivias {
if isComment(trivia.Kind) {
if startPos < trivia.Loc.Pos() {
w.trimTrailingWitespacesForPositions(startPos, trivia.Loc.Pos()-1, w.previousRange)
}
startPos = trivia.Loc.End() + 1
}
}
if startPos < w.originalRange.End() {
w.trimTrailingWitespacesForPositions(startPos, w.originalRange.End(), w.previousRange)
}
}
func (w *formatSpanWorker) trimTrailingWitespacesForPositions(startPos int, endPos int, previousRange TextRangeWithKind) {
startLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, startPos)
endLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, endPos)
w.trimTrailingWhitespacesForLines(startLine, endLine+1, previousRange)
}
func (w *formatSpanWorker) trimTrailingWhitespacesForLines(line1 int, line2 int, r TextRangeWithKind) {
lineStarts := scanner.GetECMALineStarts(w.sourceFile)
for line := line1; line < line2; line++ {
lineStartPosition := int(lineStarts[line])
lineEndPosition := scanner.GetECMAEndLinePosition(w.sourceFile, line)
// do not trim whitespaces in comments or template expression
if r != NewTextRangeWithKind(0, 0, 0) && (isComment(r.Kind) || isStringOrRegularExpressionOrTemplateLiteral(r.Kind)) && r.Loc.Pos() <= lineEndPosition && r.Loc.End() > lineEndPosition {
continue
}
whitespaceStart := w.getTrailingWhitespaceStartPosition(lineStartPosition, lineEndPosition)
if whitespaceStart != -1 {
r, _ := utf8.DecodeRuneInString(w.sourceFile.Text()[whitespaceStart-1:])
debug.Assert(whitespaceStart == lineStartPosition || !stringutil.IsWhiteSpaceSingleLine(r))
w.recordDelete(whitespaceStart, lineEndPosition+1-whitespaceStart)
}
}
}
/**
* @param start The position of the first character in range
* @param end The position of the last character in range
*/
func (w *formatSpanWorker) getTrailingWhitespaceStartPosition(start int, end int) int {
pos := end
text := w.sourceFile.Text()
for pos >= start {
ch, size := utf8.DecodeRuneInString(text[pos:])
if size == 0 {
pos-- // multibyte character, rewind more
continue
}
if !stringutil.IsWhiteSpaceSingleLine(ch) {
break
}
pos--
}
if pos != end {
return pos + 1
}
return -1
}
func isStringOrRegularExpressionOrTemplateLiteral(kind ast.Kind) bool {
return kind == ast.KindStringLiteral || kind == ast.KindRegularExpressionLiteral || ast.IsTemplateLiteralKind(kind)
}
func isComment(kind ast.Kind) bool {
return kind == ast.KindSingleLineCommentTrivia || kind == ast.KindMultiLineCommentTrivia
}
func (w *formatSpanWorker) insertIndentation(pos int, indentation int, lineAdded bool) {
indentationString := getIndentationString(indentation, w.formattingContext.Options)
if lineAdded {
// new line is added before the token by the formatting rules
// insert indentation string at the very beginning of the token
w.recordReplace(pos, 0, indentationString)
} else {
tokenStartLine, tokenStartCharacter := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, pos)
startLinePosition := int(scanner.GetECMALineStarts(w.sourceFile)[tokenStartLine])
if indentation != w.characterToColumn(startLinePosition, tokenStartCharacter) || w.indentationIsDifferent(indentationString, startLinePosition) {
w.recordReplace(startLinePosition, tokenStartCharacter, indentationString)
}
}
}
func (w *formatSpanWorker) characterToColumn(startLinePosition int, characterInLine int) int {
column := 0
for i := range characterInLine {
if w.sourceFile.Text()[startLinePosition+i] == '\t' {
column += w.formattingContext.Options.TabSize - (column % w.formattingContext.Options.TabSize)
} else {
column++
}
}
return column
}
func (w *formatSpanWorker) indentationIsDifferent(indentationString string, startLinePosition int) bool {
return indentationString != w.sourceFile.Text()[startLinePosition:startLinePosition+len(indentationString)]
}
func (w *formatSpanWorker) indentTriviaItems(trivia []TextRangeWithKind, commentIndentation int, indentNextTokenOrTrivia bool, indentSingleLine func(item TextRangeWithKind)) bool {
for _, triviaItem := range trivia {
triviaInRange := triviaItem.Loc.ContainedBy(w.originalRange)
switch triviaItem.Kind {
case ast.KindMultiLineCommentTrivia:
if triviaInRange {
w.indentMultilineComment(triviaItem.Loc, commentIndentation, !indentNextTokenOrTrivia, true)
}
indentNextTokenOrTrivia = false
case ast.KindSingleLineCommentTrivia:
if indentNextTokenOrTrivia && triviaInRange {
indentSingleLine(triviaItem)
}
indentNextTokenOrTrivia = false
case ast.KindNewLineTrivia:
indentNextTokenOrTrivia = true
}
}
return indentNextTokenOrTrivia
}
func (w *formatSpanWorker) indentMultilineComment(commentRange core.TextRange, indentation int, firstLineIsIndented bool, indentFinalLine bool) {
// split comment in lines
startLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, commentRange.Pos())
endLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, commentRange.End())
if startLine == endLine {
if !firstLineIsIndented {
// treat as single line comment
w.insertIndentation(commentRange.Pos(), indentation, false)
}
return
}
parts := make([]core.TextRange, 0, strings.Count(w.sourceFile.Text()[commentRange.Pos():commentRange.End()], "\n"))
startPos := commentRange.Pos()
for line := startLine; line < endLine; line++ {
endOfLine := scanner.GetECMAEndLinePosition(w.sourceFile, line)
parts = append(parts, core.NewTextRange(startPos, endOfLine))
startPos = int(scanner.GetECMALineStarts(w.sourceFile)[line+1])
}
if indentFinalLine {
parts = append(parts, core.NewTextRange(startPos, commentRange.End()))
}
if len(parts) == 0 {
return
}
startLinePos := int(scanner.GetECMALineStarts(w.sourceFile)[startLine])
nonWhitespaceInFirstPartCharacter, nonWhitespaceInFirstPartColumn := findFirstNonWhitespaceCharacterAndColumn(startLinePos, parts[0].Pos(), w.sourceFile, w.formattingContext.Options)
startIndex := 0
if firstLineIsIndented {
startIndex = 1
startLine++
}
// shift all parts on the delta size
delta := indentation - nonWhitespaceInFirstPartColumn
for i := startIndex; i < len(parts); i++ {
startLinePos := int(scanner.GetECMALineStarts(w.sourceFile)[startLine])
nonWhitespaceCharacter := nonWhitespaceInFirstPartCharacter
nonWhitespaceColumn := nonWhitespaceInFirstPartColumn
if i != 0 {
nonWhitespaceCharacter, nonWhitespaceColumn = findFirstNonWhitespaceCharacterAndColumn(parts[i].Pos(), parts[i].End(), w.sourceFile, w.formattingContext.Options)
}
newIndentation := nonWhitespaceColumn + delta
if newIndentation > 0 {
indentationString := getIndentationString(newIndentation, w.formattingContext.Options)
w.recordReplace(startLinePos, nonWhitespaceCharacter, indentationString)
} else {
w.recordDelete(startLinePos, nonWhitespaceCharacter)
}
startLine++
}
}
func getIndentationString(indentation int, options *FormatCodeSettings) string {
// go's `strings.Repeat` already has static, global caching for repeated tabs and spaces, so there's no need to cache here like in strada
if !options.ConvertTabsToSpaces {
tabs := int(math.Floor(float64(indentation) / float64(options.TabSize)))
spaces := indentation - (tabs * options.TabSize)
res := strings.Repeat("\t", tabs)
if spaces > 0 {
res = strings.Repeat(" ", spaces) + res
}
return res
} else {
return strings.Repeat(" ", indentation)
}
}
func createTextChangeFromStartLength(start int, length int, newText string) core.TextChange {
return core.TextChange{
NewText: newText,
TextRange: core.NewTextRange(start, start+length),
}
}
func (w *formatSpanWorker) recordDelete(start int, length int) {
if length != 0 {
w.edits = append(w.edits, createTextChangeFromStartLength(start, length, ""))
}
}
func (w *formatSpanWorker) recordReplace(start int, length int, newText string) {
if length != 0 || newText != "" {
w.edits = append(w.edits, createTextChangeFromStartLength(start, length, newText))
}
}
func (w *formatSpanWorker) recordInsert(start int, text string) {
if text != "" {
w.edits = append(w.edits, createTextChangeFromStartLength(start, 0, text))
}
}
func (w *formatSpanWorker) consumeTokenAndAdvanceScanner(currentTokenInfo tokenInfo, parent *ast.Node, dynamicIndenation *dynamicIndenter, container *ast.Node, isListEndToken bool) {
// assert(currentTokenInfo.token.Loc.ContainedBy(parent.Loc)) // !!!
lastTriviaWasNewLine := w.formattingScanner.lastTrailingTriviaWasNewLine()
indentToken := false
if len(currentTokenInfo.leadingTrivia) > 0 {
w.processTrivia(currentTokenInfo.leadingTrivia, parent, w.childContextNode, dynamicIndenation)
}
lineAction := LineActionNone
isTokenInRange := currentTokenInfo.token.Loc.ContainedBy(w.originalRange)
tokenStartLine, tokenStartChar := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, currentTokenInfo.token.Loc.Pos())
if isTokenInRange {
rangeHasError := w.rangeContainsError(currentTokenInfo.token.Loc)
// save previousRange since processRange will overwrite this value with current one
savePreviousRange := w.previousRange
lineAction = w.processRange(currentTokenInfo.token, tokenStartLine, tokenStartChar, parent, w.childContextNode, dynamicIndenation)
// do not indent comments\token if token range overlaps with some error
if !rangeHasError {
if lineAction == LineActionNone {
// indent token only if end line of previous range does not match start line of the token
if savePreviousRange != NewTextRangeWithKind(0, 0, 0) {
prevEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, savePreviousRange.Loc.End())
indentToken = lastTriviaWasNewLine && tokenStartLine != prevEndLine
}
} else {
indentToken = lineAction == LineActionLineAdded
}
}
}
if len(currentTokenInfo.trailingTrivia) > 0 {
w.previousRangeTriviaEnd = core.LastOrNil(currentTokenInfo.trailingTrivia).Loc.End()
w.processTrivia(currentTokenInfo.trailingTrivia, parent, w.childContextNode, dynamicIndenation)
}
if indentToken {
tokenIndentation := -1
if isTokenInRange && !w.rangeContainsError(currentTokenInfo.token.Loc) {
tokenIndentation = dynamicIndenation.getIndentationForToken(tokenStartLine, currentTokenInfo.token.Kind, container, !!isListEndToken)
}
indentNextTokenOrTrivia := true
if len(currentTokenInfo.leadingTrivia) > 0 {
commentIndentation := dynamicIndenation.getIndentationForComment(currentTokenInfo.token.Kind, tokenIndentation, container)
indentNextTokenOrTrivia = w.indentTriviaItems(currentTokenInfo.leadingTrivia, commentIndentation, indentNextTokenOrTrivia, func(item TextRangeWithKind) {
w.insertIndentation(item.Loc.Pos(), commentIndentation, false)
})
}
// indent token only if is it is in target range and does not overlap with any error ranges
if tokenIndentation != -1 && indentNextTokenOrTrivia {
w.insertIndentation(currentTokenInfo.token.Loc.Pos(), tokenIndentation, lineAction == LineActionLineAdded)
w.lastIndentedLine = tokenStartLine
w.indentationOnLastIndentedLine = tokenIndentation
}
}
w.formattingScanner.advance()
w.childContextNode = parent
}
type dynamicIndenter struct {
node *ast.Node
nodeStartLine int
indentation int
delta int
options *FormatCodeSettings
sourceFile *ast.SourceFile
}
func (i *dynamicIndenter) getIndentationForComment(kind ast.Kind, tokenIndentation int, container *ast.Node) int {
switch kind {
// preceding comment to the token that closes the indentation scope inherits the indentation from the scope
// .. {
// // comment
// }
case ast.KindCloseBraceToken, ast.KindCloseBracketToken, ast.KindCloseParenToken:
return i.indentation + i.getDelta(container)
}
return i.indentation
}
// if list end token is LessThanToken '>' then its delta should be explicitly suppressed
// so that LessThanToken as a binary operator can still be indented.
// foo.then
//
// <
// number,
// string,
// >();
//
// vs
// var a = xValue
//
// > yValue;
func (i *dynamicIndenter) getIndentationForToken(line int, kind ast.Kind, container *ast.Node, suppressDelta bool) int {
if !suppressDelta && i.shouldAddDelta(line, kind, container) {
return i.indentation + i.getDelta(container)
}
return i.indentation
}
func (i *dynamicIndenter) getIndentation() int {
return i.indentation
}
func (i *dynamicIndenter) getDelta(child *ast.Node) int {
// Delta value should be zero when the node explicitly prevents indentation of the child node
if NodeWillIndentChild(i.options, i.node, child, i.sourceFile, true) {
return i.delta
}
return 0
}
func (i *dynamicIndenter) recomputeIndentation(lineAdded bool, parent *ast.Node) {
if ShouldIndentChildNode(i.options, parent, i.node, i.sourceFile) {
if lineAdded {
i.indentation += i.options.IndentSize // !!! no nil check???
} else {
i.indentation -= i.options.IndentSize // !!! no nil check???
}
if ShouldIndentChildNode(i.options, i.node, nil, nil) {
i.delta = i.options.IndentSize
} else {
i.delta = 0
}
}
}
func (i *dynamicIndenter) shouldAddDelta(line int, kind ast.Kind, container *ast.Node) bool {
switch kind {
// open and close brace, 'else' and 'while' (in do statement) tokens has indentation of the parent
case ast.KindOpenBraceToken, ast.KindCloseBraceToken, ast.KindCloseParenToken, ast.KindElseKeyword, ast.KindWhileKeyword, ast.KindAtToken:
return false
case ast.KindSlashToken, ast.KindGreaterThanToken:
switch container.Kind {
case ast.KindJsxOpeningElement, ast.KindJsxClosingElement, ast.KindJsxSelfClosingElement:
return false
}
break
case ast.KindOpenBracketToken, ast.KindCloseBracketToken:
if container.Kind != ast.KindMappedType {
return false
}
break
}
// if token line equals to the line of containing node (this is a first token in the node) - use node indentation
return i.nodeStartLine != line &&
// if this token is the first token following the list of decorators, we do not need to indent
!(ast.HasDecorators(i.node) && kind == getFirstNonDecoratorTokenOfNode(i.node))
}
func getFirstNonDecoratorTokenOfNode(node *ast.Node) ast.Kind {
if ast.CanHaveModifiers(node) {
modifier := core.Find(node.Modifiers().Nodes[core.FindIndex(node.Modifiers().Nodes, ast.IsDecorator):], ast.IsModifier)
if modifier != nil {
return modifier.Kind
}
}
switch node.Kind {
case ast.KindClassDeclaration:
return ast.KindClassKeyword
case ast.KindInterfaceDeclaration:
return ast.KindInterfaceKeyword
case ast.KindFunctionDeclaration:
return ast.KindFunctionKeyword
case ast.KindEnumDeclaration:
return ast.KindEnumDeclaration
case ast.KindGetAccessor:
return ast.KindGetKeyword
case ast.KindSetAccessor:
return ast.KindSetKeyword
case ast.KindMethodDeclaration:
if node.AsMethodDeclaration().AsteriskToken != nil {
return ast.KindAsteriskToken
}
fallthrough
case ast.KindPropertyDeclaration, ast.KindParameter:
name := ast.GetNameOfDeclaration(node)
if name != nil {
return name.Kind
}
}
return ast.KindUnknown
}
func (w *formatSpanWorker) getDynamicIndentation(node *ast.Node, nodeStartLine int, indentation int, delta int) *dynamicIndenter {
return &dynamicIndenter{
node: node,
nodeStartLine: nodeStartLine,
indentation: indentation,
delta: delta,
options: w.formattingContext.Options,
sourceFile: w.sourceFile,
}
}