vendor tsgo
This commit is contained in:
382
tools/tsgo/internal/ls/file_rename.go
Normal file
382
tools/tsgo/internal/ls/file_rename.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/change"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/modulespecifiers"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/tsoptions"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
type pathUpdater func(path string) (string, bool)
|
||||
|
||||
type toImport struct {
|
||||
newFileName string
|
||||
updated bool
|
||||
}
|
||||
|
||||
func (l *LanguageService) GetEditsForFileRename(ctx context.Context, oldURI lsproto.DocumentUri, newURI lsproto.DocumentUri) []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile {
|
||||
program := l.GetProgram()
|
||||
oldPath := oldURI.FileName()
|
||||
newPath := newURI.FileName()
|
||||
|
||||
oldToNew := l.createPathUpdater(oldPath, newPath)
|
||||
|
||||
changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters)
|
||||
l.updateTsconfigFiles(program, changeTracker, oldToNew, oldPath, newPath)
|
||||
l.updateImportsForFileRename(program, changeTracker, oldToNew)
|
||||
|
||||
var documentChanges []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
|
||||
|
||||
// When renaming e.g. `foo.d.css.ts` -> `bar.d.css.ts`, also rename `foo.css` -> `bar.css` if it exists.
|
||||
if tspath.IsDeclarationFileName(oldPath) && tspath.IsDeclarationFileName(newPath) {
|
||||
dtsExt := tspath.GetDeclarationFileExtension(oldPath)
|
||||
originalExtensions := tspath.GetPossibleOriginalInputExtensionForExtension(dtsExt)
|
||||
for _, ext := range originalExtensions {
|
||||
oldOriginalPath := tspath.ChangeFullExtension(oldPath, ext)
|
||||
if l.host.FileExists(oldOriginalPath) {
|
||||
newDtsExt := tspath.GetDeclarationFileExtension(oldPath)
|
||||
newOriginalExtensions := tspath.GetPossibleOriginalInputExtensionForExtension(newDtsExt)
|
||||
if slices.Contains(newOriginalExtensions, ext) {
|
||||
newOriginalPath := tspath.ChangeFullExtension(newPath, ext)
|
||||
documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{
|
||||
RenameFile: &lsproto.RenameFile{
|
||||
OldUri: lsconv.FileNameToDocumentURI(oldOriginalPath),
|
||||
NewUri: lsconv.FileNameToDocumentURI(newOriginalPath),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for fileName, edits := range changeTracker.GetChanges() {
|
||||
uri := lsconv.FileNameToDocumentURI(fileName)
|
||||
lspEdits := make([]lsproto.TextEditOrAnnotatedTextEditOrSnippetTextEdit, 0, len(edits))
|
||||
for _, edit := range edits {
|
||||
lspEdits = append(lspEdits, lsproto.TextEditOrAnnotatedTextEditOrSnippetTextEdit{
|
||||
TextEdit: edit,
|
||||
})
|
||||
}
|
||||
documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{
|
||||
TextDocumentEdit: &lsproto.TextDocumentEdit{
|
||||
TextDocument: lsproto.OptionalVersionedTextDocumentIdentifier{Uri: uri},
|
||||
Edits: lspEdits,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return documentChanges
|
||||
}
|
||||
|
||||
func (l *LanguageService) createPathUpdater(oldPath string, newPath string) pathUpdater {
|
||||
compareOptions := tspath.ComparePathsOptions{UseCaseSensitiveFileNames: l.UseCaseSensitiveFileNames()}
|
||||
return func(path string) (string, bool) {
|
||||
if tspath.ComparePaths(path, oldPath, compareOptions) == 0 {
|
||||
return newPath, true
|
||||
}
|
||||
if tspath.StartsWithDirectory(path, oldPath, l.UseCaseSensitiveFileNames()) {
|
||||
return newPath + path[len(oldPath):], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeTracker *change.Tracker, oldToNew pathUpdater, oldPath string, newPath string) {
|
||||
commandLine := program.CommandLine()
|
||||
if commandLine == nil || commandLine.ConfigFile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
configFile := commandLine.ConfigFile.SourceFile
|
||||
if configFile == nil {
|
||||
return
|
||||
}
|
||||
configDir := tspath.GetDirectoryPath(configFile.FileName())
|
||||
jsonObjectLiteral := getTsConfigObjectLiteralExpression(configFile)
|
||||
if jsonObjectLiteral == nil {
|
||||
return
|
||||
}
|
||||
|
||||
forEachObjectProperty(jsonObjectLiteral, func(property *ast.PropertyAssignment, propertyName string) {
|
||||
switch propertyName {
|
||||
case "files", "include", "exclude":
|
||||
foundExactMatch := updatePathsProperty(configFile, configDir, property, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames())
|
||||
if foundExactMatch || propertyName != "include" || !ast.IsArrayLiteralExpression(property.Initializer) {
|
||||
return
|
||||
}
|
||||
if oldSpec, isDefault := commandLine.GetMatchedIncludeSpec(oldPath); oldSpec != "" && !isDefault {
|
||||
if newSpec, _ := commandLine.GetMatchedIncludeSpec(newPath); newSpec == "" {
|
||||
elements := property.Initializer.Elements()
|
||||
if len(elements) > 0 {
|
||||
changeTracker.InsertNodeAfter(
|
||||
configFile,
|
||||
elements[len(elements)-1],
|
||||
changeTracker.NodeFactory.NewStringLiteral(relativePathFromDirectory(configDir, newPath, l.UseCaseSensitiveFileNames()), ast.TokenFlagsNone),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
case "compilerOptions":
|
||||
if !ast.IsObjectLiteralExpression(property.Initializer) {
|
||||
return
|
||||
}
|
||||
forEachObjectProperty(property.Initializer.AsObjectLiteralExpression(), func(property *ast.PropertyAssignment, propertyName string) {
|
||||
option := tsoptions.CommandLineCompilerOptionsMap.Get(propertyName)
|
||||
if option != nil {
|
||||
elementOption := option.Elements()
|
||||
if option.IsFilePath || (option.Kind == tsoptions.CommandLineOptionTypeList && elementOption != nil && elementOption.IsFilePath) {
|
||||
updatePathsProperty(configFile, configDir, property, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if propertyName != "paths" || !ast.IsObjectLiteralExpression(property.Initializer) {
|
||||
return
|
||||
}
|
||||
forEachObjectProperty(property.Initializer.AsObjectLiteralExpression(), func(pathsProperty *ast.PropertyAssignment, _ string) {
|
||||
if !ast.IsArrayLiteralExpression(pathsProperty.Initializer) {
|
||||
return
|
||||
}
|
||||
for _, element := range pathsProperty.Initializer.Elements() {
|
||||
tryUpdateConfigString(configFile, configDir, element, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func updatePathsProperty(configFile *ast.SourceFile, configDir string, property *ast.PropertyAssignment, changeTracker *change.Tracker, oldToNew pathUpdater, converters *lsconv.Converters, useCaseSensitiveFileNames bool) bool {
|
||||
elements := []*ast.Node{property.Initializer}
|
||||
if ast.IsArrayLiteralExpression(property.Initializer) {
|
||||
elements = property.Initializer.Elements()
|
||||
}
|
||||
|
||||
foundExactMatch := false
|
||||
for _, element := range elements {
|
||||
foundExactMatch = tryUpdateConfigString(configFile, configDir, element, changeTracker, oldToNew, converters, useCaseSensitiveFileNames) || foundExactMatch
|
||||
}
|
||||
return foundExactMatch
|
||||
}
|
||||
|
||||
func tryUpdateConfigString(configFile *ast.SourceFile, configDir string, element *ast.Node, changeTracker *change.Tracker, oldToNew pathUpdater, converters *lsconv.Converters, useCaseSensitiveFileNames bool) bool {
|
||||
if !ast.IsStringLiteral(element) {
|
||||
return false
|
||||
}
|
||||
|
||||
elementFileName := tspath.NormalizePath(tspath.CombinePaths(configDir, element.Text()))
|
||||
updated, ok := oldToNew(elementFileName)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
changeTracker.ReplaceRangeWithText(configFile, lsproto.Range{
|
||||
Start: converters.PositionToLineAndCharacter(configFile, core.TextPos(scanner.GetTokenPosOfNode(element, configFile, false)+1)),
|
||||
End: converters.PositionToLineAndCharacter(configFile, core.TextPos(element.End()-1)),
|
||||
}, relativePathFromDirectory(configDir, updated, useCaseSensitiveFileNames))
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *LanguageService) updateRelativePath(oldToNew pathUpdater, oldImportFromPath, newImportFromPath, relativeSpecifier string) string {
|
||||
oldAbsolute := tspath.NormalizePath(tspath.CombinePaths(tspath.GetDirectoryPath(oldImportFromPath), relativeSpecifier))
|
||||
newAbsolute, ok := oldToNew(oldAbsolute)
|
||||
if !ok {
|
||||
newAbsolute = oldAbsolute
|
||||
}
|
||||
return relativeImportPathFromDirectory(tspath.GetDirectoryPath(newImportFromPath), newAbsolute, l.UseCaseSensitiveFileNames())
|
||||
}
|
||||
|
||||
func (l *LanguageService) updateImportsForFileRename(program *compiler.Program, changeTracker *change.Tracker, oldToNew pathUpdater) {
|
||||
allFiles := program.GetSourceFiles()
|
||||
checker, done := program.GetTypeChecker(context.Background())
|
||||
defer done()
|
||||
moduleSpecifierPreferences := l.UserPreferences().ModuleSpecifierPreferences()
|
||||
|
||||
for _, sourceFile := range allFiles {
|
||||
oldFileName := sourceFile.FileName()
|
||||
newFromOld, fileMoved := oldToNew(sourceFile.FileName())
|
||||
newImportFromPath := sourceFile.FileName()
|
||||
if fileMoved {
|
||||
newImportFromPath = newFromOld
|
||||
}
|
||||
|
||||
for _, ref := range sourceFile.ReferencedFiles {
|
||||
if !tspath.IsExternalModuleNameRelative(ref.FileName) {
|
||||
continue
|
||||
}
|
||||
updated := l.updateRelativePath(oldToNew, oldFileName, newImportFromPath, ref.FileName)
|
||||
if updated != ref.FileName {
|
||||
changeTracker.ReplaceRangeWithText(sourceFile, l.converters.ToLSPRange(sourceFile, ref.TextRange), updated)
|
||||
}
|
||||
}
|
||||
|
||||
for _, importStringLiteral := range sourceFile.Imports() {
|
||||
updated := l.getUpdatedImportSpecifier(program, checker, sourceFile, importStringLiteral, oldToNew, newImportFromPath, fileMoved, moduleSpecifierPreferences)
|
||||
if updated != "" && updated != importStringLiteral.Text() {
|
||||
changeTracker.ReplaceRangeWithText(sourceFile, l.converters.ToLSPRange(sourceFile, createStringTextRange(sourceFile, importStringLiteral)), updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We assume the source file did not move to a different program.
|
||||
func (l *LanguageService) getUpdatedImportSpecifier(
|
||||
program *compiler.Program,
|
||||
checker *checker.Checker,
|
||||
sourceFile *ast.SourceFile, // old importing source file
|
||||
importLiteral *ast.StringLiteralLike,
|
||||
oldToNew pathUpdater,
|
||||
newImportFromPath string,
|
||||
importingSourceFileMoved bool,
|
||||
userPreferences modulespecifiers.UserPreferences,
|
||||
) string {
|
||||
importedModuleSymbol := checker.GetSymbolAtLocation(importLiteral)
|
||||
if isAmbientModuleSymbol(importedModuleSymbol) {
|
||||
return ""
|
||||
}
|
||||
|
||||
target := getSourceFileToImport(program, sourceFile, importLiteral, oldToNew)
|
||||
|
||||
if target == nil {
|
||||
// First fall back: try every file in the program to see if any of them would match the import specifier, and if so, obtain the updated specifier for that file.
|
||||
if updated := getUpdatedImportSpecifierFromMovedSourceFiles(program, sourceFile, importLiteral, oldToNew, newImportFromPath, userPreferences); updated != "" && updated != importLiteral.Text() {
|
||||
return updated
|
||||
}
|
||||
// Fall back to a regular path update for unresolved module.
|
||||
if tspath.IsExternalModuleNameRelative(importLiteral.Text()) {
|
||||
return l.updateRelativePath(oldToNew, sourceFile.FileName(), newImportFromPath, importLiteral.Text())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Optimization: neither the importing or imported file changed.
|
||||
if !target.updated && !(importingSourceFileMoved && tspath.IsExternalModuleNameRelative(importLiteral.Text())) {
|
||||
return ""
|
||||
}
|
||||
|
||||
updated := modulespecifiers.UpdateModuleSpecifier(
|
||||
program.Options(),
|
||||
program,
|
||||
sourceFile,
|
||||
newImportFromPath,
|
||||
importLiteral.Text(),
|
||||
target.newFileName,
|
||||
userPreferences,
|
||||
modulespecifiers.ModuleSpecifierOptions{
|
||||
OverrideImportMode: program.GetModeForUsageLocation(sourceFile, importLiteral),
|
||||
},
|
||||
)
|
||||
return updated
|
||||
}
|
||||
|
||||
func getSourceFileToImport(
|
||||
program *compiler.Program,
|
||||
sourceFile *ast.SourceFile,
|
||||
importLiteral *ast.StringLiteralLike,
|
||||
oldToNew pathUpdater,
|
||||
) *toImport {
|
||||
if resolved := program.GetResolvedModuleFromModuleSpecifier(sourceFile, importLiteral); resolved != nil && resolved.ResolvedFileName != "" {
|
||||
oldFileName := resolved.ResolvedFileName
|
||||
if newFileName, ok := oldToNew(oldFileName); ok {
|
||||
return &toImport{newFileName: newFileName, updated: true}
|
||||
}
|
||||
return &toImport{newFileName: oldFileName, updated: false}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// As a fall back for unresolved modules, we'll check all files in the program to see if any of them would match
|
||||
// the import specifier, and if so, we'll obtain the updated specifier for that file.
|
||||
func getUpdatedImportSpecifierFromMovedSourceFiles(program *compiler.Program, sourceFile *ast.SourceFile, importLiteral *ast.StringLiteralLike, oldToNew pathUpdater, importingSourceFileName string, userPreferences modulespecifiers.UserPreferences) string {
|
||||
resolutionMode := program.GetModeForUsageLocation(sourceFile, importLiteral)
|
||||
for _, candidate := range program.GetSourceFiles() {
|
||||
newFileName, ok := oldToNew(candidate.FileName())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
oldSpecifier := modulespecifiers.UpdateModuleSpecifier(
|
||||
program.Options(),
|
||||
program,
|
||||
sourceFile,
|
||||
importingSourceFileName,
|
||||
importLiteral.Text(),
|
||||
candidate.FileName(),
|
||||
userPreferences,
|
||||
modulespecifiers.ModuleSpecifierOptions{
|
||||
OverrideImportMode: resolutionMode,
|
||||
},
|
||||
)
|
||||
if oldSpecifier != importLiteral.Text() {
|
||||
continue
|
||||
}
|
||||
|
||||
return modulespecifiers.UpdateModuleSpecifier(
|
||||
program.Options(),
|
||||
program,
|
||||
sourceFile,
|
||||
importingSourceFileName,
|
||||
importLiteral.Text(),
|
||||
newFileName,
|
||||
userPreferences,
|
||||
modulespecifiers.ModuleSpecifierOptions{
|
||||
OverrideImportMode: resolutionMode,
|
||||
},
|
||||
)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func createStringTextRange(sourceFile *ast.SourceFile, node *ast.LiteralLikeNode) core.TextRange {
|
||||
return core.NewTextRange(scanner.GetTokenPosOfNode(node, sourceFile, false)+1, node.End()-1)
|
||||
}
|
||||
|
||||
func getTsConfigObjectLiteralExpression(tsConfigSourceFile *ast.SourceFile) *ast.ObjectLiteralExpression {
|
||||
if tsConfigSourceFile != nil && tsConfigSourceFile.Statements != nil && len(tsConfigSourceFile.Statements.Nodes) > 0 {
|
||||
expression := tsConfigSourceFile.Statements.Nodes[0].Expression()
|
||||
if ast.IsObjectLiteralExpression(expression) {
|
||||
return expression.AsObjectLiteralExpression()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func forEachObjectProperty(objectLiteral *ast.ObjectLiteralExpression, cb func(property *ast.PropertyAssignment, propertyName string)) {
|
||||
if objectLiteral == nil {
|
||||
return
|
||||
}
|
||||
for _, property := range objectLiteral.Properties.Nodes {
|
||||
if !ast.IsPropertyAssignment(property) {
|
||||
continue
|
||||
}
|
||||
if name, ok := ast.TryGetTextOfPropertyName(property.Name()); ok {
|
||||
cb(property.AsPropertyAssignment(), name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func relativePathFromDirectory(fromDirectory string, to string, useCaseSensitiveFileNames bool) string {
|
||||
return tspath.GetRelativePathFromDirectory(fromDirectory, to, tspath.ComparePathsOptions{UseCaseSensitiveFileNames: useCaseSensitiveFileNames})
|
||||
}
|
||||
|
||||
func relativeImportPathFromDirectory(fromDirectory string, to string, useCaseSensitiveFileNames bool) string {
|
||||
return tspath.EnsurePathIsNonModuleName(relativePathFromDirectory(fromDirectory, to, useCaseSensitiveFileNames))
|
||||
}
|
||||
|
||||
func isAmbientModuleSymbol(symbol *ast.Symbol) bool {
|
||||
if symbol == nil {
|
||||
return false
|
||||
}
|
||||
return slices.ContainsFunc(symbol.Declarations, ast.IsModuleWithStringLiteralName)
|
||||
}
|
||||
Reference in New Issue
Block a user