752 lines
28 KiB
Go
752 lines
28 KiB
Go
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
|
|
}
|