vendor tsgo
This commit is contained in:
270
tools/tsgo/internal/ls/change/delete.go
Normal file
270
tools/tsgo/internal/ls/change/delete.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"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/format"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
// deleteDeclaration deletes a node with smart handling for different node types.
|
||||
// This handles special cases like import specifiers in lists, parameters, etc.
|
||||
func deleteDeclaration(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) {
|
||||
switch node.Kind {
|
||||
case ast.KindParameter:
|
||||
oldFunction := node.Parent
|
||||
if oldFunction.Kind == ast.KindArrowFunction &&
|
||||
len(oldFunction.AsArrowFunction().Parameters.Nodes) == 1 &&
|
||||
astnav.FindChildOfKind(oldFunction, ast.KindOpenParenToken, sourceFile) == nil {
|
||||
// Lambdas with exactly one parameter are special because, after removal, there
|
||||
// must be an empty parameter list (i.e. `()`) and this won't necessarily be the
|
||||
// case if the parameter is simply removed (e.g. in `x => 1`).
|
||||
t.ReplaceRangeWithText(sourceFile, t.GetAdjustedRange(sourceFile, node, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude), "()")
|
||||
} else {
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
}
|
||||
|
||||
case ast.KindImportDeclaration, ast.KindImportEqualsDeclaration:
|
||||
imports := sourceFile.Imports()
|
||||
isFirstImport := len(imports) > 0 && node == imports[0].Parent ||
|
||||
node == core.Find(sourceFile.Statements.Nodes, func(s *ast.Node) bool { return ast.IsAnyImportSyntax(s) })
|
||||
// For first import, leave header comment in place, otherwise only delete JSDoc comments
|
||||
leadingTrivia := LeadingTriviaOptionStartLine
|
||||
if isFirstImport {
|
||||
leadingTrivia = LeadingTriviaOptionExclude
|
||||
} else if hasJSDocNodes(node) {
|
||||
leadingTrivia = LeadingTriviaOptionJSDoc
|
||||
}
|
||||
deleteNode(t, sourceFile, node, leadingTrivia, TrailingTriviaOptionInclude)
|
||||
|
||||
case ast.KindBindingElement:
|
||||
pattern := node.Parent
|
||||
preserveComma := pattern.Kind == ast.KindArrayBindingPattern &&
|
||||
node != pattern.AsBindingPattern().Elements.Nodes[len(pattern.AsBindingPattern().Elements.Nodes)-1]
|
||||
if preserveComma {
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionExclude)
|
||||
} else {
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
}
|
||||
|
||||
case ast.KindVariableDeclaration:
|
||||
deleteVariableDeclaration(t, deletedNodesInLists, sourceFile, node)
|
||||
|
||||
case ast.KindTypeParameter:
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
|
||||
case ast.KindImportSpecifier:
|
||||
namedImports := node.Parent
|
||||
if len(namedImports.AsNamedImports().Elements.Nodes) == 1 {
|
||||
deleteImportBinding(t, sourceFile, namedImports)
|
||||
} else {
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
}
|
||||
|
||||
case ast.KindNamespaceImport:
|
||||
deleteImportBinding(t, sourceFile, node)
|
||||
|
||||
case ast.KindSemicolonToken:
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionExclude)
|
||||
|
||||
case ast.KindTypeKeyword:
|
||||
// For type keyword in import clauses, we need to delete the keyword and any trailing space
|
||||
// The trailing space is part of the next token's leading trivia, so we include it
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionExclude, TrailingTriviaOptionInclude)
|
||||
|
||||
case ast.KindFunctionKeyword:
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionExclude, TrailingTriviaOptionInclude)
|
||||
|
||||
case ast.KindClassDeclaration, ast.KindFunctionDeclaration:
|
||||
leadingTrivia := LeadingTriviaOptionStartLine
|
||||
if hasJSDocNodes(node) {
|
||||
leadingTrivia = LeadingTriviaOptionJSDoc
|
||||
}
|
||||
deleteNode(t, sourceFile, node, leadingTrivia, TrailingTriviaOptionInclude)
|
||||
|
||||
default:
|
||||
if node.Parent == nil {
|
||||
// a misbehaving client can reach here with the SourceFile node
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
} else if node.Parent.Kind == ast.KindImportClause && node.Parent.AsImportClause().Name() == node {
|
||||
deleteDefaultImport(t, sourceFile, node.Parent)
|
||||
} else if node.Parent.Kind == ast.KindCallExpression && slices.Contains(node.Parent.AsCallExpression().Arguments.Nodes, node) {
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
} else {
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteDefaultImport(t *Tracker, sourceFile *ast.SourceFile, importClause *ast.Node) {
|
||||
clause := importClause.AsImportClause()
|
||||
if clause.NamedBindings == nil {
|
||||
// Delete the whole import
|
||||
deleteNode(t, sourceFile, importClause.Parent, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
} else {
|
||||
// import |d,| * as ns from './file'
|
||||
name := clause.Name()
|
||||
start := astnav.GetStartOfNode(name, sourceFile, false)
|
||||
nextToken := astnav.GetTokenAtPosition(sourceFile, name.End())
|
||||
if nextToken != nil && nextToken.Kind == ast.KindCommaToken {
|
||||
// shift first non-whitespace position after comma to the start position of the node
|
||||
end := scanner.SkipTriviaEx(sourceFile.Text(), nextToken.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: false, StopAtComments: true})
|
||||
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(start))
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(end))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
|
||||
} else {
|
||||
deleteNode(t, sourceFile, name, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteImportBinding(t *Tracker, sourceFile *ast.SourceFile, node *ast.Node) {
|
||||
importClause := node.Parent.AsImportClause()
|
||||
if importClause.Name() != nil {
|
||||
// Delete named imports while preserving the default import
|
||||
// import d|, * as ns| from './file'
|
||||
// import d|, { a }| from './file'
|
||||
previousToken := astnav.GetTokenAtPosition(sourceFile, node.Pos()-1)
|
||||
debug.Assert(previousToken != nil, "previousToken should not be nil")
|
||||
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(astnav.GetStartOfNode(previousToken, sourceFile, false)))
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(node.End()))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
|
||||
} else {
|
||||
// Delete the entire import declaration
|
||||
// |import * as ns from './file'|
|
||||
// |import { a } from './file'|
|
||||
importDecl := ast.FindAncestorKind(node, ast.KindImportDeclaration)
|
||||
debug.Assert(importDecl != nil, "importDecl should not be nil")
|
||||
deleteNode(t, sourceFile, importDecl, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteVariableDeclaration(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) {
|
||||
parent := node.Parent
|
||||
|
||||
if parent.Kind == ast.KindCatchClause {
|
||||
// TODO: There's currently no unused diagnostic for this, could be a suggestion
|
||||
openParen := astnav.FindChildOfKind(parent, ast.KindOpenParenToken, sourceFile)
|
||||
closeParen := astnav.FindChildOfKind(parent, ast.KindCloseParenToken, sourceFile)
|
||||
debug.Assert(openParen != nil && closeParen != nil, "catch clause should have parens")
|
||||
t.DeleteNodeRange(sourceFile, openParen, closeParen, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
return
|
||||
}
|
||||
|
||||
if len(parent.AsVariableDeclarationList().Declarations.Nodes) != 1 {
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
return
|
||||
}
|
||||
|
||||
gp := parent.Parent
|
||||
switch gp.Kind {
|
||||
case ast.KindForOfStatement, ast.KindForInStatement:
|
||||
t.ReplaceNode(sourceFile, node, t.NodeFactory.NewObjectLiteralExpression(t.NodeFactory.NewNodeList([]*ast.Node{}), false), nil)
|
||||
|
||||
case ast.KindForStatement:
|
||||
deleteNode(t, sourceFile, parent, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
|
||||
case ast.KindVariableStatement:
|
||||
leadingTrivia := LeadingTriviaOptionStartLine
|
||||
if hasJSDocNodes(gp) {
|
||||
leadingTrivia = LeadingTriviaOptionJSDoc
|
||||
}
|
||||
deleteNode(t, sourceFile, gp, leadingTrivia, TrailingTriviaOptionInclude)
|
||||
|
||||
default:
|
||||
debug.Fail("Unexpected grandparent kind: " + gp.Kind.String())
|
||||
}
|
||||
}
|
||||
|
||||
// deleteNode deletes a node with the specified trivia options.
|
||||
// Warning: This deletes comments too.
|
||||
func deleteNode(t *Tracker, sourceFile *ast.SourceFile, node *ast.Node, leadingTrivia LeadingTriviaOption, trailingTrivia TrailingTriviaOption) {
|
||||
startPosition := t.getAdjustedStartPosition(sourceFile, node, leadingTrivia, false)
|
||||
endPosition := t.getAdjustedEndPosition(sourceFile, node, trailingTrivia)
|
||||
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPosition))
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(endPosition))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
|
||||
}
|
||||
|
||||
func deleteNodeInList(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) {
|
||||
containingList := format.GetContainingList(node, sourceFile)
|
||||
debug.Assert(containingList != nil, "containingList should not be nil")
|
||||
index := slices.Index(containingList.Nodes, node)
|
||||
debug.Assert(index != -1, "node should be in containing list")
|
||||
|
||||
if len(containingList.Nodes) == 1 {
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
return
|
||||
}
|
||||
|
||||
// Note: We will only delete a comma *after* a node. This will leave a trailing comma if we delete the last node.
|
||||
// That's handled in the end by finishTrailingCommaAfterDeletingNodesInList.
|
||||
debug.Assert(!deletedNodesInLists[node], "Deleting a node twice")
|
||||
deletedNodesInLists[node] = true
|
||||
|
||||
startPos := t.startPositionToDeleteNodeInList(sourceFile, node)
|
||||
var endPos int
|
||||
if index == len(containingList.Nodes)-1 {
|
||||
endPos = t.getAdjustedEndPosition(sourceFile, node, TrailingTriviaOptionNone)
|
||||
} else {
|
||||
prevNode := (*ast.Node)(nil)
|
||||
if index > 0 {
|
||||
prevNode = containingList.Nodes[index-1]
|
||||
}
|
||||
endPos = t.endPositionToDeleteNodeInList(sourceFile, node, prevNode, containingList.Nodes[index+1])
|
||||
}
|
||||
|
||||
startLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPos))
|
||||
endLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(endPos))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startLSPos, End: endLSPos}, "")
|
||||
}
|
||||
|
||||
// startPositionToDeleteNodeInList finds the first non-whitespace position in the leading trivia of the node
|
||||
func (t *Tracker) startPositionToDeleteNodeInList(sourceFile *ast.SourceFile, node *ast.Node) int {
|
||||
start := t.getAdjustedStartPosition(sourceFile, node, LeadingTriviaOptionIncludeAll, false)
|
||||
return scanner.SkipTriviaEx(sourceFile.Text(), start, &scanner.SkipTriviaOptions{StopAfterLineBreak: false, StopAtComments: true})
|
||||
}
|
||||
|
||||
func (t *Tracker) endPositionToDeleteNodeInList(sourceFile *ast.SourceFile, node *ast.Node, prevNode *ast.Node, nextNode *ast.Node) int {
|
||||
end := t.startPositionToDeleteNodeInList(sourceFile, nextNode)
|
||||
if prevNode == nil || positionsAreOnSameLine(t.getAdjustedEndPosition(sourceFile, node, TrailingTriviaOptionInclude), end, sourceFile) {
|
||||
return end
|
||||
}
|
||||
token := astnav.FindPrecedingToken(sourceFile, astnav.GetStartOfNode(nextNode, sourceFile, false))
|
||||
if isSeparator(node, token) {
|
||||
prevToken := astnav.FindPrecedingToken(sourceFile, astnav.GetStartOfNode(node, sourceFile, false))
|
||||
if isSeparator(prevNode, prevToken) {
|
||||
pos := scanner.SkipTriviaEx(sourceFile.Text(), token.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true})
|
||||
if positionsAreOnSameLine(astnav.GetStartOfNode(prevToken, sourceFile, false), astnav.GetStartOfNode(token, sourceFile, false), sourceFile) {
|
||||
if pos > 0 && stringutil.IsLineBreak(rune(sourceFile.Text()[pos-1])) {
|
||||
return pos - 1
|
||||
}
|
||||
return pos
|
||||
}
|
||||
if stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) {
|
||||
return pos
|
||||
}
|
||||
}
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func positionsAreOnSameLine(pos1, pos2 int, sourceFile *ast.SourceFile) bool {
|
||||
return format.GetLineStartPositionForPosition(pos1, sourceFile) == format.GetLineStartPositionForPosition(pos2, sourceFile)
|
||||
}
|
||||
|
||||
// hasJSDocNodes checks if a node has JSDoc comments
|
||||
func hasJSDocNodes(node *ast.Node) bool {
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
// nil is ok for JSDoc - it will return empty slice if not available
|
||||
jsdocs := node.JSDoc(nil)
|
||||
return len(jsdocs) > 0
|
||||
}
|
||||
751
tools/tsgo/internal/ls/change/tracker.go
Normal file
751
tools/tsgo/internal/ls/change/tracker.go
Normal file
@@ -0,0 +1,751 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/format"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
type NodeOptions struct {
|
||||
// Text to be inserted before the new node
|
||||
Prefix string
|
||||
|
||||
// Text to be inserted after the new node
|
||||
Suffix string
|
||||
|
||||
// Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node
|
||||
indentation *int
|
||||
|
||||
// Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind
|
||||
delta *int
|
||||
|
||||
LeadingTriviaOption
|
||||
TrailingTriviaOption
|
||||
joiner string
|
||||
}
|
||||
|
||||
type LeadingTriviaOption int
|
||||
|
||||
const (
|
||||
LeadingTriviaOptionNone LeadingTriviaOption = 0
|
||||
LeadingTriviaOptionExclude LeadingTriviaOption = 1
|
||||
LeadingTriviaOptionIncludeAll LeadingTriviaOption = 2
|
||||
LeadingTriviaOptionJSDoc LeadingTriviaOption = 3
|
||||
LeadingTriviaOptionStartLine LeadingTriviaOption = 4
|
||||
)
|
||||
|
||||
type TrailingTriviaOption int
|
||||
|
||||
const (
|
||||
TrailingTriviaOptionNone TrailingTriviaOption = 0
|
||||
TrailingTriviaOptionExclude TrailingTriviaOption = 1
|
||||
TrailingTriviaOptionExcludeWhitespace TrailingTriviaOption = 2
|
||||
TrailingTriviaOptionInclude TrailingTriviaOption = 3
|
||||
)
|
||||
|
||||
type trackerEditKind int
|
||||
|
||||
const (
|
||||
trackerEditKindText trackerEditKind = 1
|
||||
trackerEditKindRemove trackerEditKind = 2
|
||||
trackerEditKindReplaceWithSingleNode trackerEditKind = 3
|
||||
trackerEditKindReplaceWithMultipleNodes trackerEditKind = 4
|
||||
)
|
||||
|
||||
type trackerEdit struct {
|
||||
kind trackerEditKind
|
||||
lsproto.Range
|
||||
|
||||
NewText string // kind == text
|
||||
|
||||
*ast.Node // single
|
||||
nodes []*ast.Node // multiple
|
||||
options NodeOptions
|
||||
}
|
||||
|
||||
type nodesInsertedAtStartState struct {
|
||||
node *ast.Node
|
||||
sourceFile *ast.SourceFile
|
||||
}
|
||||
|
||||
type Tracker struct {
|
||||
// initialized with
|
||||
formatSettings lsutil.FormatCodeSettings
|
||||
newLine string
|
||||
converters *lsconv.Converters
|
||||
ctx context.Context
|
||||
*printer.EmitContext
|
||||
|
||||
*ast.NodeFactory
|
||||
|
||||
changes *collections.MultiMap[*ast.SourceFile, *trackerEdit]
|
||||
deletedNodes []deletedNode
|
||||
nodesWithInsertionsAtStart map[*ast.Node]*nodesInsertedAtStartState
|
||||
|
||||
// created during call to getChanges
|
||||
writer *printer.ChangeTrackerWriter
|
||||
// printer
|
||||
}
|
||||
|
||||
type deletedNode struct {
|
||||
sourceFile *ast.SourceFile
|
||||
node *ast.Node
|
||||
}
|
||||
|
||||
func NewTracker(ctx context.Context, compilerOptions *core.CompilerOptions, formatOptions lsutil.FormatCodeSettings, converters *lsconv.Converters) *Tracker {
|
||||
emitContext := printer.NewEmitContext()
|
||||
newLine := compilerOptions.NewLine.GetNewLineCharacter()
|
||||
ctx = format.WithFormatCodeSettings(ctx, formatOptions, newLine) // !!! formatSettings in context?
|
||||
return &Tracker{
|
||||
EmitContext: emitContext,
|
||||
NodeFactory: &emitContext.Factory.NodeFactory,
|
||||
changes: &collections.MultiMap[*ast.SourceFile, *trackerEdit]{},
|
||||
ctx: ctx,
|
||||
converters: converters,
|
||||
formatSettings: formatOptions,
|
||||
newLine: newLine,
|
||||
nodesWithInsertionsAtStart: make(map[*ast.Node]*nodesInsertedAtStartState),
|
||||
}
|
||||
}
|
||||
|
||||
// GetChanges returns the accumulated text edits.
|
||||
// Note: after calling this, the Tracker object must be discarded!
|
||||
func (t *Tracker) GetChanges() map[string][]*lsproto.TextEdit {
|
||||
t.finishDeleteDeclarations()
|
||||
t.finishNodesWithInsertionsAtStart()
|
||||
changes := t.getTextChangesFromChanges()
|
||||
// !!! changes for new files
|
||||
return changes
|
||||
}
|
||||
|
||||
func (t *Tracker) ReplaceNode(sourceFile *ast.SourceFile, oldNode *ast.Node, newNode *ast.Node, options *NodeOptions) {
|
||||
if options == nil {
|
||||
// defaults to `useNonAdjustedPositions`
|
||||
options = &NodeOptions{
|
||||
LeadingTriviaOption: LeadingTriviaOptionExclude,
|
||||
TrailingTriviaOption: TrailingTriviaOptionExclude,
|
||||
}
|
||||
}
|
||||
t.ReplaceRange(sourceFile, t.GetAdjustedRange(sourceFile, oldNode, oldNode, options.LeadingTriviaOption, options.TrailingTriviaOption), newNode, *options)
|
||||
}
|
||||
|
||||
func (t *Tracker) ReplaceNodeWithNodes(sourceFile *ast.SourceFile, oldNode *ast.Node, newNodes []*ast.Node, options *NodeOptions) {
|
||||
if options == nil {
|
||||
options = &NodeOptions{
|
||||
LeadingTriviaOption: LeadingTriviaOptionExclude,
|
||||
TrailingTriviaOption: TrailingTriviaOptionExclude,
|
||||
}
|
||||
}
|
||||
t.ReplaceRangeWithNodes(sourceFile, t.GetAdjustedRange(sourceFile, oldNode, oldNode, options.LeadingTriviaOption, options.TrailingTriviaOption), newNodes, *options)
|
||||
}
|
||||
|
||||
func (t *Tracker) ReplaceRange(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNode *ast.Node, options NodeOptions) {
|
||||
t.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithSingleNode, Range: lsprotoRange, options: options, Node: newNode})
|
||||
}
|
||||
|
||||
func (t *Tracker) ReplaceRangeWithText(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, text string) {
|
||||
t.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindText, Range: lsprotoRange, NewText: text})
|
||||
}
|
||||
|
||||
func (t *Tracker) ReplaceRangeWithNodes(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNodes []*ast.Node, options NodeOptions) {
|
||||
if len(newNodes) == 1 {
|
||||
t.ReplaceRange(sourceFile, lsprotoRange, newNodes[0], options)
|
||||
return
|
||||
}
|
||||
t.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithMultipleNodes, Range: lsprotoRange, nodes: newNodes, options: options})
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertText(sourceFile *ast.SourceFile, pos lsproto.Position, text string) {
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: pos, End: pos}, text)
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertNodeAt(sourceFile *ast.SourceFile, pos core.TextPos, newNode *ast.Node, options NodeOptions) {
|
||||
lsPos := t.converters.PositionToLineAndCharacter(sourceFile, pos)
|
||||
t.ReplaceRange(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNode, options)
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertNodesAt(sourceFile *ast.SourceFile, pos core.TextPos, newNodes []*ast.Node, options NodeOptions) {
|
||||
lsPos := t.converters.PositionToLineAndCharacter(sourceFile, pos)
|
||||
t.ReplaceRangeWithNodes(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNodes, options)
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) {
|
||||
endPosition := t.endPosForInsertNodeAfter(sourceFile, after, newNode)
|
||||
t.InsertNodeAt(sourceFile, endPosition, newNode, t.getInsertNodeAfterOptions(sourceFile, after))
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertNodesAfter(sourceFile *ast.SourceFile, after *ast.Node, newNodes []*ast.Node) {
|
||||
endPosition := t.endPosForInsertNodeAfter(sourceFile, after, newNodes[0])
|
||||
t.InsertNodesAt(sourceFile, endPosition, newNodes, t.getInsertNodeAfterOptions(sourceFile, after))
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertNodeBefore(sourceFile *ast.SourceFile, before *ast.Node, newNode *ast.Node, blankLineBetween bool, leadingTriviaOption LeadingTriviaOption) {
|
||||
t.InsertNodeAt(sourceFile, core.TextPos(t.getAdjustedStartPosition(sourceFile, before, leadingTriviaOption, false)), newNode, t.getOptionsForInsertNodeBefore(before, newNode, blankLineBetween))
|
||||
}
|
||||
|
||||
// TryInsertTypeAnnotation inserts a type annotation after the appropriate position on a node
|
||||
// (after the close paren for function-like, after the name/exclamation/question for variable-like).
|
||||
// Returns true if successful.
|
||||
func (t *Tracker) TryInsertTypeAnnotation(sourceFile *ast.SourceFile, node *ast.Node, typeNode *ast.Node) bool {
|
||||
var endNode *ast.Node
|
||||
if ast.IsFunctionLike(node) {
|
||||
endNode = astnav.FindChildOfKind(node, ast.KindCloseParenToken, sourceFile)
|
||||
if endNode == nil {
|
||||
if !ast.IsArrowFunction(node) {
|
||||
return false
|
||||
}
|
||||
// If no `)`, is an arrow function `x => x`, so use the end of the first parameter
|
||||
params := node.Parameters()
|
||||
if len(params) == 0 {
|
||||
return false
|
||||
}
|
||||
endNode = params[0]
|
||||
}
|
||||
} else {
|
||||
switch node.Kind {
|
||||
case ast.KindVariableDeclaration:
|
||||
endNode = node.AsVariableDeclaration().ExclamationToken
|
||||
case ast.KindPropertySignature:
|
||||
endNode = node.AsPropertySignatureDeclaration().PostfixToken
|
||||
case ast.KindPropertyDeclaration:
|
||||
endNode = node.AsPropertyDeclaration().PostfixToken
|
||||
case ast.KindParameter:
|
||||
endNode = node.AsParameterDeclaration().QuestionToken
|
||||
}
|
||||
if endNode == nil {
|
||||
endNode = node.Name()
|
||||
}
|
||||
}
|
||||
if endNode == nil {
|
||||
return false
|
||||
}
|
||||
t.InsertNodeAt(sourceFile, core.TextPos(endNode.End()), typeNode, NodeOptions{Prefix: ": "})
|
||||
return true
|
||||
}
|
||||
|
||||
// ParenthesizeArrowParameters wraps the parameters of a paren-less arrow function in `(` and `)`.
|
||||
// This is a no-op if the arrow function already has parens.
|
||||
func (t *Tracker) ParenthesizeArrowParameters(sourceFile *ast.SourceFile, arrowFunc *ast.Node) {
|
||||
if astnav.FindChildOfKind(arrowFunc, ast.KindCloseParenToken, sourceFile) != nil {
|
||||
return
|
||||
}
|
||||
params := arrowFunc.Parameters()
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
firstParam := params[0]
|
||||
lastParam := params[len(params)-1]
|
||||
startPos := astnav.GetStartOfNode(firstParam, sourceFile, false)
|
||||
t.InsertText(sourceFile, t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPos)), "(")
|
||||
t.InsertText(sourceFile, t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(lastParam.End())), ")")
|
||||
}
|
||||
|
||||
// InsertModifierBefore inserts a modifier token (like 'type') before a node with a trailing space.
|
||||
func (t *Tracker) InsertModifierBefore(sourceFile *ast.SourceFile, modifier ast.Kind, before *ast.Node) {
|
||||
pos := astnav.GetStartOfNode(before, sourceFile, false)
|
||||
token := t.NewToken(modifier)
|
||||
token.Loc = core.NewTextRange(pos, pos)
|
||||
token.Parent = before.Parent
|
||||
t.InsertNodeAt(sourceFile, core.TextPos(pos), token, NodeOptions{Suffix: " "})
|
||||
}
|
||||
|
||||
// Delete queues a node for deletion with smart handling of list items, imports, etc.
|
||||
// The actual deletion happens in finishDeleteDeclarations during GetChanges.
|
||||
func (t *Tracker) Delete(sourceFile *ast.SourceFile, node *ast.Node) {
|
||||
t.deletedNodes = append(t.deletedNodes, deletedNode{sourceFile: sourceFile, node: node})
|
||||
}
|
||||
|
||||
// DeleteRange deletes a text range from the source file.
|
||||
func (t *Tracker) DeleteRange(sourceFile *ast.SourceFile, textRange core.TextRange) {
|
||||
lspRange := t.converters.ToLSPRange(sourceFile, textRange)
|
||||
t.ReplaceRangeWithText(sourceFile, lspRange, "")
|
||||
}
|
||||
|
||||
// DeleteNode deletes a node immediately with specified trivia options.
|
||||
// Stop! Consider using Delete instead, which has logic for deleting nodes from delimited lists.
|
||||
func (t *Tracker) DeleteNode(sourceFile *ast.SourceFile, node *ast.Node, leadingTrivia LeadingTriviaOption, trailingTrivia TrailingTriviaOption) {
|
||||
rng := t.GetAdjustedRange(sourceFile, node, node, leadingTrivia, trailingTrivia)
|
||||
t.ReplaceRangeWithText(sourceFile, rng, "")
|
||||
}
|
||||
|
||||
// DeleteNodeRange deletes a range of nodes with specified trivia options.
|
||||
func (t *Tracker) DeleteNodeRange(sourceFile *ast.SourceFile, startNode *ast.Node, endNode *ast.Node, leadingTrivia LeadingTriviaOption, trailingTrivia TrailingTriviaOption) {
|
||||
startPosition := t.getAdjustedStartPosition(sourceFile, startNode, leadingTrivia, false)
|
||||
endPosition := t.getAdjustedEndPosition(sourceFile, endNode, trailingTrivia)
|
||||
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPosition))
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(endPosition))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
|
||||
}
|
||||
|
||||
// finishDeleteDeclarations processes all queued deletions with smart handling for lists and trailing commas.
|
||||
func (t *Tracker) finishDeleteDeclarations() {
|
||||
deletedNodesInLists := make(map[*ast.Node]bool)
|
||||
|
||||
for _, deleted := range t.deletedNodes {
|
||||
// Skip if this node is contained within another deleted node
|
||||
isContained := false
|
||||
for _, other := range t.deletedNodes {
|
||||
if other.sourceFile == deleted.sourceFile && other.node != deleted.node &&
|
||||
rangeContainsRangeExclusive(other.node, deleted.node) {
|
||||
isContained = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isContained {
|
||||
continue
|
||||
}
|
||||
|
||||
deleteDeclaration(t, deletedNodesInLists, deleted.sourceFile, deleted.node)
|
||||
}
|
||||
|
||||
// Handle trailing commas for last elements in lists
|
||||
for node := range deletedNodesInLists {
|
||||
sourceFile := ast.GetSourceFileOfNode(node)
|
||||
list := format.GetContainingList(node, sourceFile)
|
||||
if list == nil || node != list.Nodes[len(list.Nodes)-1] {
|
||||
continue
|
||||
}
|
||||
|
||||
lastNonDeletedIndex := -1
|
||||
for i := len(list.Nodes) - 2; i >= 0; i-- {
|
||||
if !deletedNodesInLists[list.Nodes[i]] {
|
||||
lastNonDeletedIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lastNonDeletedIndex != -1 {
|
||||
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(list.Nodes[lastNonDeletedIndex].End()))
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(t.startPositionToDeleteNodeInList(sourceFile, list.Nodes[lastNonDeletedIndex+1])))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) endPosForInsertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) core.TextPos {
|
||||
if needSemicolonBetween(after, newNode) && (rune(sourceFile.Text()[after.End()-1]) != ';') {
|
||||
// check if previous statement ends with semicolon
|
||||
// if not - insert semicolon to preserve the code from changing the meaning due to ASI
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End()))
|
||||
semicolon := t.NewToken(ast.KindSemicolonToken)
|
||||
semicolon.Loc = core.NewTextRange(after.End(), after.End())
|
||||
semicolon.Parent = after.Parent
|
||||
t.ReplaceRange(
|
||||
sourceFile,
|
||||
lsproto.Range{Start: endPos, End: endPos},
|
||||
semicolon,
|
||||
NodeOptions{},
|
||||
)
|
||||
}
|
||||
return core.TextPos(t.getAdjustedEndPosition(sourceFile, after, TrailingTriviaOptionNone))
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range,
|
||||
* i.e. arguments in arguments lists, parameters in parameter lists etc.
|
||||
* Note that separators are part of the node in statements and class elements.
|
||||
*/
|
||||
func (t *Tracker) InsertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node, containingList *ast.NodeList) {
|
||||
if containingList == nil {
|
||||
containingList = format.GetContainingList(after, sourceFile)
|
||||
}
|
||||
if containingList == nil {
|
||||
// Debug.fail("node is not a list element")
|
||||
return
|
||||
}
|
||||
index := slices.Index(containingList.Nodes, after)
|
||||
if index < 0 {
|
||||
return
|
||||
}
|
||||
end := after.End()
|
||||
if index != len(containingList.Nodes)-1 {
|
||||
// any element except the last one
|
||||
// use next sibling as an anchor
|
||||
if nextToken := astnav.GetTokenAtPosition(sourceFile, after.End()); nextToken != nil && isSeparator(after, nextToken) {
|
||||
// for list
|
||||
// a, b, c
|
||||
// create change for adding 'e' after 'a' as
|
||||
// - find start of next element after a (it is b)
|
||||
// - use next element start as start and end position in final change
|
||||
// - build text of change by formatting the text of node + whitespace trivia of b
|
||||
|
||||
// in multiline case it will work as
|
||||
// a,
|
||||
// b,
|
||||
// c,
|
||||
// result - '*' denotes leading trivia that will be inserted after new text (displayed as '#')
|
||||
// a,
|
||||
// insertedtext<separator>#
|
||||
// ###b,
|
||||
// c,
|
||||
nextNode := containingList.Nodes[index+1]
|
||||
startPos := scanner.SkipTriviaEx(sourceFile.Text(), nextNode.Pos(), &scanner.SkipTriviaOptions{StopAfterLineBreak: false, StopAtComments: true})
|
||||
|
||||
// write separator and leading trivia of the next element as suffix
|
||||
suffix := scanner.TokenToString(nextToken.Kind) + sourceFile.Text()[nextToken.End():startPos]
|
||||
t.InsertNodesAt(sourceFile, core.TextPos(startPos), []*ast.Node{newNode}, NodeOptions{Suffix: suffix})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
afterStart := astnav.GetStartOfNode(after, sourceFile, false)
|
||||
afterStartLinePosition := format.GetLineStartPositionForPosition(afterStart, sourceFile)
|
||||
|
||||
// insert element after the last element in the list that has more than one item
|
||||
// pick the element preceding the after element to:
|
||||
// - pick the separator
|
||||
// - determine if list is a multiline
|
||||
multilineList := false
|
||||
|
||||
// if list has only one element then we'll format is as multiline if node has comment in trailing trivia, or as singleline otherwise
|
||||
// i.e. var x = 1 // this is x
|
||||
// | new element will be inserted at this position
|
||||
separator := ast.KindCommaToken // SyntaxKind.CommaToken | SyntaxKind.SemicolonToken
|
||||
if len(containingList.Nodes) != 1 {
|
||||
// otherwise, if list has more than one element, pick separator from the list
|
||||
tokenBeforeInsertPosition := astnav.FindPrecedingToken(sourceFile, after.Pos())
|
||||
separator = core.IfElse(isSeparator(after, tokenBeforeInsertPosition), tokenBeforeInsertPosition.Kind, ast.KindCommaToken)
|
||||
// determine if list is multiline by checking lines of after element and element that precedes it.
|
||||
afterMinusOneStartLinePosition := format.GetLineStartPositionForPosition(astnav.GetStartOfNode(containingList.Nodes[index-1], sourceFile, false), sourceFile)
|
||||
multilineList = afterMinusOneStartLinePosition != afterStartLinePosition
|
||||
}
|
||||
if hasCommentsBeforeLineBreak(sourceFile.Text(), after.End()) || !positionsAreOnSameLine(containingList.Pos(), containingList.End(), sourceFile) {
|
||||
// in this case we'll always treat containing list as multiline
|
||||
multilineList = true
|
||||
}
|
||||
if multilineList {
|
||||
// insert separator immediately following the 'after' node to preserve comments in trailing trivia
|
||||
separatorToken := t.NewToken(separator)
|
||||
separatorString := scanner.TokenToString(separator)
|
||||
separatorToken.Loc = core.NewTextRange(end, end+len(separatorString))
|
||||
separatorToken.Parent = after.Parent
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(end))
|
||||
t.ReplaceRange(sourceFile, lsproto.Range{Start: endPos, End: endPos}, separatorToken, NodeOptions{})
|
||||
// use the same indentation as 'after' item
|
||||
indentation := format.FindFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, t.formatSettings)
|
||||
// insert element before the line break on the line that contains 'after' element
|
||||
insertPos := scanner.SkipTriviaEx(sourceFile.Text(), end, &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: false})
|
||||
// find position before "\n" or "\r\n"
|
||||
for insertPos != end && stringutil.IsLineBreak(rune(sourceFile.Text()[insertPos-1])) {
|
||||
insertPos--
|
||||
}
|
||||
insertLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(insertPos))
|
||||
t.ReplaceRange(
|
||||
sourceFile,
|
||||
lsproto.Range{Start: insertLSPos, End: insertLSPos},
|
||||
newNode,
|
||||
NodeOptions{
|
||||
indentation: &indentation,
|
||||
Prefix: t.newLine,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
separatorString := scanner.TokenToString(separator)
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(end))
|
||||
t.ReplaceRange(sourceFile, lsproto.Range{Start: endPos, End: endPos}, newNode, NodeOptions{Prefix: separatorString + " "})
|
||||
}
|
||||
}
|
||||
|
||||
// InsertImportSpecifierAtIndex inserts a new import specifier at the specified index in a NamedImports list
|
||||
func (t *Tracker) InsertImportSpecifierAtIndex(sourceFile *ast.SourceFile, newSpecifier *ast.Node, namedImports *ast.Node, index int) {
|
||||
namedImportsNode := namedImports.AsNamedImports()
|
||||
elements := namedImportsNode.Elements.Nodes
|
||||
|
||||
var prevSpecifier *ast.Node
|
||||
if index > 0 && index-1 < len(elements) {
|
||||
prevSpecifier = elements[index-1]
|
||||
}
|
||||
if prevSpecifier != nil {
|
||||
t.InsertNodeInListAfter(sourceFile, prevSpecifier, newSpecifier, nil)
|
||||
} else {
|
||||
t.InsertNodeBefore(
|
||||
sourceFile,
|
||||
elements[0],
|
||||
newSpecifier,
|
||||
!positionsAreOnSameLine(astnav.GetStartOfNode(elements[0], sourceFile, false), astnav.GetStartOfNode(namedImports.Parent.Parent, sourceFile, false), sourceFile),
|
||||
LeadingTriviaOptionNone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertAtTopOfFile(sourceFile *ast.SourceFile, insert []*ast.Statement, blankLineBetween bool) {
|
||||
if len(insert) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
pos := t.getInsertionPositionAtSourceFileTop(sourceFile)
|
||||
options := NodeOptions{}
|
||||
if pos != 0 {
|
||||
options.Prefix = t.newLine
|
||||
}
|
||||
if len(sourceFile.Text()) == 0 || !stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) {
|
||||
options.Suffix = t.newLine
|
||||
}
|
||||
if blankLineBetween {
|
||||
options.Suffix += t.newLine
|
||||
}
|
||||
|
||||
if len(insert) == 1 {
|
||||
t.InsertNodeAt(sourceFile, core.TextPos(pos), insert[0], options)
|
||||
} else {
|
||||
t.InsertNodesAt(sourceFile, core.TextPos(pos), insert, options)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertMemberAtStart(sourceFile *ast.SourceFile, node *ast.Node, newElement *ast.Node) {
|
||||
t.insertNodeAtStartWorker(sourceFile, node, newElement)
|
||||
}
|
||||
|
||||
func (t *Tracker) insertNodeAtStartWorker(sourceFile *ast.SourceFile, node *ast.Node, newElement *ast.Node) {
|
||||
indentation := t.tryComputeIndentationFromExistingMembers(sourceFile, node)
|
||||
if indentation < 0 {
|
||||
indentation = t.tryComputeIndentationForNewMember(sourceFile, node)
|
||||
}
|
||||
|
||||
members := getMembersOrProperties(node)
|
||||
if members == nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.InsertNodeAt(sourceFile, core.TextPos(members.Pos()), newElement, t.getInsertNodeAtStartInsertOptions(sourceFile, node, indentation))
|
||||
}
|
||||
|
||||
func (t *Tracker) tryComputeIndentationForNewMember(sourceFile *ast.SourceFile, node *ast.Node) int {
|
||||
nodeStart := astnav.GetStartOfNode(node, sourceFile, false)
|
||||
lineStart := format.GetLineStartPositionForPosition(nodeStart, sourceFile)
|
||||
|
||||
tabSize := t.formatSettings.TabSize
|
||||
if tabSize <= 0 {
|
||||
tabSize = 4
|
||||
}
|
||||
|
||||
indentSize := t.formatSettings.IndentSize
|
||||
if indentSize <= 0 {
|
||||
indentSize = 4
|
||||
}
|
||||
return max(findIndentationColumn(sourceFile.Text(), lineStart, nodeStart, tabSize), 0) + indentSize
|
||||
}
|
||||
|
||||
func (t *Tracker) tryComputeIndentationFromExistingMembers(sourceFile *ast.SourceFile, node *ast.Node) int {
|
||||
members := getMembersOrProperties(node)
|
||||
if members == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
indentation := -1
|
||||
text := sourceFile.Text()
|
||||
tabSize := t.formatSettings.TabSize
|
||||
last := node
|
||||
|
||||
if tabSize <= 0 {
|
||||
tabSize = 4
|
||||
}
|
||||
|
||||
for _, member := range members.Nodes {
|
||||
if member == nil {
|
||||
continue
|
||||
}
|
||||
if printer.RangeStartPositionsAreOnSameLine(last.Loc, member.Loc, sourceFile) {
|
||||
return -1
|
||||
}
|
||||
|
||||
memberStart := astnav.GetStartOfNode(member, sourceFile, false)
|
||||
lineStart := format.GetLineStartPositionForPosition(memberStart, sourceFile)
|
||||
column := findIndentationColumn(text, lineStart, memberStart, tabSize)
|
||||
if column < 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
if indentation >= 0 {
|
||||
if indentation != column {
|
||||
return -1
|
||||
}
|
||||
last = member
|
||||
continue
|
||||
}
|
||||
|
||||
indentation = column
|
||||
last = member
|
||||
}
|
||||
|
||||
return indentation
|
||||
}
|
||||
|
||||
func (t *Tracker) getInsertNodeAfterOptions(sourceFile *ast.SourceFile, node *ast.Node) NodeOptions {
|
||||
newLineChar := t.newLine
|
||||
var options NodeOptions
|
||||
switch node.Kind {
|
||||
case ast.KindParameter:
|
||||
// default opts
|
||||
options = NodeOptions{}
|
||||
case ast.KindClassDeclaration, ast.KindModuleDeclaration:
|
||||
options = NodeOptions{Prefix: newLineChar, Suffix: newLineChar}
|
||||
|
||||
case ast.KindVariableDeclaration, ast.KindStringLiteral, ast.KindIdentifier:
|
||||
options = NodeOptions{Prefix: ", "}
|
||||
|
||||
case ast.KindPropertyAssignment:
|
||||
options = NodeOptions{Suffix: "," + newLineChar}
|
||||
|
||||
case ast.KindExportKeyword:
|
||||
options = NodeOptions{Prefix: " "}
|
||||
|
||||
default:
|
||||
if !(ast.IsStatement(node) || ast.IsClassOrTypeElement(node)) {
|
||||
// Else we haven't handled this kind of node yet -- add it
|
||||
panic("unimplemented node type " + node.Kind.String() + " in changeTracker.getInsertNodeAfterOptions")
|
||||
}
|
||||
options = NodeOptions{Suffix: newLineChar}
|
||||
}
|
||||
if node.End() == sourceFile.End() && ast.IsStatement(node) {
|
||||
options.Prefix = t.newLine + options.Prefix
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func (t *Tracker) getOptionsForInsertNodeBefore(before *ast.Node, inserted *ast.Node, blankLineBetween bool) NodeOptions {
|
||||
if ast.IsStatement(before) || ast.IsClassOrTypeElement(before) {
|
||||
if blankLineBetween {
|
||||
return NodeOptions{Suffix: t.newLine + t.newLine}
|
||||
}
|
||||
return NodeOptions{Suffix: t.newLine}
|
||||
} else if before.Kind == ast.KindVariableDeclaration {
|
||||
// insert `x = 1, ` into `const x = 1, y = 2;
|
||||
return NodeOptions{Suffix: ", "}
|
||||
} else if before.Kind == ast.KindParameter {
|
||||
if inserted.Kind == ast.KindParameter {
|
||||
return NodeOptions{Suffix: ", "}
|
||||
}
|
||||
return NodeOptions{}
|
||||
} else if (before.Kind == ast.KindStringLiteral && before.Parent != nil && before.Parent.Kind == ast.KindImportDeclaration) || before.Kind == ast.KindNamedImports {
|
||||
return NodeOptions{Suffix: ", "}
|
||||
} else if before.Kind == ast.KindImportSpecifier {
|
||||
suffix := ","
|
||||
if blankLineBetween {
|
||||
suffix += t.newLine
|
||||
} else {
|
||||
suffix += " "
|
||||
}
|
||||
return NodeOptions{Suffix: suffix}
|
||||
}
|
||||
// We haven't handled this kind of node yet -- add it
|
||||
panic("unimplemented node type " + before.Kind.String() + " in changeTracker.getOptionsForInsertNodeBefore")
|
||||
}
|
||||
|
||||
func (t *Tracker) getInsertNodeAtStartInsertOptions(sourceFile *ast.SourceFile, node *ast.Node, indentation int) NodeOptions {
|
||||
state := t.nodesWithInsertionsAtStart[node]
|
||||
hasPreviousInsertion := state != nil
|
||||
if state == nil {
|
||||
state = &nodesInsertedAtStartState{
|
||||
node: node,
|
||||
sourceFile: sourceFile,
|
||||
}
|
||||
t.nodesWithInsertionsAtStart[node] = state
|
||||
}
|
||||
|
||||
members := getMembersOrProperties(node)
|
||||
isObjectLiteral := ast.IsObjectLiteralExpression(node)
|
||||
isJSON := ast.IsJsonSourceFile(sourceFile)
|
||||
|
||||
hasMembers := members != nil && len(members.Nodes) > 0
|
||||
|
||||
insertTrailingComma := isObjectLiteral && (hasMembers || !isJSON)
|
||||
insertLeadingComma := isObjectLiteral && isJSON && !hasMembers && hasPreviousInsertion
|
||||
|
||||
suffix := ""
|
||||
if insertTrailingComma {
|
||||
suffix = ","
|
||||
} else if ast.IsInterfaceDeclaration(node) && !hasMembers {
|
||||
suffix = ";"
|
||||
}
|
||||
|
||||
prefix := t.newLine
|
||||
if insertLeadingComma {
|
||||
prefix = "," + prefix
|
||||
}
|
||||
|
||||
return NodeOptions{indentation: &indentation, Prefix: prefix, Suffix: suffix}
|
||||
}
|
||||
|
||||
func (t *Tracker) finishNodesWithInsertionsAtStart() {
|
||||
for _, state := range t.nodesWithInsertionsAtStart {
|
||||
if state == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
openBrace := astnav.FindChildOfKind(state.node, ast.KindOpenBraceToken, state.sourceFile)
|
||||
if openBrace == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
closeBrace := astnav.FindChildOfKind(state.node, ast.KindCloseBraceToken, state.sourceFile)
|
||||
if closeBrace == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
members := getMembersOrProperties(state.node)
|
||||
isEmpty := members == nil || len(members.Nodes) == 0
|
||||
isSingleLine := positionsAreOnSameLine(openBrace.End(), closeBrace.End(), state.sourceFile)
|
||||
|
||||
if isEmpty && isSingleLine && openBrace.End() != closeBrace.End()-1 {
|
||||
t.DeleteRange(state.sourceFile, core.NewTextRange(openBrace.End(), closeBrace.End()-1))
|
||||
}
|
||||
|
||||
if isSingleLine {
|
||||
t.InsertText(state.sourceFile, t.converters.PositionToLineAndCharacter(state.sourceFile, core.TextPos(closeBrace.End()-1)), t.newLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getMembersOrProperties(node *ast.Node) *ast.NodeList {
|
||||
if ast.IsObjectLiteralExpression(node) {
|
||||
return node.PropertyList()
|
||||
}
|
||||
return node.MemberList()
|
||||
}
|
||||
|
||||
func rangeContainsRangeExclusive(outer *ast.Node, inner *ast.Node) bool {
|
||||
return outer.Pos() < inner.Pos() && inner.End() < outer.End()
|
||||
}
|
||||
|
||||
func isSeparator(node *ast.Node, candidate *ast.Node) bool {
|
||||
return candidate != nil && node.Parent != nil && (candidate.Kind == ast.KindCommaToken || (candidate.Kind == ast.KindSemicolonToken && node.Parent.Kind == ast.KindObjectLiteralExpression))
|
||||
}
|
||||
|
||||
func findIndentationColumn(text string, lineStart, memberStart, tabSize int) int {
|
||||
column := 0
|
||||
|
||||
for i := lineStart; i < memberStart && i < len(text); i++ {
|
||||
ch := rune(text[i])
|
||||
|
||||
if stringutil.IsLineBreak(ch) {
|
||||
return -1
|
||||
}
|
||||
if stringutil.IsWhiteSpaceSingleLine(ch) {
|
||||
column = advanceIndentationColumn(column, ch, tabSize)
|
||||
continue
|
||||
}
|
||||
return column
|
||||
}
|
||||
|
||||
return column
|
||||
}
|
||||
|
||||
func advanceIndentationColumn(column int, ch rune, tabSize int) int {
|
||||
if ch == '\t' {
|
||||
return column + tabSize - (column % tabSize)
|
||||
}
|
||||
return column + 1
|
||||
}
|
||||
402
tools/tsgo/internal/ls/change/trackerimpl.go
Normal file
402
tools/tsgo/internal/ls/change/trackerimpl.go
Normal file
@@ -0,0 +1,402 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"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/format"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
func (t *Tracker) getTextChangesFromChanges() map[string][]*lsproto.TextEdit {
|
||||
changes := map[string][]*lsproto.TextEdit{}
|
||||
for sourceFile, changesInFile := range t.changes.M {
|
||||
// order changes by start position
|
||||
// If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa.
|
||||
slices.SortStableFunc(changesInFile, func(a, b *trackerEdit) int { return lsproto.CompareRanges(a.Range, b.Range) })
|
||||
// verify that change intervals do not overlap, except possibly at end points.
|
||||
for i := range len(changesInFile) - 1 {
|
||||
if lsproto.ComparePositions(changesInFile[i].Range.End, changesInFile[i+1].Range.Start) > 0 {
|
||||
// assert change[i].End <= change[i + 1].Start
|
||||
panic(fmt.Sprintf("changes overlap: %v and %v", changesInFile[i].Range, changesInFile[i+1].Range))
|
||||
}
|
||||
}
|
||||
|
||||
textChanges := core.MapNonNil(changesInFile, func(change *trackerEdit) *lsproto.TextEdit {
|
||||
// !!! targetSourceFile
|
||||
|
||||
newText := t.computeNewText(change, sourceFile, sourceFile)
|
||||
// span := createTextSpanFromRange(c.Range)
|
||||
// !!!
|
||||
// Filter out redundant changes.
|
||||
// if (span.length == newText.length && stringContainsAt(targetSourceFile.text, newText, span.start)) { return nil }
|
||||
|
||||
return &lsproto.TextEdit{
|
||||
NewText: newText,
|
||||
Range: change.Range,
|
||||
}
|
||||
})
|
||||
|
||||
if len(textChanges) > 0 {
|
||||
changes[sourceFile.FileName()] = textChanges
|
||||
}
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func (t *Tracker) computeNewText(change *trackerEdit, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile) string {
|
||||
switch change.kind {
|
||||
case trackerEditKindRemove:
|
||||
return ""
|
||||
case trackerEditKindText:
|
||||
return change.NewText
|
||||
}
|
||||
|
||||
pos := int(t.converters.LineAndCharacterToPosition(sourceFile, change.Range.Start))
|
||||
formatNode := func(n *ast.Node) string {
|
||||
return t.getFormattedTextOfNode(n, targetSourceFile, sourceFile, pos, change.options)
|
||||
}
|
||||
|
||||
var text string
|
||||
switch change.kind {
|
||||
|
||||
case trackerEditKindReplaceWithMultipleNodes:
|
||||
if change.options.joiner == "" {
|
||||
change.options.joiner = t.newLine
|
||||
}
|
||||
text = strings.Join(core.Map(change.nodes, func(n *ast.Node) string { return strings.TrimSuffix(formatNode(n), t.newLine) }), change.options.joiner)
|
||||
case trackerEditKindReplaceWithSingleNode:
|
||||
text = formatNode(change.Node)
|
||||
default:
|
||||
panic(fmt.Sprintf("change kind %d should have been handled earlier", change.kind))
|
||||
}
|
||||
// strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line
|
||||
noIndent := text
|
||||
if !(change.options.indentation != nil || format.GetLineStartPositionForPosition(pos, targetSourceFile) == pos) {
|
||||
noIndent = strings.TrimLeftFunc(text, unicode.IsSpace)
|
||||
}
|
||||
return change.options.Prefix + noIndent + core.IfElse(strings.HasSuffix(noIndent, change.options.Suffix), "", change.options.Suffix)
|
||||
}
|
||||
|
||||
/** Note: this may mutate `nodeIn`. */
|
||||
func (t *Tracker) getFormattedTextOfNode(nodeIn *ast.Node, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile, pos int, options NodeOptions) string {
|
||||
text, sourceFileLike := t.getNonformattedText(nodeIn, targetSourceFile)
|
||||
// !!! if (validate) validate(node, text);
|
||||
formatOptions := getFormatCodeSettingsForWriting(t.formatSettings, targetSourceFile)
|
||||
|
||||
var initialIndentation, delta int
|
||||
if options.indentation == nil {
|
||||
initialIndentation = format.GetIndentation(pos, sourceFile, formatOptions, options.Prefix == t.newLine || format.GetLineStartPositionForPosition(pos, targetSourceFile) == pos)
|
||||
} else {
|
||||
initialIndentation = *options.indentation
|
||||
}
|
||||
|
||||
if options.delta != nil {
|
||||
delta = *options.delta
|
||||
} else if formatOptions.IndentSize != 0 && format.ShouldIndentChildNode(formatOptions, nodeIn, nil, nil) {
|
||||
delta = formatOptions.IndentSize
|
||||
}
|
||||
|
||||
changes := format.FormatNodeGivenIndentation(t.ctx, sourceFileLike, sourceFileLike.AsSourceFile(), targetSourceFile.LanguageVariant, initialIndentation, delta)
|
||||
return core.ApplyBulkEdits(text, changes)
|
||||
}
|
||||
|
||||
func getFormatCodeSettingsForWriting(options lsutil.FormatCodeSettings, sourceFile *ast.SourceFile) lsutil.FormatCodeSettings {
|
||||
shouldAutoDetectSemicolonPreference := options.Semicolons == lsutil.SemicolonPreferenceIgnore
|
||||
shouldRemoveSemicolons := options.Semicolons == lsutil.SemicolonPreferenceRemove || shouldAutoDetectSemicolonPreference && !lsutil.ProbablyUsesSemicolons(sourceFile)
|
||||
if shouldRemoveSemicolons {
|
||||
options.Semicolons = lsutil.SemicolonPreferenceRemove
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func (t *Tracker) getNonformattedText(node *ast.Node, sourceFile *ast.SourceFile) (string, *ast.Node) {
|
||||
writer := printer.NewChangeTrackerWriter(t.newLine, t.formatSettings.IndentSize)
|
||||
printer.NewPrinter(
|
||||
printer.PrinterOptions{
|
||||
NewLine: core.GetNewLineKind(t.newLine),
|
||||
NeverAsciiEscape: true,
|
||||
PreserveSourceNewlines: true,
|
||||
TerminateUnterminatedLiterals: true,
|
||||
},
|
||||
writer.GetPrintHandlers(),
|
||||
t.EmitContext,
|
||||
).Write(node, sourceFile, writer, nil)
|
||||
|
||||
text := writer.String()
|
||||
text = strings.TrimSuffix(text, t.newLine)
|
||||
|
||||
nodeOut := writer.AssignPositionsToNode(node, t.NodeFactory)
|
||||
eofToken := t.Factory.NewToken(ast.KindEndOfFile)
|
||||
nodeList := t.Factory.NewNodeList([]*ast.Node{nodeOut})
|
||||
nodeList.Loc = nodeOut.Loc
|
||||
eofToken.Loc = core.NewTextRange(nodeOut.End(), nodeOut.End())
|
||||
sourceFileLike := t.Factory.NewSourceFile(
|
||||
ast.SourceFileParseOptions{FileName: sourceFile.FileName(), Path: sourceFile.Path()},
|
||||
text,
|
||||
nodeList,
|
||||
eofToken,
|
||||
)
|
||||
sourceFileLike.ForEachChild(func(child *ast.Node) bool {
|
||||
child.Parent = sourceFileLike
|
||||
return true
|
||||
})
|
||||
sourceFileLike.Loc = nodeOut.Loc
|
||||
return text, sourceFileLike
|
||||
}
|
||||
|
||||
// method on the changeTracker because use of converters
|
||||
// GetAdjustedRange computes the adjusted range for a node in a source file, accounting for trivia.
|
||||
func (t *Tracker) GetAdjustedRange(sourceFile *ast.SourceFile, startNode *ast.Node, endNode *ast.Node, leadingOption LeadingTriviaOption, trailingOption TrailingTriviaOption) lsproto.Range {
|
||||
return t.converters.ToLSPRange(
|
||||
sourceFile,
|
||||
core.NewTextRange(
|
||||
t.getAdjustedStartPosition(sourceFile, startNode, leadingOption, false),
|
||||
t.getAdjustedEndPosition(sourceFile, endNode, trailingOption),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// method on the changeTracker because use of converters
|
||||
func (t *Tracker) getAdjustedStartPosition(sourceFile *ast.SourceFile, node *ast.Node, leadingOption LeadingTriviaOption, hasTrailingComment bool) int {
|
||||
if leadingOption == LeadingTriviaOptionJSDoc {
|
||||
if JSDocComments := parser.GetJSDocCommentRanges(t.NodeFactory, nil, node, sourceFile.Text()); len(JSDocComments) > 0 {
|
||||
return format.GetLineStartPositionForPosition(JSDocComments[0].Pos(), sourceFile)
|
||||
}
|
||||
}
|
||||
|
||||
start := astnav.GetStartOfNode(node, sourceFile, false)
|
||||
startOfLinePos := format.GetLineStartPositionForPosition(start, sourceFile)
|
||||
|
||||
switch leadingOption {
|
||||
case LeadingTriviaOptionExclude:
|
||||
return start
|
||||
case LeadingTriviaOptionStartLine:
|
||||
if node.Loc.ContainsInclusive(startOfLinePos) {
|
||||
return startOfLinePos
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
fullStart := node.Pos()
|
||||
if fullStart == start {
|
||||
return start
|
||||
}
|
||||
lineStarts := sourceFile.ECMALineMap()
|
||||
fullStartLineIndex := scanner.ComputeLineOfPosition(lineStarts, fullStart)
|
||||
fullStartLinePos := int(lineStarts[fullStartLineIndex])
|
||||
if startOfLinePos == fullStartLinePos {
|
||||
// full start and start of the node are on the same line
|
||||
// a, b;
|
||||
// ^ ^
|
||||
// | start
|
||||
// fullstart
|
||||
// when b is replaced - we usually want to keep the leading trvia
|
||||
// when b is deleted - we delete it
|
||||
if leadingOption == LeadingTriviaOptionIncludeAll {
|
||||
return fullStart
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
// if node has a trailing comments, use comment end position as the text has already been included.
|
||||
if hasTrailingComment {
|
||||
// Check first for leading comments as if the node is the first import, we want to exclude the trivia;
|
||||
// otherwise we get the trailing comments.
|
||||
comments := slices.Collect(scanner.GetLeadingCommentRanges(t.NodeFactory, sourceFile.Text(), fullStart))
|
||||
if len(comments) == 0 {
|
||||
comments = slices.Collect(scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), fullStart))
|
||||
}
|
||||
if len(comments) > 0 {
|
||||
return scanner.SkipTriviaEx(sourceFile.Text(), comments[0].End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true})
|
||||
}
|
||||
}
|
||||
|
||||
// get start position of the line following the line that contains fullstart position
|
||||
// (but only if the fullstart isn't the very beginning of the file)
|
||||
nextLineStart := core.IfElse(fullStart > 0, 1, 0)
|
||||
adjustedStartPosition := int(lineStarts[fullStartLineIndex+nextLineStart])
|
||||
// skip whitespaces/newlines
|
||||
adjustedStartPosition = scanner.SkipTriviaEx(sourceFile.Text(), adjustedStartPosition, &scanner.SkipTriviaOptions{StopAtComments: true})
|
||||
return int(lineStarts[scanner.ComputeLineOfPosition(lineStarts, adjustedStartPosition)])
|
||||
}
|
||||
|
||||
// method on the changeTracker because of converters
|
||||
// Return the end position of a multiline comment of it is on another line; otherwise returns `undefined`;
|
||||
func (t *Tracker) getEndPositionOfMultilineTrailingComment(sourceFile *ast.SourceFile, node *ast.Node, trailingOpt TrailingTriviaOption) int {
|
||||
if trailingOpt == TrailingTriviaOptionInclude {
|
||||
// If the trailing comment is a multiline comment that extends to the next lines,
|
||||
// return the end of the comment and track it for the next nodes to adjust.
|
||||
lineStarts := sourceFile.ECMALineMap()
|
||||
nodeEndLine := scanner.ComputeLineOfPosition(lineStarts, node.End())
|
||||
for comment := range scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End()) {
|
||||
// Single line can break the loop as trivia will only be this line.
|
||||
// Comments on subsequent lines are also ignored.
|
||||
if comment.Kind == ast.KindSingleLineCommentTrivia || scanner.ComputeLineOfPosition(lineStarts, comment.Pos()) > nodeEndLine {
|
||||
break
|
||||
}
|
||||
|
||||
// Get the end line of the comment and compare against the end line of the node.
|
||||
// If the comment end line position and the multiline comment extends to multiple lines,
|
||||
// then is safe to return the end position.
|
||||
if commentEndLine := scanner.ComputeLineOfPosition(lineStarts, comment.End()); commentEndLine > nodeEndLine {
|
||||
return scanner.SkipTriviaEx(sourceFile.Text(), comment.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// method on the changeTracker because of converters
|
||||
func (t *Tracker) getAdjustedEndPosition(sourceFile *ast.SourceFile, node *ast.Node, TrailingTriviaOption TrailingTriviaOption) int {
|
||||
if TrailingTriviaOption == TrailingTriviaOptionExclude {
|
||||
return node.End()
|
||||
}
|
||||
if TrailingTriviaOption == TrailingTriviaOptionExcludeWhitespace {
|
||||
if comments := slices.AppendSeq(
|
||||
slices.Collect(scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End())),
|
||||
scanner.GetLeadingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End()),
|
||||
); len(comments) > 0 {
|
||||
if realEnd := comments[len(comments)-1].End(); realEnd != 0 {
|
||||
return realEnd
|
||||
}
|
||||
}
|
||||
return node.End()
|
||||
}
|
||||
|
||||
if multilineEndPosition := t.getEndPositionOfMultilineTrailingComment(sourceFile, node, TrailingTriviaOption); multilineEndPosition != 0 {
|
||||
return multilineEndPosition
|
||||
}
|
||||
|
||||
newEnd := scanner.SkipTriviaEx(sourceFile.Text(), node.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true})
|
||||
|
||||
if newEnd != node.End() && (TrailingTriviaOption == TrailingTriviaOptionInclude || stringutil.IsLineBreak(rune(sourceFile.Text()[newEnd-1]))) {
|
||||
return newEnd
|
||||
}
|
||||
return node.End()
|
||||
}
|
||||
|
||||
// ============= utilities =============
|
||||
|
||||
func hasCommentsBeforeLineBreak(text string, start int) bool {
|
||||
for _, ch := range []rune(text[start:]) {
|
||||
if !stringutil.IsWhiteSpaceSingleLine(ch) {
|
||||
return ch == '/'
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func needSemicolonBetween(a, b *ast.Node) bool {
|
||||
return (ast.IsPropertySignatureDeclaration(a) || ast.IsPropertyDeclaration(a)) &&
|
||||
ast.IsClassOrTypeElement(b) &&
|
||||
b.Name().Kind == ast.KindComputedPropertyName ||
|
||||
ast.IsStatementButNotDeclaration(a) &&
|
||||
ast.IsStatementButNotDeclaration(b) // TODO: only if b would start with a `(` or `[`
|
||||
}
|
||||
|
||||
func (t *Tracker) getInsertionPositionAtSourceFileTop(sourceFile *ast.SourceFile) int {
|
||||
var lastPrologue *ast.Node
|
||||
for _, node := range sourceFile.Statements.Nodes {
|
||||
if ast.IsPrologueDirective(node) {
|
||||
lastPrologue = node
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
position := 0
|
||||
text := sourceFile.Text()
|
||||
advancePastLineBreak := func() {
|
||||
if position >= len(text) {
|
||||
return
|
||||
}
|
||||
if char := rune(text[position]); stringutil.IsLineBreak(char) {
|
||||
position++
|
||||
if position < len(text) && char == '\r' && rune(text[position]) == '\n' {
|
||||
position++
|
||||
}
|
||||
}
|
||||
}
|
||||
if lastPrologue != nil {
|
||||
position = lastPrologue.End()
|
||||
advancePastLineBreak()
|
||||
return position
|
||||
}
|
||||
|
||||
shebang := scanner.GetShebang(text)
|
||||
if shebang != "" {
|
||||
position = len(shebang)
|
||||
advancePastLineBreak()
|
||||
}
|
||||
|
||||
ranges := slices.Collect(scanner.GetLeadingCommentRanges(t.NodeFactory, text, position))
|
||||
if len(ranges) == 0 {
|
||||
return position
|
||||
}
|
||||
// Find the first attached comment to the first node and add before it
|
||||
var lastComment *ast.CommentRange
|
||||
pinnedOrTripleSlash := false
|
||||
firstNodeLine := -1
|
||||
|
||||
lenStatements := len(sourceFile.Statements.Nodes)
|
||||
lineMap := sourceFile.ECMALineMap()
|
||||
for _, r := range ranges {
|
||||
if r.Kind == ast.KindMultiLineCommentTrivia {
|
||||
if printer.IsPinnedComment(text, r) {
|
||||
lastComment = &r
|
||||
pinnedOrTripleSlash = true
|
||||
continue
|
||||
}
|
||||
} else if printer.IsRecognizedTripleSlashComment(text, r) {
|
||||
lastComment = &r
|
||||
pinnedOrTripleSlash = true
|
||||
continue
|
||||
}
|
||||
|
||||
if lastComment != nil {
|
||||
// Always insert after pinned or triple slash comments
|
||||
if pinnedOrTripleSlash {
|
||||
break
|
||||
}
|
||||
|
||||
// There was a blank line between the last comment and this comment.
|
||||
// This comment is not part of the copyright comments
|
||||
commentLine := scanner.ComputeLineOfPosition(lineMap, r.Pos())
|
||||
lastCommentEndLine := scanner.ComputeLineOfPosition(lineMap, lastComment.End())
|
||||
if commentLine >= lastCommentEndLine+2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lenStatements > 0 {
|
||||
if firstNodeLine == -1 {
|
||||
firstNodeLine = scanner.ComputeLineOfPosition(lineMap, astnav.GetStartOfNode(sourceFile.Statements.Nodes[0], sourceFile, false))
|
||||
}
|
||||
commentEndLine := scanner.ComputeLineOfPosition(lineMap, r.End())
|
||||
if firstNodeLine < commentEndLine+2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
lastComment = &r
|
||||
pinnedOrTripleSlash = false
|
||||
}
|
||||
|
||||
if lastComment != nil {
|
||||
position = lastComment.End()
|
||||
advancePastLineBreak()
|
||||
}
|
||||
return position
|
||||
}
|
||||
Reference in New Issue
Block a user