vendor tsgo
This commit is contained in:
821
tools/tsgo/internal/format/indent.go
Normal file
821
tools/tsgo/internal/format/indent.go
Normal file
@@ -0,0 +1,821 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"slices"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/debug"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
func GetIndentationForNode(n *ast.Node, ignoreActualIndentationRange *core.TextRange, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
startline, startpos := scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false))
|
||||
return getIndentationForNodeWorker(n, startline, startpos, ignoreActualIndentationRange /*indentationDelta*/, 0, sourceFile /*isNextChild*/, false, options)
|
||||
}
|
||||
|
||||
// GetIndentation computes the expected indentation for a position in a source file.
|
||||
// This is the Go port of SmartIndenter.getIndentation from TypeScript.
|
||||
func GetIndentation(position int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings, assumeNewLineBeforeCloseBrace bool) int {
|
||||
if position > len(sourceFile.Text()) {
|
||||
return options.BaseIndentSize // past EOF
|
||||
}
|
||||
|
||||
// no indentation when the indent style is set to none,
|
||||
// so we can return fast
|
||||
if options.IndentStyle == lsutil.IndentStyleNone {
|
||||
return 0
|
||||
}
|
||||
|
||||
precedingToken := astnav.FindPrecedingTokenEx(sourceFile, position, nil /*startNode*/, true /*excludeJSDoc*/)
|
||||
|
||||
enclosingCommentRange := getRangeOfEnclosingComment(sourceFile, position, precedingToken)
|
||||
if enclosingCommentRange != nil && enclosingCommentRange.Kind == ast.KindMultiLineCommentTrivia {
|
||||
return getCommentIndent(sourceFile, position, options, enclosingCommentRange)
|
||||
}
|
||||
|
||||
if precedingToken == nil {
|
||||
return options.BaseIndentSize
|
||||
}
|
||||
|
||||
// no indentation in string/regex/template literals
|
||||
if isStringOrRegularExpressionOrTemplateLiteral(precedingToken.Kind) {
|
||||
tokenStart := scanner.GetTokenPosOfNode(precedingToken, sourceFile, false)
|
||||
if tokenStart <= position && position < precedingToken.End() {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
lineAtPosition := scanner.GetECMALineOfPosition(sourceFile, position)
|
||||
|
||||
// indentation is first non-whitespace character in a previous line
|
||||
// for block indentation, we should look for a line which contains something that's not
|
||||
// whitespace.
|
||||
currentToken := astnav.GetTokenAtPosition(sourceFile, position)
|
||||
// For object literals, we want indentation to work just like with blocks.
|
||||
// If the `{` starts in any position (even in the middle of a line), then
|
||||
// the following indentation should treat `{` as the start of that line (including leading whitespace).
|
||||
// ```
|
||||
// const a: { x: undefined, y: undefined } = {} // leading 4 whitespaces and { starts in the middle of line
|
||||
// ->
|
||||
// const a: { x: undefined, y: undefined } = {
|
||||
// x: undefined,
|
||||
// y: undefined,
|
||||
// }
|
||||
// ---------------------
|
||||
// const a: {x : undefined, y: undefined } =
|
||||
// {}
|
||||
// ->
|
||||
// const a: { x: undefined, y: undefined } =
|
||||
// { // leading 5 whitespaces and { starts at 6 column
|
||||
// x: undefined,
|
||||
// y: undefined,
|
||||
// }
|
||||
// ```
|
||||
isObjectLiteral := currentToken.Kind == ast.KindOpenBraceToken && currentToken.Parent != nil && currentToken.Parent.Kind == ast.KindObjectLiteralExpression
|
||||
if options.IndentStyle == lsutil.IndentStyleBlock || isObjectLiteral {
|
||||
return getBlockIndent(sourceFile, position, options)
|
||||
}
|
||||
|
||||
if precedingToken.Kind == ast.KindCommaToken && precedingToken.Parent != nil && precedingToken.Parent.Kind != ast.KindBinaryExpression {
|
||||
// previous token is comma that separates items in list - find the previous item and try to derive indentation from it
|
||||
actualIndentation := getActualIndentationForListItemBeforeComma(precedingToken, sourceFile, options)
|
||||
if actualIndentation != -1 {
|
||||
return actualIndentation
|
||||
}
|
||||
}
|
||||
|
||||
containerList := getListByPosition(position, precedingToken.Parent, sourceFile)
|
||||
// use list position if the preceding token is before any list items
|
||||
if containerList != nil && !precedingToken.Loc.ContainedBy(containerList.Loc) {
|
||||
useTheSameBaseIndentation := currentToken.Parent != nil && (currentToken.Parent.Kind == ast.KindFunctionExpression || currentToken.Parent.Kind == ast.KindArrowFunction)
|
||||
indentSize := 0
|
||||
if !useTheSameBaseIndentation {
|
||||
indentSize = options.IndentSize
|
||||
}
|
||||
res := getActualIndentationForListStartLine(containerList, sourceFile, options)
|
||||
if res == -1 {
|
||||
return indentSize
|
||||
}
|
||||
return res + indentSize
|
||||
}
|
||||
|
||||
return getSmartIndent(sourceFile, position, precedingToken, lineAtPosition, assumeNewLineBeforeCloseBrace, options)
|
||||
}
|
||||
|
||||
func getCommentIndent(sourceFile *ast.SourceFile, position int, options lsutil.FormatCodeSettings, enclosingCommentRange *ast.CommentRange) int {
|
||||
previousLine := scanner.GetECMALineOfPosition(sourceFile, position) - 1
|
||||
commentStartLine := scanner.GetECMALineOfPosition(sourceFile, enclosingCommentRange.Pos())
|
||||
|
||||
debug.Assert(commentStartLine >= 0, "commentStartLine >= 0")
|
||||
|
||||
if previousLine <= commentStartLine {
|
||||
lineStarts := scanner.GetECMALineStarts(sourceFile)
|
||||
return FindFirstNonWhitespaceColumn(int(lineStarts[commentStartLine]), position, sourceFile, options)
|
||||
}
|
||||
|
||||
lineStarts := scanner.GetECMALineStarts(sourceFile)
|
||||
startPositionOfLine := int(lineStarts[previousLine])
|
||||
character, column := findFirstNonWhitespaceCharacterAndColumn(startPositionOfLine, position, sourceFile, options)
|
||||
|
||||
if column == 0 {
|
||||
return column
|
||||
}
|
||||
|
||||
firstNonWhitespaceCharacterCode := sourceFile.Text()[startPositionOfLine+character]
|
||||
if firstNonWhitespaceCharacterCode == '*' {
|
||||
return column - 1
|
||||
}
|
||||
return column
|
||||
}
|
||||
|
||||
func getLeadingCommentRangesOfNode(node *ast.Node, file *ast.SourceFile) iter.Seq[ast.CommentRange] {
|
||||
if node.Kind == ast.KindJsxText {
|
||||
return nil
|
||||
}
|
||||
return scanner.GetLeadingCommentRanges(&ast.NodeFactory{}, file.Text(), node.Pos())
|
||||
}
|
||||
|
||||
func getRangeOfEnclosingComment(
|
||||
sourceFile *ast.SourceFile,
|
||||
position int,
|
||||
precedingToken *ast.Node,
|
||||
) *ast.CommentRange {
|
||||
tokenAtPosition := astnav.GetTokenAtPosition(sourceFile, position)
|
||||
jsdoc := ast.FindAncestor(tokenAtPosition, (*ast.Node).IsJSDoc)
|
||||
if jsdoc != nil {
|
||||
tokenAtPosition = jsdoc.Parent
|
||||
}
|
||||
tokenStart := astnav.GetStartOfNode(tokenAtPosition, sourceFile, false /*includeJSDoc*/)
|
||||
if tokenStart <= position && position < tokenAtPosition.End() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Between two consecutive tokens, all comments are either trailing on the former
|
||||
// or leading on the latter (and none are in both lists).
|
||||
var trailingRangesOfPreviousToken iter.Seq[ast.CommentRange]
|
||||
if precedingToken != nil {
|
||||
trailingRangesOfPreviousToken = scanner.GetTrailingCommentRanges(&ast.NodeFactory{}, sourceFile.Text(), precedingToken.End())
|
||||
}
|
||||
leadingRangesOfNextToken := getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile)
|
||||
commentRanges := core.ConcatenateSeq(trailingRangesOfPreviousToken, leadingRangesOfNextToken)
|
||||
for commentRange := range commentRanges {
|
||||
if commentRange.ContainsExclusive(position) ||
|
||||
position == commentRange.End() &&
|
||||
(commentRange.Kind == ast.KindSingleLineCommentTrivia || position == len(sourceFile.Text())) {
|
||||
return &commentRange
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBlockIndent(sourceFile *ast.SourceFile, position int, options lsutil.FormatCodeSettings) int {
|
||||
// move backwards until we find a line with a non-whitespace character,
|
||||
// then find the first non-whitespace character for that line.
|
||||
current := position
|
||||
for current > 0 {
|
||||
ch, size := utf8.DecodeRuneInString(sourceFile.Text()[current:])
|
||||
if !stringutil.IsWhiteSpaceLike(ch) {
|
||||
break
|
||||
}
|
||||
current -= size
|
||||
}
|
||||
|
||||
lineStart := GetLineStartPositionForPosition(current, sourceFile)
|
||||
return FindFirstNonWhitespaceColumn(lineStart, current, sourceFile, options)
|
||||
}
|
||||
|
||||
func getActualIndentationForListItemBeforeComma(commaToken *ast.Node, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
// previous token is comma that separates items in list - find the previous item and try to derive indentation from it
|
||||
if commaToken.Parent == nil {
|
||||
return -1
|
||||
}
|
||||
containingList := GetContainingList(commaToken, sourceFile)
|
||||
if containingList == nil {
|
||||
return -1
|
||||
}
|
||||
commaIndex := core.FindIndex(containingList.Nodes, func(n *ast.Node) bool { return n == commaToken })
|
||||
if commaIndex > 0 {
|
||||
return deriveActualIndentationFromList(containingList, commaIndex-1, sourceFile, options)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type nextTokenKind int
|
||||
|
||||
const (
|
||||
nextTokenKindUnknown nextTokenKind = 0
|
||||
nextTokenKindOpenBrace nextTokenKind = 1
|
||||
nextTokenKindCloseBrace nextTokenKind = 2
|
||||
)
|
||||
|
||||
func nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken *ast.Node, current *ast.Node, lineAtPosition int, sourceFile *ast.SourceFile) nextTokenKind {
|
||||
nextToken := astnav.FindNextToken(precedingToken, current, sourceFile)
|
||||
if nextToken == nil {
|
||||
return nextTokenKindUnknown
|
||||
}
|
||||
|
||||
if nextToken.Kind == ast.KindOpenBraceToken {
|
||||
// open braces are always indented at the parent level
|
||||
return nextTokenKindOpenBrace
|
||||
} else if nextToken.Kind == ast.KindCloseBraceToken {
|
||||
// close braces are indented at the parent level if they are located on the same line with cursor
|
||||
nextTokenStartLine := getStartLineForNode(nextToken, sourceFile)
|
||||
if lineAtPosition == nextTokenStartLine {
|
||||
return nextTokenKindCloseBrace
|
||||
}
|
||||
return nextTokenKindUnknown
|
||||
}
|
||||
|
||||
return nextTokenKindUnknown
|
||||
}
|
||||
|
||||
func getSmartIndent(sourceFile *ast.SourceFile, position int, precedingToken *ast.Node, lineAtPosition int, assumeNewLineBeforeCloseBrace bool, options lsutil.FormatCodeSettings) int {
|
||||
// try to find node that can contribute to indentation and includes 'position' starting from 'precedingToken'
|
||||
// if such node is found - compute initial indentation for 'position' inside this node
|
||||
var previous *ast.Node
|
||||
current := precedingToken
|
||||
|
||||
for current != nil {
|
||||
if lsutil.PositionBelongsToNode(current, position, sourceFile) && ShouldIndentChildNode(options, current, previous, sourceFile, true) {
|
||||
currentStartLine, currentStartChar := getStartLineAndCharacterForNode(current, sourceFile)
|
||||
ntk := nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken, current, lineAtPosition, sourceFile)
|
||||
var indentationDelta int
|
||||
if ntk != nextTokenKindUnknown {
|
||||
// handle cases when codefix is about to be inserted before the close brace
|
||||
if assumeNewLineBeforeCloseBrace && ntk == nextTokenKindCloseBrace {
|
||||
indentationDelta = options.IndentSize
|
||||
}
|
||||
// else 0
|
||||
} else {
|
||||
if lineAtPosition != currentStartLine {
|
||||
indentationDelta = options.IndentSize
|
||||
}
|
||||
}
|
||||
return getIndentationForNodeWorker(current, currentStartLine, currentStartChar, nil, indentationDelta, sourceFile, true, options)
|
||||
}
|
||||
|
||||
// check if current node is a list item - if yes, take indentation from it
|
||||
// do not consider parent-child line sharing yet:
|
||||
// function foo(a
|
||||
// | preceding node 'a' does share line with its parent but indentation is expected
|
||||
actualIndentation := getActualIndentationForListItem(current, sourceFile, options, true /*listIndentsChild*/)
|
||||
if actualIndentation != -1 {
|
||||
return actualIndentation
|
||||
}
|
||||
|
||||
previous = current
|
||||
current = current.Parent
|
||||
}
|
||||
// no parent was found - return the base indentation of the SourceFile
|
||||
return options.BaseIndentSize
|
||||
}
|
||||
|
||||
func getIndentationForNodeWorker(
|
||||
current *ast.Node,
|
||||
currentStartLine int,
|
||||
currentStartCharacter int,
|
||||
ignoreActualIndentationRange *core.TextRange,
|
||||
indentationDelta int,
|
||||
sourceFile *ast.SourceFile,
|
||||
isNextChild bool,
|
||||
options lsutil.FormatCodeSettings,
|
||||
) int {
|
||||
parent := current.Parent
|
||||
|
||||
// Walk up the tree and collect indentation for parent-child node pairs. Indentation is not added if
|
||||
// * parent and child nodes start on the same line, or
|
||||
// * parent is an IfStatement and child starts on the same line as an 'else clause'.
|
||||
for parent != nil {
|
||||
useActualIndentation := true
|
||||
if ignoreActualIndentationRange != nil {
|
||||
start := scanner.GetTokenPosOfNode(current, sourceFile, false)
|
||||
useActualIndentation = start < ignoreActualIndentationRange.Pos() || start > ignoreActualIndentationRange.End()
|
||||
}
|
||||
|
||||
containingListOrParentStartLine, containingListOrParentStartCharacter := getContainingListOrParentStart(parent, current, sourceFile)
|
||||
parentAndChildShareLine := containingListOrParentStartLine == currentStartLine ||
|
||||
childStartsOnTheSameLineWithElseInIfStatement(parent, current, currentStartLine, sourceFile)
|
||||
|
||||
if useActualIndentation {
|
||||
// check if current node is a list item - if yes, take indentation from it
|
||||
var firstListChild *ast.Node
|
||||
containerList := GetContainingList(current, sourceFile)
|
||||
if containerList != nil {
|
||||
firstListChild = core.FirstOrNil(containerList.Nodes)
|
||||
}
|
||||
// A list indents its children if the children begin on a later line than the list itself:
|
||||
//
|
||||
// f1( L0 - List start
|
||||
// { L1 - First child start: indented, along with all other children
|
||||
// prop: 0
|
||||
// },
|
||||
// {
|
||||
// prop: 1
|
||||
// }
|
||||
// )
|
||||
//
|
||||
// f2({ L0 - List start and first child start: children are not indented.
|
||||
// prop: 0 Object properties are indented only one level, because the list
|
||||
// }, { itself contributes nothing.
|
||||
// prop: 1 L3 - The indentation of the second object literal is best understood by
|
||||
// }) looking at the relationship between the list and *first* list item.
|
||||
var listIndentsChild bool
|
||||
if firstListChild != nil {
|
||||
listLine := getStartLineForNode(firstListChild, sourceFile)
|
||||
listIndentsChild = listLine > containingListOrParentStartLine
|
||||
}
|
||||
actualIndentation := getActualIndentationForListItem(current, sourceFile, options, listIndentsChild)
|
||||
if actualIndentation != -1 {
|
||||
return actualIndentation + indentationDelta
|
||||
}
|
||||
|
||||
// try to fetch actual indentation for current node from source text
|
||||
actualIndentation = getActualIndentationForNode(current, parent, currentStartLine, currentStartCharacter, parentAndChildShareLine, sourceFile, options)
|
||||
if actualIndentation != -1 {
|
||||
return actualIndentation + indentationDelta
|
||||
}
|
||||
}
|
||||
|
||||
// increase indentation if parent node wants its content to be indented and parent and child nodes don't start on the same line
|
||||
if ShouldIndentChildNode(options, parent, current, sourceFile, isNextChild) && !parentAndChildShareLine {
|
||||
indentationDelta += options.IndentSize
|
||||
}
|
||||
|
||||
// In our AST, a call argument's `parent` is the call-expression, not the argument list.
|
||||
// We would like to increase indentation based on the relationship between an argument and its argument-list,
|
||||
// so we spoof the starting position of the (parent) call-expression to match the (non-parent) argument-list.
|
||||
// But, the spoofed start-value could then cause a problem when comparing the start position of the call-expression
|
||||
// to *its* parent (in the case of an iife, an expression statement), adding an extra level of indentation.
|
||||
//
|
||||
// Instead, when at an argument, we unspoof the starting position of the enclosing call expression
|
||||
// *after* applying indentation for the argument.
|
||||
|
||||
useTrueStart := isArgumentAndStartLineOverlapsExpressionBeingCalled(parent, current, currentStartLine, sourceFile)
|
||||
|
||||
current = parent
|
||||
parent = current.Parent
|
||||
|
||||
if useTrueStart {
|
||||
currentStartLine, currentStartCharacter = scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, scanner.GetTokenPosOfNode(current, sourceFile, false))
|
||||
} else {
|
||||
currentStartLine = containingListOrParentStartLine
|
||||
currentStartCharacter = containingListOrParentStartCharacter
|
||||
}
|
||||
}
|
||||
|
||||
return indentationDelta + options.BaseIndentSize
|
||||
}
|
||||
|
||||
/*
|
||||
* Function returns -1 if actual indentation for node should not be used (i.e because node is nested expression)
|
||||
*/
|
||||
func getActualIndentationForNode(current *ast.Node, parent *ast.Node, cuurentLine int, currentChar int, parentAndChildShareLine bool, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
// actual indentation is used for statements\declarations if one of cases below is true:
|
||||
// - parent is SourceFile - by default immediate children of SourceFile are not indented except when user indents them manually
|
||||
// - parent and child are not on the same line
|
||||
useActualIndentation := (ast.IsDeclaration(current) || ast.IsStatementButNotDeclaration(current)) && (parent.Kind == ast.KindSourceFile || !parentAndChildShareLine)
|
||||
|
||||
if !useActualIndentation {
|
||||
return -1
|
||||
}
|
||||
|
||||
return findColumnForFirstNonWhitespaceCharacterInLine(cuurentLine, currentChar, sourceFile, options)
|
||||
}
|
||||
|
||||
func isArgumentAndStartLineOverlapsExpressionBeingCalled(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
|
||||
if !(ast.IsCallExpression(parent) && slices.Contains(parent.Arguments(), child)) {
|
||||
return false
|
||||
}
|
||||
expressionOfCallExpressionEnd := parent.Expression().End()
|
||||
expressionOfCallExpressionEndLine := scanner.GetECMALineOfPosition(sourceFile, expressionOfCallExpressionEnd)
|
||||
return expressionOfCallExpressionEndLine == childStartLine
|
||||
}
|
||||
|
||||
func getActualIndentationForListItem(node *ast.Node, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings, listIndentsChild bool) int {
|
||||
if node.Parent != nil && node.Parent.Kind == ast.KindVariableDeclarationList {
|
||||
// VariableDeclarationList has no wrapping tokens
|
||||
return -1
|
||||
}
|
||||
containingList := GetContainingList(node, sourceFile)
|
||||
if containingList != nil {
|
||||
index := core.FindIndex(containingList.Nodes, func(e *ast.Node) bool { return e == node })
|
||||
if index != -1 {
|
||||
result := deriveActualIndentationFromList(containingList, index, sourceFile, options)
|
||||
if result != -1 {
|
||||
return result
|
||||
}
|
||||
}
|
||||
delta := 0
|
||||
if listIndentsChild {
|
||||
delta = options.IndentSize
|
||||
}
|
||||
res := getActualIndentationForListStartLine(containingList, sourceFile, options)
|
||||
if res == -1 {
|
||||
return delta
|
||||
}
|
||||
return res + delta
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func getActualIndentationForListStartLine(list *ast.NodeList, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
if list == nil {
|
||||
return -1
|
||||
}
|
||||
line, char := scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, list.Loc.Pos())
|
||||
return findColumnForFirstNonWhitespaceCharacterInLine(line, char, sourceFile, options)
|
||||
}
|
||||
|
||||
func deriveActualIndentationFromList(list *ast.NodeList, index int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
debug.Assert(list != nil && index >= 0 && index < len(list.Nodes))
|
||||
|
||||
node := list.Nodes[index]
|
||||
|
||||
// walk toward the start of the list starting from current node and check if the line is the same for all items.
|
||||
// if end line for item [i - 1] differs from the start line for item [i] - find column of the first non-whitespace character on the line of item [i]
|
||||
|
||||
line, char := getStartLineAndCharacterForNode(node, sourceFile)
|
||||
|
||||
for i := index; i >= 0; i-- {
|
||||
if list.Nodes[i].Kind == ast.KindCommaToken {
|
||||
continue
|
||||
}
|
||||
// skip list items that ends on the same line with the current list element
|
||||
prevEndLine := scanner.GetECMALineOfPosition(sourceFile, list.Nodes[i].End())
|
||||
if prevEndLine != line {
|
||||
return findColumnForFirstNonWhitespaceCharacterInLine(line, char, sourceFile, options)
|
||||
}
|
||||
|
||||
line, char = getStartLineAndCharacterForNode(list.Nodes[i], sourceFile)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func findColumnForFirstNonWhitespaceCharacterInLine(line int, char int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
lineStart := scanner.GetECMAPositionOfLineAndByteOffset(sourceFile, line, 0)
|
||||
return FindFirstNonWhitespaceColumn(lineStart, lineStart+char, sourceFile, options)
|
||||
}
|
||||
|
||||
func FindFirstNonWhitespaceColumn(startPos int, endPos int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
|
||||
_, col := findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options)
|
||||
return col
|
||||
}
|
||||
|
||||
/**
|
||||
* Character is the actual index of the character since the beginning of the line.
|
||||
* Column - position of the character after expanding tabs to spaces.
|
||||
* "0\t2$"
|
||||
* value of 'character' for '$' is 3
|
||||
* value of 'column' for '$' is 6 (assuming that tab size is 4)
|
||||
*/
|
||||
func findFirstNonWhitespaceCharacterAndColumn(startPos int, endPos int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) (character int, column int) {
|
||||
column = 0
|
||||
text := sourceFile.Text()
|
||||
pos := startPos
|
||||
for pos < endPos {
|
||||
ch, size := utf8.DecodeRuneInString(text[pos:])
|
||||
if !stringutil.IsWhiteSpaceSingleLine(ch) {
|
||||
break
|
||||
}
|
||||
|
||||
if ch == '\t' {
|
||||
if options.TabSize > 0 {
|
||||
column += options.TabSize + (column % options.TabSize)
|
||||
}
|
||||
} else {
|
||||
column++
|
||||
}
|
||||
|
||||
pos += size
|
||||
}
|
||||
return pos - startPos, column
|
||||
}
|
||||
|
||||
func childStartsOnTheSameLineWithElseInIfStatement(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
|
||||
if parent.Kind == ast.KindIfStatement && parent.AsIfStatement().ElseStatement == child {
|
||||
elseKeyword := astnav.FindPrecedingToken(sourceFile, child.Pos())
|
||||
debug.Assert(elseKeyword != nil)
|
||||
elseKeywordStartLine := getStartLineForNode(elseKeyword, sourceFile)
|
||||
return elseKeywordStartLine == childStartLine
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getStartLineAndCharacterForNode(n *ast.Node, sourceFile *ast.SourceFile) (line int, character int) {
|
||||
return scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false))
|
||||
}
|
||||
|
||||
func getStartLineForNode(n *ast.Node, sourceFile *ast.SourceFile) int {
|
||||
return scanner.GetECMALineOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false))
|
||||
}
|
||||
|
||||
func GetContainingList(node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
|
||||
if node.Parent == nil {
|
||||
return nil
|
||||
}
|
||||
return getListByRange(scanner.GetTokenPosOfNode(node, sourceFile, false), node.End(), node.Parent, sourceFile)
|
||||
}
|
||||
|
||||
func getListByPosition(pos int, node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return getListByRange(pos, pos, node, sourceFile)
|
||||
}
|
||||
|
||||
func getListByRange(start int, end int, node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
|
||||
r := core.NewTextRange(start, end)
|
||||
switch node.Kind {
|
||||
case ast.KindTypeReference:
|
||||
return getList(node.TypeArgumentList(), r, node, sourceFile)
|
||||
case ast.KindObjectLiteralExpression:
|
||||
return getList(node.PropertyList(), r, node, sourceFile)
|
||||
case ast.KindArrayLiteralExpression:
|
||||
return getList(node.ElementList(), r, node, sourceFile)
|
||||
case ast.KindTypeLiteral:
|
||||
return getList(node.MemberList(), r, node, sourceFile)
|
||||
case ast.KindFunctionDeclaration,
|
||||
ast.KindFunctionExpression,
|
||||
ast.KindArrowFunction,
|
||||
ast.KindMethodDeclaration,
|
||||
ast.KindMethodSignature,
|
||||
ast.KindCallSignature,
|
||||
ast.KindConstructor,
|
||||
ast.KindConstructorType,
|
||||
ast.KindConstructSignature:
|
||||
tpl := getList(node.TypeParameterList(), r, node, sourceFile)
|
||||
if tpl != nil {
|
||||
return tpl
|
||||
}
|
||||
return getList(node.ParameterList(), r, node, sourceFile)
|
||||
case ast.KindGetAccessor:
|
||||
return getList(node.ParameterList(), r, node, sourceFile)
|
||||
case ast.KindClassDeclaration,
|
||||
ast.KindClassExpression,
|
||||
ast.KindInterfaceDeclaration,
|
||||
ast.KindTypeAliasDeclaration,
|
||||
ast.KindJSDocTemplateTag:
|
||||
return getList(node.TypeParameterList(), r, node, sourceFile)
|
||||
case ast.KindNewExpression, ast.KindCallExpression:
|
||||
l := getList(node.TypeArgumentList(), r, node, sourceFile)
|
||||
if l != nil {
|
||||
return l
|
||||
}
|
||||
return getList(node.ArgumentList(), r, node, sourceFile)
|
||||
case ast.KindVariableDeclarationList:
|
||||
return getList(node.AsVariableDeclarationList().Declarations, r, node, sourceFile)
|
||||
case ast.KindObjectBindingPattern, ast.KindArrayBindingPattern, ast.KindNamedImports, ast.KindNamedExports:
|
||||
return getList(node.ElementList(), r, node, sourceFile)
|
||||
}
|
||||
return nil // TODO: should this be a panic? It isn't in strada.
|
||||
}
|
||||
|
||||
func getList(list *ast.NodeList, r core.TextRange, node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
if r.ContainedBy(getVisualListRange(node, list.Loc, sourceFile)) {
|
||||
return list
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getVisualListRange(node *ast.Node, list core.TextRange, sourceFile *ast.SourceFile) core.TextRange {
|
||||
// In strada, this relied on the services .getChildren method, which manifested synthetic token nodes
|
||||
// _however_, the logic boils down to "find the child with the matching span and adjust its start to the
|
||||
// previous (possibly token) child's end and its end to the token start of the following element" - basically
|
||||
// expanding the range to encompass all the neighboring non-token trivia
|
||||
// Now, we perform that logic with the scanner instead
|
||||
prior := astnav.FindPrecedingToken(sourceFile, list.Pos())
|
||||
var priorEnd int
|
||||
if prior == nil {
|
||||
priorEnd = list.Pos()
|
||||
} else {
|
||||
priorEnd = prior.End()
|
||||
}
|
||||
// Find the token that starts at or after list.End() using the scanner
|
||||
scan := scanner.GetScannerForSourceFile(sourceFile, list.End())
|
||||
var nextStart int
|
||||
if scan.Token() == ast.KindEndOfFile {
|
||||
nextStart = list.End()
|
||||
} else {
|
||||
nextStart = scan.TokenStart()
|
||||
}
|
||||
return core.NewTextRange(priorEnd, nextStart)
|
||||
}
|
||||
|
||||
func getContainingListOrParentStart(parent *ast.Node, child *ast.Node, sourceFile *ast.SourceFile) (line int, character int) {
|
||||
containingList := GetContainingList(child, sourceFile)
|
||||
var startPos int
|
||||
if containingList != nil {
|
||||
startPos = containingList.Loc.Pos()
|
||||
} else {
|
||||
startPos = scanner.GetTokenPosOfNode(parent, sourceFile, false)
|
||||
}
|
||||
return scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, startPos)
|
||||
}
|
||||
|
||||
func isControlFlowEndingStatement(kind ast.Kind, parentKind ast.Kind) bool {
|
||||
switch kind {
|
||||
case ast.KindReturnStatement, ast.KindThrowStatement, ast.KindContinueStatement, ast.KindBreakStatement:
|
||||
return parentKind != ast.KindBlock
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the parent node should indent the given child by an explicit rule.
|
||||
* @param isNextChild If true, we are judging indent of a hypothetical child *after* this one, not the current child.
|
||||
*/
|
||||
func ShouldIndentChildNode(settings lsutil.FormatCodeSettings, parent *ast.Node, child *ast.Node, sourceFile *ast.SourceFile, isNextChildArg ...bool) bool {
|
||||
isNextChild := false
|
||||
if len(isNextChildArg) > 0 {
|
||||
isNextChild = isNextChildArg[0]
|
||||
}
|
||||
|
||||
return NodeWillIndentChild(settings, parent, child, sourceFile, false) && !(isNextChild && child != nil && isControlFlowEndingStatement(child.Kind, parent.Kind))
|
||||
}
|
||||
|
||||
func NodeWillIndentChild(settings lsutil.FormatCodeSettings, parent *ast.Node, child *ast.Node, sourceFile *ast.SourceFile, indentByDefault bool) bool {
|
||||
childKind := ast.KindUnknown
|
||||
if child != nil {
|
||||
childKind = child.Kind
|
||||
}
|
||||
|
||||
switch parent.Kind {
|
||||
case ast.KindExpressionStatement,
|
||||
ast.KindClassDeclaration,
|
||||
ast.KindClassExpression,
|
||||
ast.KindInterfaceDeclaration,
|
||||
ast.KindEnumDeclaration,
|
||||
ast.KindTypeAliasDeclaration,
|
||||
ast.KindArrayLiteralExpression,
|
||||
ast.KindBlock,
|
||||
ast.KindModuleBlock,
|
||||
ast.KindObjectLiteralExpression,
|
||||
ast.KindTypeLiteral,
|
||||
ast.KindMappedType,
|
||||
ast.KindTupleType,
|
||||
ast.KindParenthesizedExpression,
|
||||
ast.KindPropertyAccessExpression,
|
||||
ast.KindCallExpression,
|
||||
ast.KindNewExpression,
|
||||
ast.KindVariableStatement,
|
||||
ast.KindExportAssignment,
|
||||
ast.KindReturnStatement,
|
||||
ast.KindConditionalExpression,
|
||||
ast.KindArrayBindingPattern,
|
||||
ast.KindObjectBindingPattern,
|
||||
ast.KindJsxOpeningElement,
|
||||
ast.KindJsxOpeningFragment,
|
||||
ast.KindJsxSelfClosingElement,
|
||||
ast.KindJsxExpression,
|
||||
ast.KindMethodSignature,
|
||||
ast.KindCallSignature,
|
||||
ast.KindConstructSignature,
|
||||
ast.KindParameter,
|
||||
ast.KindFunctionType,
|
||||
ast.KindConstructorType,
|
||||
ast.KindParenthesizedType,
|
||||
ast.KindTaggedTemplateExpression,
|
||||
ast.KindAwaitExpression,
|
||||
ast.KindNamedExports,
|
||||
ast.KindNamedImports,
|
||||
ast.KindExportSpecifier,
|
||||
ast.KindImportSpecifier,
|
||||
ast.KindPropertyDeclaration,
|
||||
ast.KindCaseClause,
|
||||
ast.KindDefaultClause:
|
||||
return true
|
||||
case ast.KindCaseBlock:
|
||||
return settings.IndentSwitchCase.IsTrueOrUnknown()
|
||||
case ast.KindVariableDeclaration, ast.KindPropertyAssignment, ast.KindBinaryExpression:
|
||||
if settings.IndentMultiLineObjectLiteralBeginningOnBlankLine.IsFalseOrUnknown() && sourceFile != nil && childKind == ast.KindObjectLiteralExpression {
|
||||
return rangeIsOnOneLine(child.Loc, sourceFile)
|
||||
}
|
||||
if parent.Kind == ast.KindBinaryExpression && sourceFile != nil && childKind == ast.KindJsxElement {
|
||||
parentStartLine := scanner.GetECMALineOfPosition(sourceFile, scanner.SkipTrivia(sourceFile.Text(), parent.Pos()))
|
||||
childStartLine := scanner.GetECMALineOfPosition(sourceFile, scanner.SkipTrivia(sourceFile.Text(), child.Pos()))
|
||||
return parentStartLine != childStartLine
|
||||
}
|
||||
if parent.Kind != ast.KindBinaryExpression {
|
||||
return true
|
||||
}
|
||||
return indentByDefault
|
||||
case ast.KindDoStatement,
|
||||
ast.KindWhileStatement,
|
||||
ast.KindForInStatement,
|
||||
ast.KindForOfStatement,
|
||||
ast.KindForStatement,
|
||||
ast.KindIfStatement,
|
||||
ast.KindFunctionDeclaration,
|
||||
ast.KindFunctionExpression,
|
||||
ast.KindMethodDeclaration,
|
||||
ast.KindConstructor,
|
||||
ast.KindGetAccessor,
|
||||
ast.KindSetAccessor:
|
||||
return childKind != ast.KindBlock
|
||||
case ast.KindArrowFunction:
|
||||
if sourceFile != nil && childKind == ast.KindParenthesizedExpression {
|
||||
return rangeIsOnOneLine(child.Loc, sourceFile)
|
||||
}
|
||||
return childKind != ast.KindBlock
|
||||
case ast.KindExportDeclaration:
|
||||
return childKind != ast.KindNamedExports
|
||||
case ast.KindImportDeclaration:
|
||||
return childKind != ast.KindImportClause || (child.AsImportClause().NamedBindings != nil && child.AsImportClause().NamedBindings.Kind != ast.KindNamedImports)
|
||||
case ast.KindJsxElement:
|
||||
return childKind != ast.KindJsxClosingElement
|
||||
case ast.KindJsxFragment:
|
||||
return childKind != ast.KindJsxClosingFragment
|
||||
case ast.KindIntersectionType, ast.KindUnionType, ast.KindSatisfiesExpression:
|
||||
if childKind == ast.KindTypeLiteral || childKind == ast.KindTupleType || childKind == ast.KindMappedType {
|
||||
return false
|
||||
}
|
||||
return indentByDefault
|
||||
case ast.KindTryStatement:
|
||||
if childKind == ast.KindBlock {
|
||||
return false
|
||||
}
|
||||
return indentByDefault
|
||||
}
|
||||
|
||||
// No explicit rule for given nodes so the result will follow the default value argument
|
||||
return indentByDefault
|
||||
}
|
||||
|
||||
// A multiline conditional typically increases the indentation of its whenTrue and whenFalse children:
|
||||
//
|
||||
// condition
|
||||
//
|
||||
// ? whenTrue
|
||||
// : whenFalse;
|
||||
//
|
||||
// However, that indentation does not apply if the subexpressions themselves span multiple lines,
|
||||
// applying their own indentation:
|
||||
//
|
||||
// (() => {
|
||||
// return complexCalculationForCondition();
|
||||
// })() ? {
|
||||
//
|
||||
// whenTrue: 'multiline object literal'
|
||||
// } : (
|
||||
//
|
||||
// whenFalse('multiline parenthesized expression')
|
||||
//
|
||||
// );
|
||||
//
|
||||
// In these cases, we must discard the indentation increase that would otherwise be applied to the
|
||||
// whenTrue and whenFalse children to avoid double-indenting their contents. To identify this scenario,
|
||||
// we check for the whenTrue branch beginning on the line that the condition ends, and the whenFalse
|
||||
// branch beginning on the line that the whenTrue branch ends.
|
||||
func childIsUnindentedBranchOfConditionalExpression(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
|
||||
if parent.Kind == ast.KindConditionalExpression && (child == parent.AsConditionalExpression().WhenTrue || child == parent.AsConditionalExpression().WhenFalse) {
|
||||
conditionEndLine := scanner.GetECMALineOfPosition(sourceFile, parent.AsConditionalExpression().Condition.End())
|
||||
if child == parent.AsConditionalExpression().WhenTrue {
|
||||
return childStartLine == conditionEndLine
|
||||
} else {
|
||||
// On the whenFalse side, we have to look at the whenTrue side, because if that one was
|
||||
// indented, whenFalse must also be indented:
|
||||
//
|
||||
// const y = true
|
||||
// ? 1 : ( L1: whenTrue indented because it's on a new line
|
||||
// 0 L2: indented two stops, one because whenTrue was indented
|
||||
// ); and one because of the parentheses spanning multiple lines
|
||||
trueStartLine := getStartLineForNode(parent.AsConditionalExpression().WhenTrue, sourceFile)
|
||||
trueEndLine := scanner.GetECMALineOfPosition(sourceFile, parent.AsConditionalExpression().WhenTrue.End())
|
||||
return conditionEndLine == trueStartLine && trueEndLine == childStartLine
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func argumentStartsOnSameLineAsPreviousArgument(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
|
||||
if ast.IsCallExpression(parent) || ast.IsNewExpression(parent) {
|
||||
if len(parent.Arguments()) == 0 {
|
||||
return false
|
||||
}
|
||||
currentIndex := core.FindIndex(parent.Arguments(), func(n *ast.Node) bool { return n == child })
|
||||
if currentIndex == -1 {
|
||||
// If it's not one of the arguments, don't look past this
|
||||
return false
|
||||
}
|
||||
if currentIndex == 0 {
|
||||
return false // Can't look at previous node if first
|
||||
}
|
||||
|
||||
previousNode := parent.Arguments()[currentIndex-1]
|
||||
lineOfPreviousNode := scanner.GetECMALineOfPosition(sourceFile, previousNode.End())
|
||||
if childStartLine == lineOfPreviousNode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user