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, } }