822 lines
32 KiB
Go
822 lines
32 KiB
Go
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
|
|
}
|