diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 91fefba966..408e1c955b 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -994,6 +994,8 @@ func (n *Node) ModuleSpecifier() *Expression { return n.AsImportDeclaration().ModuleSpecifier case KindExportDeclaration: return n.AsExportDeclaration().ModuleSpecifier + case KindJSDocImportTag: + return n.AsJSDocImportTag().ModuleSpecifier } panic("Unhandled case in Node.ModuleSpecifier: " + n.Kind.String()) } diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 41e77d527b..2cddf320ff 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -1409,6 +1409,16 @@ func GetNameOfDeclaration(declaration *Node) *Node { return nil } +func GetImportClauseOfDeclaration(declaration *Declaration) *ImportClause { + switch declaration.Kind { + case KindImportDeclaration: + return declaration.AsImportDeclaration().ImportClause.AsImportClause() + case KindJSDocImportTag: + return declaration.AsJSDocImportTag().ImportClause.AsImportClause() + } + return nil +} + func GetNonAssignedNameOfDeclaration(declaration *Node) *Node { // !!! switch declaration.Kind { @@ -2723,6 +2733,15 @@ func IsRequireCall(node *Node, requireStringLiteralLikeArgument bool) bool { return !requireStringLiteralLikeArgument || IsStringLiteralLike(call.Arguments.Nodes[0]) } +func IsRequireVariableStatement(node *Node) bool { + if IsVariableStatement(node) { + if declarations := node.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes; len(declarations) > 0 { + return core.Every(declarations, IsVariableDeclarationInitializedToRequire) + } + } + return false +} + func GetJSXImplicitImportBase(compilerOptions *core.CompilerOptions, file *SourceFile) string { jsxImportSourcePragma := GetPragmaFromSourceFile(file, "jsximportsource") jsxRuntimePragma := GetPragmaFromSourceFile(file, "jsxruntime") diff --git a/internal/checker/exports.go b/internal/checker/exports.go index 83d4352c8e..dc61a4a547 100644 --- a/internal/checker/exports.go +++ b/internal/checker/exports.go @@ -26,6 +26,26 @@ func (c *Checker) GetMergedSymbol(symbol *ast.Symbol) *ast.Symbol { return c.getMergedSymbol(symbol) } +func (c *Checker) TryFindAmbientModule(moduleName string) *ast.Symbol { + return c.tryFindAmbientModule(moduleName, true /* withAugmentations */) +} + +func (c *Checker) GetImmediateAliasedSymbol(symbol *ast.Symbol) *ast.Symbol { + return c.getImmediateAliasedSymbol(symbol) +} + +func (c *Checker) GetTypeOnlyAliasDeclaration(symbol *ast.Symbol) *ast.Node { + return c.getTypeOnlyAliasDeclaration(symbol) +} + +func (c *Checker) ResolveExternalModuleName(moduleSpecifier *ast.Node) *ast.Symbol { + return c.resolveExternalModuleName(moduleSpecifier, moduleSpecifier, true /*ignoreErrors*/) +} + +func (c *Checker) ResolveExternalModuleSymbol(moduleSymbol *ast.Symbol) *ast.Symbol { + return c.resolveExternalModuleSymbol(moduleSymbol, false /*dontResolveAlias*/) +} + func (c *Checker) GetTypeFromTypeNode(node *ast.Node) *Type { return c.getTypeFromTypeNode(node) } @@ -150,7 +170,3 @@ func (c *Checker) GetIndexSignaturesAtLocation(node *ast.Node) []*ast.Node { func (c *Checker) GetResolvedSymbol(node *ast.Node) *ast.Symbol { return c.getResolvedSymbol(node) } - -func (c *Checker) GetImmediateAliasedSymbol(symbol *ast.Symbol) *ast.Symbol { - return c.getImmediateAliasedSymbol(symbol) -} diff --git a/internal/checker/nodebuilderimpl.go b/internal/checker/nodebuilderimpl.go index 9331c09d67..c9d84a294d 100644 --- a/internal/checker/nodebuilderimpl.go +++ b/internal/checker/nodebuilderimpl.go @@ -1076,7 +1076,7 @@ func canHaveModuleSpecifier(node *ast.Node) bool { return false } -func tryGetModuleSpecifierFromDeclaration(node *ast.Node) *ast.Node { +func TryGetModuleSpecifierFromDeclaration(node *ast.Node) *ast.Node { res := tryGetModuleSpecifierFromDeclarationWorker(node) if res == nil || !ast.IsStringLiteral(res) { return nil @@ -1162,7 +1162,7 @@ func (b *nodeBuilderImpl) getSpecifierForModuleSymbol(symbol *ast.Symbol, overri enclosingDeclaration := b.e.MostOriginal(b.ctx.enclosingDeclaration) var originalModuleSpecifier *ast.Node if canHaveModuleSpecifier(enclosingDeclaration) { - originalModuleSpecifier = tryGetModuleSpecifierFromDeclaration(enclosingDeclaration) + originalModuleSpecifier = TryGetModuleSpecifierFromDeclaration(enclosingDeclaration) } contextFile := b.ctx.enclosingFile resolutionMode := overrideImportMode @@ -1213,6 +1213,7 @@ func (b *nodeBuilderImpl) getSpecifierForModuleSymbol(symbol *ast.Symbol, overri modulespecifiers.ModuleSpecifierOptions{ OverrideImportMode: overrideImportMode, }, + false, /*forAutoImports*/ ) specifier := allSpecifiers[0] links.specifierCache[cacheKey] = specifier diff --git a/internal/checker/services.go b/internal/checker/services.go index 95edc6e39d..caed1fbccc 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -118,6 +118,35 @@ func (c *Checker) GetExportsOfModule(symbol *ast.Symbol) []*ast.Symbol { return symbolsToArray(c.getExportsOfModule(symbol)) } +func (c *Checker) ForEachExportAndPropertyOfModule(moduleSymbol *ast.Symbol, cb func(*ast.Symbol, string)) { + for key, exportedSymbol := range c.getExportsOfModule(moduleSymbol) { + if !isReservedMemberName(key) { + cb(exportedSymbol, key) + } + } + + exportEquals := c.resolveExternalModuleSymbol(moduleSymbol, false /*dontResolveAlias*/) + if exportEquals == moduleSymbol { + return + } + + typeOfSymbol := c.getTypeOfSymbol(exportEquals) + if !c.shouldTreatPropertiesOfExternalModuleAsExports(typeOfSymbol) { + return + } + + // forEachPropertyOfType + reducedType := c.getReducedApparentType(typeOfSymbol) + if reducedType.flags&TypeFlagsStructuredType == 0 { + return + } + for name, symbol := range c.resolveStructuredTypeMembers(reducedType).members { + if c.isNamedMember(symbol, name) { + cb(symbol, name) + } + } +} + func (c *Checker) IsValidPropertyAccess(node *ast.Node, propertyName string) bool { return c.isValidPropertyAccess(node, propertyName) } @@ -345,6 +374,13 @@ func runWithoutResolvedSignatureCaching[T any](c *Checker, node *ast.Node, fn fu return fn() } +func (c *Checker) SkipAlias(symbol *ast.Symbol) *ast.Symbol { + if symbol.Flags&ast.SymbolFlagsAlias != 0 { + return c.GetAliasedSymbol(symbol) + } + return symbol +} + func (c *Checker) GetRootSymbols(symbol *ast.Symbol) []*ast.Symbol { roots := c.getImmediateRootSymbols(symbol) if len(roots) == 0 { diff --git a/internal/compiler/program.go b/internal/compiler/program.go index f8d4d8fc87..27d291ed53 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -155,6 +155,10 @@ func (p *Program) UseCaseSensitiveFileNames() bool { return p.Host().FS().UseCaseSensitiveFileNames() } +func (p *Program) UsesUriStyleNodeCoreModules() bool { + return p.usesUriStyleNodeCoreModules.IsTrue() +} + var _ checker.Program = (*Program)(nil) /** This should have similar behavior to 'processSourceFile' without diagnostics or mutation. */ diff --git a/internal/core/compileroptions.go b/internal/core/compileroptions.go index ffca3d6c3d..7da3fba394 100644 --- a/internal/core/compileroptions.go +++ b/internal/core/compileroptions.go @@ -477,6 +477,17 @@ const ( NewLineKindLF NewLineKind = 2 ) +func GetNewLineKind(s string) NewLineKind { + switch s { + case "\r\n": + return NewLineKindCRLF + case "\n": + return NewLineKindLF + default: + return NewLineKindNone + } +} + func (newLine NewLineKind) GetNewLineCharacter() string { switch newLine { case NewLineKindCRLF: diff --git a/internal/core/core.go b/internal/core/core.go index a5528d8088..45198a0370 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -176,6 +176,17 @@ func Every[T any](slice []T, f func(T) bool) bool { return true } +func Or[T any](funcs ...func(T) bool) func(T) bool { + return func(input T) bool { + for _, f := range funcs { + if f(input) { + return true + } + } + return false + } +} + func Find[T any](slice []T, f func(T) bool) T { for _, value := range slice { if f(value) { diff --git a/internal/core/nodemodules.go b/internal/core/nodemodules.go index c103e4f837..a3abda62b0 100644 --- a/internal/core/nodemodules.go +++ b/internal/core/nodemodules.go @@ -70,7 +70,7 @@ var ExclusivelyPrefixedNodeCoreModules = map[string]bool{ "node:test/reporters": true, } -var nodeCoreModules = sync.OnceValue(func() map[string]bool { +var NodeCoreModules = sync.OnceValue(func() map[string]bool { nodeCoreModules := make(map[string]bool, len(UnprefixedNodeCoreModules)*2+len(ExclusivelyPrefixedNodeCoreModules)) for unprefixed := range UnprefixedNodeCoreModules { nodeCoreModules[unprefixed] = true @@ -81,7 +81,7 @@ var nodeCoreModules = sync.OnceValue(func() map[string]bool { }) func NonRelativeModuleNameForTypingCache(moduleName string) string { - if nodeCoreModules()[moduleName] { + if NodeCoreModules()[moduleName] { return "node" } return moduleName diff --git a/internal/core/textchange.go b/internal/core/textchange.go index 0b4901c048..9945af4f70 100644 --- a/internal/core/textchange.go +++ b/internal/core/textchange.go @@ -1,5 +1,7 @@ package core +import "strings" + type TextChange struct { TextRange NewText string @@ -8,3 +10,21 @@ type TextChange struct { func (t TextChange) ApplyTo(text string) string { return text[:t.Pos()] + t.NewText + text[t.End():] } + +func ApplyBulkEdits(text string, edits []TextChange) string { + b := strings.Builder{} + b.Grow(len(text)) + lastEnd := 0 + for _, e := range edits { + start := e.TextRange.Pos() + if start != lastEnd { + b.WriteString(text[lastEnd:e.TextRange.Pos()]) + } + b.WriteString(e.NewText) + + lastEnd = e.TextRange.End() + } + b.WriteString(text[lastEnd:]) + + return b.String() +} diff --git a/internal/execute/tsc.go b/internal/execute/tsc.go index 680c50f8e3..395685041f 100644 --- a/internal/execute/tsc.go +++ b/internal/execute/tsc.go @@ -20,24 +20,6 @@ import ( "github.com/microsoft/typescript-go/internal/tspath" ) -func applyBulkEdits(text string, edits []core.TextChange) string { - b := strings.Builder{} - b.Grow(len(text)) - lastEnd := 0 - for _, e := range edits { - start := e.TextRange.Pos() - if start != lastEnd { - b.WriteString(text[lastEnd:e.TextRange.Pos()]) - } - b.WriteString(e.NewText) - - lastEnd = e.TextRange.End() - } - b.WriteString(text[lastEnd:]) - - return b.String() -} - func CommandLine(sys tsc.System, commandLineArgs []string, testing tsc.CommandLineTesting) tsc.CommandLineResult { if len(commandLineArgs) > 0 { // !!! build mode @@ -69,7 +51,7 @@ func fmtMain(sys tsc.System, input, output string) tsc.ExitStatus { JSDocParsingMode: ast.JSDocParsingModeParseAll, }, text, core.GetScriptKindFromFileName(string(pathified))) edits := format.FormatDocument(ctx, sourceFile) - newText := applyBulkEdits(text, edits) + newText := core.ApplyBulkEdits(text, edits) if err := sys.FS().WriteFile(output, newText, false); err != nil { fmt.Fprintln(sys.Writer(), err.Error()) diff --git a/internal/format/api.go b/internal/format/api.go index 7e11bd3266..77be9feece 100644 --- a/internal/format/api.go +++ b/internal/format/api.go @@ -75,6 +75,25 @@ func FormatSpan(ctx context.Context, span core.TextRange, file *ast.SourceFile, ) } +func FormatNodeGivenIndentation(ctx context.Context, node *ast.Node, file *ast.SourceFile, languageVariant core.LanguageVariant, initialIndentation int, delta int) []core.TextChange { + textRange := core.NewTextRange(node.Pos(), node.End()) + return newFormattingScanner( + file.Text(), + languageVariant, + textRange.Pos(), + textRange.End(), + newFormatSpanWorker( + ctx, + textRange, + node, + initialIndentation, + delta, + FormatRequestKindFormatSelection, + func(core.TextRange) bool { return false }, // assume that node does not have any errors + file, + )) +} + func formatNodeLines(ctx context.Context, sourceFile *ast.SourceFile, node *ast.Node, requestKind FormatRequestKind) []core.TextChange { if node == nil { return nil diff --git a/internal/format/indent.go b/internal/format/indent.go index 0dfc596ffd..589a160032 100644 --- a/internal/format/indent.go +++ b/internal/format/indent.go @@ -46,7 +46,7 @@ func getIndentationForNodeWorker( if useActualIndentation { // check if current node is a list item - if yes, take indentation from it var firstListChild *ast.Node - containerList := getContainingList(current, sourceFile) + containerList := GetContainingList(current, sourceFile) if containerList != nil { firstListChild = core.FirstOrNil(containerList.Nodes) } @@ -140,7 +140,7 @@ func getActualIndentationForListItem(node *ast.Node, sourceFile *ast.SourceFile, // VariableDeclarationList has no wrapping tokens return -1 } - containingList := getContainingList(node, sourceFile) + containingList := GetContainingList(node, sourceFile) if containingList != nil { index := core.FindIndex(containingList.Nodes, func(e *ast.Node) bool { return e == node }) if index != -1 { @@ -197,10 +197,10 @@ func deriveActualIndentationFromList(list *ast.NodeList, index int, sourceFile * func findColumnForFirstNonWhitespaceCharacterInLine(line int, char int, sourceFile *ast.SourceFile, options *FormatCodeSettings) int { lineStart := scanner.GetPositionOfLineAndCharacter(sourceFile, line, 0) - return findFirstNonWhitespaceColumn(lineStart, lineStart+char, sourceFile, options) + return FindFirstNonWhitespaceColumn(lineStart, lineStart+char, sourceFile, options) } -func findFirstNonWhitespaceColumn(startPos int, endPos int, sourceFile *ast.SourceFile, options *FormatCodeSettings) int { +func FindFirstNonWhitespaceColumn(startPos int, endPos int, sourceFile *ast.SourceFile, options *FormatCodeSettings) int { _, col := findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options) return col } @@ -250,7 +250,7 @@ func getStartLineAndCharacterForNode(n *ast.Node, sourceFile *ast.SourceFile) (l return scanner.GetLineAndCharacterOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false)) } -func getContainingList(node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList { +func GetContainingList(node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList { if node.Parent == nil { return nil } @@ -349,7 +349,7 @@ func getVisualListRange(node *ast.Node, list core.TextRange, sourceFile *ast.Sou } func getContainingListOrParentStart(parent *ast.Node, child *ast.Node, sourceFile *ast.SourceFile) (line int, character int) { - containingList := getContainingList(child, sourceFile) + containingList := GetContainingList(child, sourceFile) var startPos int if containingList != nil { startPos = containingList.Loc.Pos() diff --git a/internal/format/span.go b/internal/format/span.go index 1ae4d56f4a..7276a97ebf 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -470,7 +470,7 @@ func (w *formatSpanWorker) processChildNodes( indentationOnListStartToken = w.indentationOnLastIndentedLine } else { startLinePosition := GetLineStartPositionForPosition(tokenInfo.token.Loc.Pos(), w.sourceFile) - indentationOnListStartToken = findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.Loc.Pos(), w.sourceFile, w.formattingContext.Options) + indentationOnListStartToken = FindFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.Loc.Pos(), w.sourceFile, w.formattingContext.Options) } listDynamicIndentation = w.getDynamicIndentation(parent, parentStartLine, indentationOnListStartToken, w.formattingContext.Options.IndentSize) @@ -580,7 +580,7 @@ func (w *formatSpanWorker) tryComputeIndentationForListItem(startPos int, endPos } else { startLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, startPos) startLinePosition := GetLineStartPositionForPosition(startPos, w.sourceFile) - column := findFirstNonWhitespaceColumn(startLinePosition, startPos, w.sourceFile, w.formattingContext.Options) + column := FindFirstNonWhitespaceColumn(startLinePosition, startPos, w.sourceFile, w.formattingContext.Options) if startLine != parentStartLine || startPos == column { // Use the base indent size if it is greater than // the indentation of the inherited predecessor. diff --git a/internal/fourslash/_scripts/failingTests.txt b/internal/fourslash/_scripts/failingTests.txt index 07bab926d1..54854f47e3 100644 --- a/internal/fourslash/_scripts/failingTests.txt +++ b/internal/fourslash/_scripts/failingTests.txt @@ -18,7 +18,6 @@ TestCompletionEntryForArrayElementConstrainedToString2 TestCompletionEntryForClassMembers_StaticWhenBaseTypeIsNotResolved TestCompletionEntryForUnionProperty TestCompletionEntryForUnionProperty2 -TestCompletionExportFrom TestCompletionForComputedStringProperties TestCompletionForMetaProperty TestCompletionForStringLiteral @@ -168,6 +167,7 @@ TestDoubleUnderscoreCompletions TestEditJsdocType TestExportDefaultClass TestExportDefaultFunction +TestExportEqualNamespaceClassESModuleInterop TestFindAllReferencesDynamicImport1 TestFindAllReferencesTripleSlash TestFindAllReferencesUmdModuleAsGlobalConst @@ -225,9 +225,6 @@ TestImportCompletions_importsMap2 TestImportCompletions_importsMap3 TestImportCompletions_importsMap4 TestImportCompletions_importsMap5 -TestImportStatementCompletions4 -TestImportStatementCompletions_noPatternAmbient -TestImportStatementCompletions_pnpmTransitive TestIndexerReturnTypes1 TestIndirectClassInstantiation TestInstanceTypesForGenericType1 diff --git a/internal/fourslash/baselineutil.go b/internal/fourslash/baselineutil.go index b2dc636f18..d9c625f69d 100644 --- a/internal/fourslash/baselineutil.go +++ b/internal/fourslash/baselineutil.go @@ -563,19 +563,23 @@ func (t *textWithContext) sliceOfContent(start *int, end *int) string { } func (t *textWithContext) getIndex(i interface{}) *int { - switch i.(type) { + switch i := i.(type) { case *int: - return i.(*int) + return i case int: - return ptrTo(i.(int)) + return ptrTo(i) case core.TextPos: - return ptrTo(int(i.(core.TextPos))) + return ptrTo(int(i)) case *core.TextPos: - return ptrTo(int(*i.(*core.TextPos))) + return ptrTo(int(*i)) case lsproto.Position: - return t.getIndex(t.converters.LineAndCharacterToPosition(t, i.(lsproto.Position))) + return t.getIndex(t.converters.LineAndCharacterToPosition(t, i)) case *lsproto.Position: - return t.getIndex(t.converters.LineAndCharacterToPosition(t, *i.(*lsproto.Position))) + return t.getIndex(t.converters.LineAndCharacterToPosition(t, *i)) } panic(fmt.Sprintf("getIndex: unsupported type %T", i)) } + +func codeFence(lang string, code string) string { + return "```" + lang + "\n" + code + "\n```" +} diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 058074ce7a..c64cd1f72d 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -467,9 +467,10 @@ func getLanguageKind(filename string) lsproto.LanguageKind { } type CompletionsExpectedList struct { - IsIncomplete bool - ItemDefaults *CompletionsExpectedItemDefaults - Items *CompletionsExpectedItems + IsIncomplete bool + ItemDefaults *CompletionsExpectedItemDefaults + Items *CompletionsExpectedItems + UserPreferences *ls.UserPreferences // !!! allow user preferences in fourslash } type Ignored = struct{} @@ -1431,3 +1432,106 @@ func (f *FourslashTest) getCurrentPositionPrefix() string { } return fmt.Sprintf("At position (Ln %d, Col %d): ", f.currentCaretPosition.Line, f.currentCaretPosition.Character) } + +func (f *FourslashTest) BaselineAutoImportsCompletions(t *testing.T, markerNames []string) { + if f.baseline != nil { + t.Fatalf("Error during test '%s': Another baseline is already in progress", t.Name()) + } else { + f.baseline = &baselineFromTest{ + content: &strings.Builder{}, + baselineName: "autoImport/" + strings.TrimPrefix(t.Name(), "Test"), + ext: ".baseline.md", + } + } + for _, markerName := range markerNames { + f.GoToMarker(t, markerName) + params := &lsproto.CompletionParams{ + TextDocument: lsproto.TextDocumentIdentifier{ + Uri: ls.FileNameToDocumentURI(f.activeFilename), + }, + Position: f.currentCaretPosition, + Context: &lsproto.CompletionContext{}, + } + resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentCompletionInfo, params) + + prefix := fmt.Sprintf("At marker '%s': ", markerName) + if resMsg == nil { + t.Fatalf(prefix+"Nil response received for completion request for autoimports", f.lastKnownMarkerName) + } + if !resultOk { + t.Fatalf(prefix+"Unexpected response type for completion request for autoimports: %T", resMsg.AsResponse().Result) + } + + f.baseline.content.WriteString("// === Auto Imports === \n") + + fileContent, ok := f.vfs.ReadFile(f.activeFilename) + if !ok { + t.Fatalf(prefix+"Failed to read file %s for auto-import baseline", f.activeFilename) + } + + marker := f.testData.MarkerPositions[markerName] + ext := strings.TrimPrefix(tspath.GetAnyExtensionFromPath(f.activeFilename, nil, true), ".") + lang := core.IfElse(ext == "mts" || ext == "cts", "ts", ext) + f.baseline.content.WriteString(codeFence( + lang, + "// @FileName: "+f.activeFilename+"\n"+fileContent[:marker.Position]+"/*"+markerName+"*/"+fileContent[marker.Position:], + )) + + currentFile := newScriptInfo(f.activeFilename, fileContent) + converters := ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *ls.LineMap { + return currentFile.lineMap + }) + var list []*lsproto.CompletionItem + if result.Items == nil || len(*result.Items) == 0 { + if result.List == nil || result.List.Items == nil || len(result.List.Items) == 0 { + f.baseline.content.WriteString("no autoimport completions found" + "\n\n") + + continue + } + list = result.List.Items + } else { + list = *result.Items + } + + for _, item := range list { + if item.Data == nil || *item.SortText != string(ls.SortTextAutoImportSuggestions) { + continue + } + resMsg, details, resultOk := sendRequest(t, f, lsproto.CompletionItemResolveInfo, item) + if resMsg == nil { + t.Fatalf(prefix+"Nil response received for resolve completion", f.lastKnownMarkerName) + } + if !resultOk { + t.Fatalf(prefix+"Unexpected response type for resolve completion: %T", resMsg.AsResponse().Result) + } + if details == nil || details.AdditionalTextEdits == nil || len(*details.AdditionalTextEdits) == 0 { + t.Fatalf(prefix+"Entry %s from %s returned no code changes from completion details request", item.Label, item.Detail) + } + allChanges := *details.AdditionalTextEdits + + // !!! calculate the change provided by the completiontext + // completionChange:= &lsproto.TextEdit{} + // if details.TextEdit != nil { + // completionChange = details.TextEdit.TextEdit + // } else if details.AdditionalTextEdits != nil && len(*details.AdditionalTextEdits) > 0 { + // completionChange = (*details.AdditionalTextEdits)[0] + // } else { + // completionChange.Range = lsproto.Range{ Start: marker.LSPosition, End: marker.LSPosition } + // if item.InsertText != nil { + // completionChange.NewText = *item.InsertText + // } else { + // completionChange.NewText = item.Label + // } + // } + // allChanges := append(allChanges, completionChange) + // sorted from back-of-file-most to front-of-file-most + slices.SortFunc(allChanges, func(a, b *lsproto.TextEdit) int { return ls.ComparePositions(b.Range.Start, a.Range.Start) }) + newFileContent := fileContent + for _, change := range allChanges { + newFileContent = newFileContent[:converters.LineAndCharacterToPosition(currentFile, change.Range.Start)] + change.NewText + newFileContent[converters.LineAndCharacterToPosition(currentFile, change.Range.End):] + } + f.baseline.content.WriteString(codeFence(lang, newFileContent) + "\n\n") + } + } + baseline.Run(t, f.baseline.getBaselineFileName(), f.baseline.content.String(), baseline.Options{}) +} diff --git a/internal/fourslash/tests/autoImportCompletion_test.go b/internal/fourslash/tests/autoImportCompletion_test.go new file mode 100644 index 0000000000..53e473f0fe --- /dev/null +++ b/internal/fourslash/tests/autoImportCompletion_test.go @@ -0,0 +1,142 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + . "github.com/microsoft/typescript-go/internal/fourslash/tests/util" + "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestAutoImportCompletion1(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: a.ts +export const someVar = 10; + +// @Filename: b.ts +export const anotherVar = 10; + +// @Filename: c.ts +import {someVar} from "./a.ts"; +someVar; +a/**/ +` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ + UserPreferences: &ls.UserPreferences{ + IncludeCompletionsForModuleExports: PtrTo(true), + IncludeCompletionsForImportStatements: PtrTo(true), + }, + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{"someVar", "anotherVar"}, + }, + }) + f.BaselineAutoImportsCompletions(t, []string{""}) +} + +func TestAutoImportCompletion2(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: a.ts +export const someVar = 10; +export const anotherVar = 10; + +// @Filename: c.ts +import {someVar} from "./a.ts"; +someVar; +a/**/ +` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ + UserPreferences: &ls.UserPreferences{ + IncludeCompletionsForModuleExports: PtrTo(true), + IncludeCompletionsForImportStatements: PtrTo(true), + }, + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{"someVar", "anotherVar"}, + }, + }) + f.BaselineAutoImportsCompletions(t, []string{""}) +} + +func TestAutoImportCompletion3(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: a.ts +export const aa = "asdf"; +export const someVar = 10; +export const bb = 10; + +// @Filename: c.ts +import { aa, someVar } from "./a.ts"; +someVar; +b/**/ +` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ + UserPreferences: &ls.UserPreferences{ + IncludeCompletionsForModuleExports: PtrTo(true), + IncludeCompletionsForImportStatements: PtrTo(true), + }, + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{"bb"}, + }, + }) + f.BaselineAutoImportsCompletions(t, []string{""}) +} + +func TestNodeModulesImportCompletions1Baseline(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @allowJs: true +// @module: node18 +// @Filename: /src/module.mts +export {} +// @Filename: /src/module.cts +export {} +// @Filename: /src/module.js +export {} +// @Filename: /src/decl.d.mts +export {} +// @Filename: /src/decl.d.cts +export {} +// @Filename: /src/decl.d.ts +export {} +// @Filename: /src/js.mjs +export {} +// @Filename: /src/js.cjs +export {} +// @Filename: /src/js.js +export {} +// @Filename: /main.mts +import {} from "./src//*1*/"; //note, this test should not work until packagejsonautoimportprovider is implemented +import mod = require("./src//*2*/"); +const m = import("./src//*3*/"); +// @Filename: /main.cts +import {} from "./src//*4*/"; +import mod = require("./src//*5*/"); +const m = import("./src//*6*/"); +// @Filename: /main.ts +import {} from "./src//*7*/"; +import mod = require("./src//*8*/"); +const m = import("./src//*9*/");` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.BaselineAutoImportsCompletions(t, []string{"1", "3", "6", "9", "2", "4", "5", "7", "8"}) +} diff --git a/internal/fourslash/tests/gen/completionExportFrom_test.go b/internal/fourslash/tests/gen/completionExportFrom_test.go index c19346f4cf..e648a2b101 100644 --- a/internal/fourslash/tests/gen/completionExportFrom_test.go +++ b/internal/fourslash/tests/gen/completionExportFrom_test.go @@ -12,7 +12,7 @@ import ( func TestCompletionExportFrom(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `export * /*1*/; export {} /*2*/;` diff --git a/internal/fourslash/tests/gen/exportEqualNamespaceClassESModuleInterop_test.go b/internal/fourslash/tests/gen/exportEqualNamespaceClassESModuleInterop_test.go index 32f1db0ff0..1c3669c741 100644 --- a/internal/fourslash/tests/gen/exportEqualNamespaceClassESModuleInterop_test.go +++ b/internal/fourslash/tests/gen/exportEqualNamespaceClassESModuleInterop_test.go @@ -10,7 +10,7 @@ import ( func TestExportEqualNamespaceClassESModuleInterop(t *testing.T) { t.Parallel() - + t.Skip() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @esModuleInterop: true // @moduleResolution: node diff --git a/internal/fourslash/tests/gen/importStatementCompletions4_test.go b/internal/fourslash/tests/gen/importStatementCompletions4_test.go index 30c586e479..274183401c 100644 --- a/internal/fourslash/tests/gen/importStatementCompletions4_test.go +++ b/internal/fourslash/tests/gen/importStatementCompletions4_test.go @@ -12,7 +12,7 @@ import ( func TestImportStatementCompletions4(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @allowJs: true // @Filename: /a.js diff --git a/internal/fourslash/tests/gen/importStatementCompletions_noPatternAmbient_test.go b/internal/fourslash/tests/gen/importStatementCompletions_noPatternAmbient_test.go index 09532666fa..4700a41492 100644 --- a/internal/fourslash/tests/gen/importStatementCompletions_noPatternAmbient_test.go +++ b/internal/fourslash/tests/gen/importStatementCompletions_noPatternAmbient_test.go @@ -12,7 +12,7 @@ import ( func TestImportStatementCompletions_noPatternAmbient(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @Filename: /types.d.ts declare module "*.css" { diff --git a/internal/fourslash/tests/gen/importStatementCompletions_pnpmTransitive_test.go b/internal/fourslash/tests/gen/importStatementCompletions_pnpmTransitive_test.go index becc9be3b2..297213158f 100644 --- a/internal/fourslash/tests/gen/importStatementCompletions_pnpmTransitive_test.go +++ b/internal/fourslash/tests/gen/importStatementCompletions_pnpmTransitive_test.go @@ -12,7 +12,7 @@ import ( func TestImportStatementCompletions_pnpmTransitive(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @Filename: /home/src/workspaces/project/tsconfig.json { "compilerOptions": { "module": "commonjs" } } diff --git a/internal/ls/autoImports_stringer_generated.go b/internal/ls/autoImports_stringer_generated.go new file mode 100644 index 0000000000..d39f19211e --- /dev/null +++ b/internal/ls/autoImports_stringer_generated.go @@ -0,0 +1,27 @@ +// Code generated by "stringer -type=ExportKind -output=autoImports_stringer_generated.go"; DO NOT EDIT. + +package ls + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ExportKindNamed-0] + _ = x[ExportKindDefault-1] + _ = x[ExportKindExportEquals-2] + _ = x[ExportKindUMD-3] + _ = x[ExportKindModule-4] +} + +const _ExportKind_name = "ExportKindNamedExportKindDefaultExportKindExportEqualsExportKindUMDExportKindModule" + +var _ExportKind_index = [...]uint8{0, 15, 32, 54, 67, 83} + +func (i ExportKind) String() string { + if i < 0 || i >= ExportKind(len(_ExportKind_index)-1) { + return "ExportKind(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ExportKind_name[_ExportKind_index[i]:_ExportKind_index[i+1]] +} diff --git a/internal/ls/autoimportfixes.go b/internal/ls/autoimportfixes.go new file mode 100644 index 0000000000..907f74d238 --- /dev/null +++ b/internal/ls/autoimportfixes.go @@ -0,0 +1,325 @@ +package ls + +import ( + "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" +) + +type Import struct { + name string + kind ImportKind // ImportKindCommonJS | ImportKindNamespace + addAsTypeOnly AddAsTypeOnly + propertyName string // Use when needing to generate an `ImportSpecifier with a `propertyName`; the name preceding "as" keyword (propertyName = "" when "as" is absent) +} + +func (ct *changeTracker) addNamespaceQualifier(sourceFile *ast.SourceFile, qualification *Qualification) { + ct.insertText(sourceFile, qualification.usagePosition, qualification.namespacePrefix+".") +} + +func (ct *changeTracker) doAddExistingFix( + sourceFile *ast.SourceFile, + clause *ast.Node, // ImportClause | ObjectBindingPattern, + defaultImport *Import, + namedImports []*Import, + // removeExistingImportSpecifiers *core.Set[ImportSpecifier | BindingElement] // !!! remove imports not implemented + preferences *UserPreferences, +) { + switch clause.Kind { + case ast.KindObjectBindingPattern: + if clause.Kind == ast.KindObjectBindingPattern { + // bindingPattern := clause.AsBindingPattern() + // !!! adding *and* removing imports not implemented + // if (removeExistingImportSpecifiers && core.Some(bindingPattern.Elements, func(e *ast.Node) bool { + // return removeExistingImportSpecifiers.Has(e) + // })) { + // If we're both adding and removing elements, just replace and reprint the whole + // node. The change tracker doesn't understand all the operations and can insert or + // leave behind stray commas. + // ct.replaceNode( + // sourceFile, + // bindingPattern, + // ct.NodeFactory.NewObjectBindingPattern([ + // ...bindingPattern.Elements.Filter(func(e *ast.Node) bool { + // return !removeExistingImportSpecifiers.Has(e) + // }), + // ...defaultImport ? [ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, /*propertyName*/ "default", defaultImport.name)] : emptyArray, + // ...namedImports.map(i => ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, i.propertyName, i.name)), + // ]), + // ) + // return + // } + if defaultImport != nil { + ct.addElementToBindingPattern(sourceFile, clause, defaultImport.name, ptrTo("default")) + } + for _, specifier := range namedImports { + ct.addElementToBindingPattern(sourceFile, clause, specifier.name, &specifier.propertyName) + } + return + } + case ast.KindImportClause: + + importClause := clause.AsImportClause() + + // promoteFromTypeOnly = true if we need to promote the entire original clause from type only + promoteFromTypeOnly := importClause.IsTypeOnly && core.Some(append(namedImports, defaultImport), func(i *Import) bool { + if i == nil { + return false + } + return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed + }) + + existingSpecifiers := []*ast.Node{} // []*ast.ImportSpecifier + if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamedImports { + existingSpecifiers = importClause.NamedBindings.Elements() + } + + if defaultImport != nil { + debug.Assert(clause.Name() == nil, "Cannot add a default import to an import clause that already has one") + ct.insertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(clause, sourceFile, false)), ct.NodeFactory.NewIdentifier(defaultImport.name), changeNodeOptions{suffix: ", "}) + } + + if len(namedImports) > 0 { + // !!! OrganizeImports not yet implemented + // specifierComparer, isSorted := OrganizeImports.getNamedImportSpecifierComparerWithDetection(importClause.Parent, preferences, sourceFile); + newSpecifiers := core.Map(namedImports, func(namedImport *Import) *ast.Node { + var identifier *ast.Node + if namedImport.propertyName != "" { + identifier = ct.NodeFactory.NewIdentifier(namedImport.propertyName).AsIdentifier().AsNode() + } + return ct.NodeFactory.NewImportSpecifier( + (!importClause.IsTypeOnly || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), + identifier, + ct.NodeFactory.NewIdentifier(namedImport.name), + ) + }) // !!! sort with specifierComparer + + // !!! remove imports not implemented + // if (removeExistingImportSpecifiers) { + // // If we're both adding and removing specifiers, just replace and reprint the whole + // // node. The change tracker doesn't understand all the operations and can insert or + // // leave behind stray commas. + // ct.replaceNode( + // sourceFile, + // importClause.NamedBindings, + // ct.NodeFactory.updateNamedImports( + // importClause.NamedBindings.AsNamedImports(), + // append(core.Filter(existingSpecifiers, func (s *ast.ImportSpecifier) bool {return !removeExistingImportSpecifiers.Has(s)}), newSpecifiers...), // !!! sort with specifierComparer + // ), + // ); + // } else if (len(existingSpecifiers) > 0 && isSorted != false) { + // !!! OrganizeImports not implemented + // The sorting preference computed earlier may or may not have validated that these particular + // import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return + // nonsense. So if there are existing specifiers, even if we know the sorting preference, we + // need to ensure that the existing specifiers are sorted according to the preference in order + // to do a sorted insertion. + // changed to check if existing specifiers are sorted + // if we're promoting the clause from type-only, we need to transform the existing imports before attempting to insert the new named imports + // transformedExistingSpecifiers := existingSpecifiers + // if promoteFromTypeOnly && existingSpecifiers { + // transformedExistingSpecifiers = ct.NodeFactory.updateNamedImports( + // importClause.NamedBindings.AsNamedImports(), + // core.SameMap(existingSpecifiers, func(e *ast.ImportSpecifier) *ast.ImportSpecifier { + // return ct.NodeFactory.updateImportSpecifier(e, /*isTypeOnly*/ true, e.propertyName, e.name) + // }), + // ).elements + // } + // for _, spec := range newSpecifiers { + // insertionIndex := OrganizeImports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec, specifierComparer); + // ct.insertImportSpecifierAtIndex(sourceFile, spec, importClause.namedBindings as NamedImports, insertionIndex); + // } + // } else + if len(existingSpecifiers) > 0 { + for _, spec := range newSpecifiers { + ct.insertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers) + } + } else { + if len(newSpecifiers) > 0 { + namedImports := ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(newSpecifiers)) + if importClause.NamedBindings != nil { + ct.replaceNode(sourceFile, importClause.NamedBindings, namedImports, nil) + } else { + if clause.Name() == nil { + panic("Import clause must have either named imports or a default import") + } + ct.insertNodeAfter(sourceFile, clause.Name(), namedImports) + } + } + } + } + + if promoteFromTypeOnly { + // !!! promote type-only imports not implemented + + // ct.delete(sourceFile, getTypeKeywordOfTypeOnlyImport(clause, sourceFile)); + // if (existingSpecifiers) { + // // We used to convert existing specifiers to type-only only if compiler options indicated that + // // would be meaningful (see the `importNameElisionDisabled` utility function), but user + // // feedback indicated a preference for preserving the type-onlyness of existing specifiers + // // regardless of whether it would make a difference in emit. + // for _, specifier := range existingSpecifiers { + // ct.insertModifierBefore(sourceFile, SyntaxKind.TypeKeyword, specifier); + // } + // } + } + default: + panic("Unsupported clause kind: " + clause.Kind.String() + "for doAddExistingFix") + } +} + +func (ct *changeTracker) addElementToBindingPattern(sourceFile *ast.SourceFile, bindingPattern *ast.Node, name string, propertyName *string) { + element := ct.newBindingElementFromNameAndPropertyName(name, propertyName) + if len(bindingPattern.Elements()) > 0 { + ct.insertNodeInListAfter(sourceFile, bindingPattern.Elements()[len(bindingPattern.Elements())-1], element, nil) + } else { + ct.replaceNode(sourceFile, bindingPattern, ct.NodeFactory.NewBindingPattern( + ast.KindObjectBindingPattern, + ct.NodeFactory.NewNodeList([]*ast.Node{element}), + ), nil) + } +} + +func (ct *changeTracker) newBindingElementFromNameAndPropertyName(name string, propertyName *string) *ast.Node { + var newPropertyNameIdentifier *ast.Node + if propertyName != nil { + newPropertyNameIdentifier = ct.NodeFactory.NewIdentifier(*propertyName) + } + return ct.NodeFactory.NewBindingElement( + nil, /*dotDotDotToken*/ + newPropertyNameIdentifier, + ct.NodeFactory.NewIdentifier(name), + nil, /* initializer */ + ) +} + +func (ct *changeTracker) insertImports(sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool, preferences *UserPreferences) { + var existingImportStatements []*ast.Statement + + if imports[0].Kind == ast.KindVariableStatement { + existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsRequireVariableStatement) + } else { + existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsAnyImportSyntax) + } + // !!! OrganizeImports + // { comparer, isSorted } := OrganizeImports.getOrganizeImportsStringComparerWithDetection(existingImportStatements, preferences); + // sortedNewImports := isArray(imports) ? toSorted(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; + sortedNewImports := imports + // !!! FutureSourceFile + // if !isFullSourceFile(sourceFile) { + // for _, newImport := range sortedNewImports { + // // Insert one at a time to send correct original source file for accurate text reuse + // // when some imports are cloned from existing ones in other files. + // ct.insertStatementsInNewFile(sourceFile.fileName, []*ast.Node{newImport}, ast.GetSourceFileOfNode(getOriginalNode(newImport))) + // } + // return; + // } + + // if len(existingImportStatements) > 0 && isSorted { + // for _, newImport := range sortedNewImports { + // insertionIndex := OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer) + // if insertionIndex == 0 { + // // If the first import is top-of-file, insert after the leading comment which is likely the header. + // options := existingImportStatements[0] == sourceFile.statements[0] ? { leadingTriviaOption: textchanges.LeadingTriviaOption.Exclude } : {}; + // ct.insertNodeBefore(sourceFile, existingImportStatements[0], newImport, /*blankLineBetween*/ false, options); + // } else { + // prevImport := existingImportStatements[insertionIndex - 1] + // ct.insertNodeAfter(sourceFile, prevImport, newImport); + // } + // } + // return + // } + + if len(existingImportStatements) > 0 { + ct.insertNodesAfter(sourceFile, existingImportStatements[len(existingImportStatements)-1], sortedNewImports) + } else { + ct.insertAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween) + } +} + +func (ct *changeTracker) makeImport(defaultImport *ast.IdentifierNode, namedImports []*ast.Node, moduleSpecifier *ast.Expression, isTypeOnly bool) *ast.Statement { + var newNamedImports *ast.Node + if len(namedImports) > 0 { + newNamedImports = ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(namedImports)) + } + var importClause *ast.Node + if defaultImport != nil || newNamedImports != nil { + importClause = ct.NodeFactory.NewImportClause(isTypeOnly, defaultImport, newNamedImports) + } + return ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, importClause, moduleSpecifier, nil /*attributes*/) +} + +func (ct *changeTracker) getNewImports( + moduleSpecifier string, + // quotePreference quotePreference, // !!! quotePreference + defaultImport *Import, + namedImports []*Import, + namespaceLikeImport *Import, // { importKind: ImportKind.CommonJS | ImportKind.Namespace; } + compilerOptions *core.CompilerOptions, + preferences *UserPreferences, +) []*ast.Statement { + moduleSpecifierStringLiteral := ct.NodeFactory.NewStringLiteral(moduleSpecifier) + var statements []*ast.Statement // []AnyImportSyntax + if defaultImport != nil || len(namedImports) > 0 { + // `verbatimModuleSyntax` should prefer top-level `import type` - + // even though it's not an error, it would add unnecessary runtime emit. + topLevelTypeOnly := (defaultImport == nil || needsTypeOnly(defaultImport.addAsTypeOnly)) && + core.Every(namedImports, func(i *Import) bool { return needsTypeOnly(i.addAsTypeOnly) }) || + (compilerOptions.VerbatimModuleSyntax.IsTrue() || ptrIsTrue(preferences.PreferTypeOnlyAutoImports)) && + defaultImport != nil && defaultImport.addAsTypeOnly != AddAsTypeOnlyNotAllowed && !core.Some(namedImports, func(i *Import) bool { return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed }) + + var defaultImportNode *ast.Node + if defaultImport != nil { + defaultImportNode = ct.NodeFactory.NewIdentifier(defaultImport.name) + } + + statements = append(statements, ct.makeImport(defaultImportNode, core.Map(namedImports, func(namedImport *Import) *ast.Node { + var namedImportPropertyName *ast.Node + if namedImport.propertyName != "" { + namedImportPropertyName = ct.NodeFactory.NewIdentifier(namedImport.propertyName) + } + return ct.NodeFactory.NewImportSpecifier( + !topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), + namedImportPropertyName, + ct.NodeFactory.NewIdentifier(namedImport.name), + ) + }), moduleSpecifierStringLiteral, topLevelTypeOnly)) + } + + if namespaceLikeImport != nil { + var declaration *ast.Statement + if namespaceLikeImport.kind == ImportKindCommonJS { + declaration = ct.NodeFactory.NewImportEqualsDeclaration( + /*modifiers*/ nil, + shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), + ct.NodeFactory.NewIdentifier(namespaceLikeImport.name), + ct.NodeFactory.NewExternalModuleReference(moduleSpecifierStringLiteral), + ) + } else { + declaration = ct.NodeFactory.NewImportDeclaration( + /*modifiers*/ nil, + ct.NodeFactory.NewImportClause( + shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), + /*name*/ nil, + ct.NodeFactory.NewNamespaceImport(ct.NodeFactory.NewIdentifier(namespaceLikeImport.name)), + ), + moduleSpecifierStringLiteral, + /*attributes*/ nil, + ) + } + statements = append(statements, declaration) + } + if len(statements) == 0 { + panic("No statements to insert for new imports") + } + return statements +} + +func needsTypeOnly(addAsTypeOnly AddAsTypeOnly) bool { + return addAsTypeOnly == AddAsTypeOnlyRequired +} + +func shouldUseTypeOnly(addAsTypeOnly AddAsTypeOnly, preferences *UserPreferences) bool { + return needsTypeOnly(addAsTypeOnly) || addAsTypeOnly != AddAsTypeOnlyNotAllowed && preferences.PreferTypeOnlyAutoImports != nil && *preferences.PreferTypeOnlyAutoImports +} diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go new file mode 100644 index 0000000000..49128a1895 --- /dev/null +++ b/internal/ls/autoimports.go @@ -0,0 +1,1489 @@ +package ls + +import ( + "context" + "fmt" + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/binder" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/compiler" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/debug" + "github.com/microsoft/typescript-go/internal/diagnostics" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/modulespecifiers" + "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type SymbolExportInfo struct { + symbol *ast.Symbol + moduleSymbol *ast.Symbol + moduleFileName string + exportKind ExportKind + targetFlags ast.SymbolFlags + isFromPackageJson bool +} + +type symbolExportEntry struct { + symbol *ast.Symbol + moduleSymbol *ast.Symbol +} + +type ExportInfoMapKey struct { + SymbolName string + SymbolId ast.SymbolId + AmbientModuleName string + ModuleFile tspath.Path +} + +func newExportInfoMapKey(importedName string, symbol *ast.Symbol, ambientModuleNameKey string, ch *checker.Checker) ExportInfoMapKey { + return ExportInfoMapKey{ + SymbolName: importedName, + SymbolId: ast.GetSymbolId(ch.SkipAlias(symbol)), + AmbientModuleName: ambientModuleNameKey, + } +} + +type CachedSymbolExportInfo struct { + // Used to rehydrate `symbol` and `moduleSymbol` when transient + id int + symbolTableKey string + symbolName string + capitalizedSymbolName string + moduleName string + moduleFile *ast.SourceFile // may be nil + packageName string + + symbol *ast.Symbol // may be nil + moduleSymbol *ast.Symbol // may be nil + moduleFileName string // may be "" + targetFlags ast.SymbolFlags + exportKind ExportKind + isFromPackageJson bool +} + +type exportInfoMap struct { + exportInfo collections.MultiMap[ExportInfoMapKey, CachedSymbolExportInfo] + symbols map[int]symbolExportEntry + exportInfoId int + usableByFileName tspath.Path + packages map[string]string + + globalTypingsCacheLocation string + + // !!! releaseSymbols func() + // !!! onFileChanged func(oldSourceFile *ast.SourceFile, newSourceFile *ast.SourceFile, typeAcquisitionEnabled bool) bool +} + +func (e *exportInfoMap) clear() { + e.symbols = map[int]symbolExportEntry{} + e.exportInfo = collections.MultiMap[ExportInfoMapKey, CachedSymbolExportInfo]{} + e.usableByFileName = "" +} + +func (e *exportInfoMap) get(importingFile tspath.Path, ch *checker.Checker, key ExportInfoMapKey) []*SymbolExportInfo { + if e.usableByFileName != importingFile { + return nil + } + return core.Map(e.exportInfo.Get(key), func(info CachedSymbolExportInfo) *SymbolExportInfo { return e.rehydrateCachedInfo(ch, info) }) +} + +func (e *exportInfoMap) add( + importingFile tspath.Path, + symbol *ast.Symbol, + symbolTableKey string, + moduleSymbol *ast.Symbol, + moduleFile *ast.SourceFile, + exportKind ExportKind, + isFromPackageJson bool, + ch *checker.Checker, + symbolNameMatch func(string) bool, + flagMatch func(ast.SymbolFlags) bool, +) { + if importingFile != e.usableByFileName { + e.clear() + e.usableByFileName = importingFile + } + + packageName := "" + if moduleFile != nil { + if nodeModulesPathParts := modulespecifiers.GetNodeModulePathParts(moduleFile.FileName()); nodeModulesPathParts != nil { + topLevelNodeModulesIndex := nodeModulesPathParts.TopLevelNodeModulesIndex + topLevelPackageNameIndex := nodeModulesPathParts.TopLevelPackageNameIndex + packageRootIndex := nodeModulesPathParts.PackageRootIndex + packageName = module.UnmangleScopedPackageName(modulespecifiers.GetPackageNameFromTypesPackageName(moduleFile.FileName()[topLevelPackageNameIndex+1 : packageRootIndex])) + if strings.HasPrefix(string(importingFile), string(moduleFile.Path())[0:topLevelNodeModulesIndex]) { + nodeModulesPath := moduleFile.FileName()[0 : topLevelPackageNameIndex+1] + if prevDeepestNodeModulesPath, ok := e.packages[packageName]; ok { + prevDeepestNodeModulesIndex := strings.Index(prevDeepestNodeModulesPath, "/node_modules/") + if topLevelNodeModulesIndex > prevDeepestNodeModulesIndex { + e.packages[packageName] = nodeModulesPath + } + } else { + e.packages[packageName] = nodeModulesPath + } + } + } + } + + isDefault := exportKind == ExportKindDefault + namedSymbol := symbol + if isDefault { + if s := binder.GetLocalSymbolForExportDefault(symbol); s != nil { + namedSymbol = s + } + } + // 1. A named export must be imported by its key in `moduleSymbol.exports` or `moduleSymbol.members`. + // 2. A re-export merged with an export from a module augmentation can result in `symbol` + // being an external module symbol; the name it is re-exported by will be `symbolTableKey` + // (which comes from the keys of `moduleSymbol.exports`.) + // 3. Otherwise, we have a default/namespace import that can be imported by any name, and + // `symbolTableKey` will be something undesirable like `export=` or `default`, so we try to + // get a better name. + names := []string{} + if exportKind == ExportKindNamed || checker.IsExternalModuleSymbol(namedSymbol) { + names = append(names, symbolTableKey) + } else { + names = getNamesForExportedSymbol(namedSymbol, ch, core.ScriptTargetNone) + } + + symbolName := names[0] + if symbolNameMatch != nil && !symbolNameMatch(symbolName) { + return + } + + capitalizedSymbolName := "" + if len(names) > 1 { + capitalizedSymbolName = names[1] + } + + moduleName := stringutil.StripQuotes(moduleSymbol.Name) + id := e.exportInfoId + 1 + target := ch.SkipAlias(symbol) + + if flagMatch != nil && !flagMatch(target.Flags) { + return + } + + var storedSymbol, storedModuleSymbol *ast.Symbol + + if symbol.Flags&ast.SymbolFlagsTransient == 0 { + storedSymbol = symbol + } + if moduleSymbol.Flags&ast.SymbolFlagsTransient == 0 { + storedModuleSymbol = moduleSymbol + } + + if storedSymbol == nil || storedModuleSymbol == nil { + e.symbols[id] = symbolExportEntry{storedSymbol, storedModuleSymbol} + } + + moduleKey := "" + if !tspath.IsExternalModuleNameRelative(moduleName) { + moduleKey = moduleName + } + + moduleFileName := "" + if moduleFile != nil { + moduleFileName = moduleFile.FileName() + } + e.exportInfo.Add(newExportInfoMapKey(symbolName, symbol, moduleKey, ch), CachedSymbolExportInfo{ + id: id, + symbolTableKey: symbolTableKey, + symbolName: symbolName, + capitalizedSymbolName: capitalizedSymbolName, + moduleName: moduleName, + moduleFile: moduleFile, + moduleFileName: moduleFileName, + packageName: packageName, + + symbol: storedSymbol, + moduleSymbol: storedModuleSymbol, + exportKind: exportKind, + targetFlags: target.Flags, + isFromPackageJson: isFromPackageJson, + }) +} + +func (e *exportInfoMap) search( + ch *checker.Checker, + importingFile tspath.Path, + preferCapitalized bool, + matches func(name string, targetFlags ast.SymbolFlags) bool, + action func(info []*SymbolExportInfo, symbolName string, isFromAmbientModule bool, key ExportInfoMapKey) []*SymbolExportInfo, +) []*SymbolExportInfo { + if importingFile != e.usableByFileName { + return nil + } + for key, info := range e.exportInfo.M { + symbolName, ambientModuleName := key.SymbolName, key.AmbientModuleName + if preferCapitalized && info[0].capitalizedSymbolName != "" { + symbolName = info[0].capitalizedSymbolName + } + if matches(symbolName, info[0].targetFlags) { + rehydrated := core.Map(info, func(info CachedSymbolExportInfo) *SymbolExportInfo { + return e.rehydrateCachedInfo(ch, info) + }) + filtered := core.FilterIndex(rehydrated, func(r *SymbolExportInfo, i int, _ []*SymbolExportInfo) bool { + return e.isNotShadowedByDeeperNodeModulesPackage(r, info[i].packageName) + }) + if len(filtered) > 0 { + if res := action(filtered, symbolName, ambientModuleName != "", key); res != nil { + return res + } + } + } + } + return nil +} + +func (e *exportInfoMap) isNotShadowedByDeeperNodeModulesPackage(info *SymbolExportInfo, packageName string) bool { + if packageName == "" || info.moduleFileName == "" { + return true + } + if e.globalTypingsCacheLocation != "" && strings.HasPrefix(info.moduleFileName, e.globalTypingsCacheLocation) { + return true + } + packageDeepestNodeModulesPath, ok := e.packages[packageName] + return !ok || strings.HasPrefix(info.moduleFileName, packageDeepestNodeModulesPath) +} + +func (e *exportInfoMap) rehydrateCachedInfo(ch *checker.Checker, info CachedSymbolExportInfo) *SymbolExportInfo { + if info.symbol != nil && info.moduleSymbol != nil { + return &SymbolExportInfo{ + symbol: info.symbol, + moduleSymbol: info.moduleSymbol, + moduleFileName: info.moduleFileName, + exportKind: info.exportKind, + targetFlags: info.targetFlags, + isFromPackageJson: info.isFromPackageJson, + } + } + cached := e.symbols[info.id] + cachedSymbol, cachedModuleSymbol := cached.symbol, cached.moduleSymbol + if cachedSymbol != nil && cachedModuleSymbol != nil { + return &SymbolExportInfo{ + symbol: cachedSymbol, + moduleSymbol: cachedModuleSymbol, + moduleFileName: info.moduleFileName, + exportKind: info.exportKind, + targetFlags: info.targetFlags, + isFromPackageJson: info.isFromPackageJson, + } + } + + moduleSymbol := core.Coalesce(info.moduleSymbol, cachedModuleSymbol) + if moduleSymbol == nil { + if info.moduleFile != nil { + moduleSymbol = ch.GetMergedSymbol(info.moduleFile.Symbol) + } else { + moduleSymbol = ch.TryFindAmbientModule(info.moduleName) + } + } + if moduleSymbol == nil { + panic(fmt.Sprintf("Could not find module symbol for %s in exportInfoMap", info.moduleName)) + } + symbol := core.Coalesce(info.symbol, cachedSymbol) + if symbol == nil { + if info.exportKind == ExportKindExportEquals { + symbol = ch.ResolveExternalModuleSymbol(moduleSymbol) + } else { + symbol = ch.TryGetMemberInModuleExportsAndProperties(info.symbolTableKey, moduleSymbol) + } + } + + if symbol == nil { + panic(fmt.Sprintf("Could not find symbol '%s' by key '%s' in module %s", info.symbolName, info.symbolTableKey, moduleSymbol.Name)) + } + e.symbols[info.id] = symbolExportEntry{symbol, moduleSymbol} + return &SymbolExportInfo{ + symbol, + moduleSymbol, + info.moduleFileName, + info.exportKind, + info.targetFlags, + info.isFromPackageJson, + } +} + +func getNamesForExportedSymbol(defaultExport *ast.Symbol, ch *checker.Checker, scriptTarget core.ScriptTarget) []string { + var names []string + forEachNameOfDefaultExport(defaultExport, ch, scriptTarget, func(name, capitalizedName string) string { + if capitalizedName != "" { + names = []string{name, capitalizedName} + } else { + names = []string{name} + } + return name + }) + return names +} + +type packageJsonImportFilter struct { + allowsImportingAmbientModule func(moduleSymbol *ast.Symbol, host modulespecifiers.ModuleSpecifierGenerationHost) bool + getSourceFileInfo func(sourceFile *ast.SourceFile, host modulespecifiers.ModuleSpecifierGenerationHost) packageJsonFilterResult + /** + * Use for a specific module specifier that has already been resolved. + * Use `allowsImportingAmbientModule` or `allowsImportingSourceFile` to resolve + * the best module specifier for a given module _and_ determine if it's importable. + */ + allowsImportingSpecifier func(moduleSpecifier string) bool +} + +type packageJsonFilterResult struct { + importable bool + packageName string +} +type projectPackageJsonInfo struct { + fileName string + parseable bool + dependencies map[string]string + devDependencies map[string]string + peerDependencies map[string]string + optionalDependencies map[string]string +} + +func (info *projectPackageJsonInfo) has(dependencyName string) bool { + if _, ok := info.dependencies[dependencyName]; ok { + return true + } + if _, ok := info.devDependencies[dependencyName]; ok { + return true + } + + if _, ok := info.peerDependencies[dependencyName]; ok { + return true + } + if _, ok := info.optionalDependencies[dependencyName]; ok { + return true + } + + return false +} + +func (l *LanguageService) getImportCompletionAction( + ctx context.Context, + ch *checker.Checker, + targetSymbol *ast.Symbol, + moduleSymbol *ast.Symbol, + sourceFile *ast.SourceFile, + position int, + exportMapKey ExportInfoMapKey, + symbolName string, // !!! needs *string ? + isJsxTagName bool, + // formatContext *formattingContext, + preferences *UserPreferences, +) (string, codeAction) { + var exportInfos []*SymbolExportInfo + // `exportMapKey` should be in the `itemData` of each auto-import completion entry and sent in resolving completion entry requests + exportInfos = l.getExportInfos(ctx, ch, sourceFile, preferences, exportMapKey) + if len(exportInfos) == 0 { + panic("Some exportInfo should match the specified exportMapKey") + } + + isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(astnav.GetTokenAtPosition(sourceFile, position)) + fix := l.getImportFixForSymbol(ch, sourceFile, exportInfos, position, ptrTo(isValidTypeOnlyUseSite), preferences) + if fix == nil { + lineAndChar := l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(position)) + panic(fmt.Sprintf("expected importFix at %s: (%v,%v)", sourceFile.FileName(), lineAndChar.Line, lineAndChar.Character)) + } + return fix.moduleSpecifier, l.codeActionForFix(ctx, sourceFile, symbolName, fix /*includeSymbolNameInDescription*/, false, preferences) +} + +func NewExportInfoMap(globalsTypingCacheLocation string) *exportInfoMap { + return &exportInfoMap{ + packages: map[string]string{}, + symbols: map[int]symbolExportEntry{}, + exportInfo: collections.MultiMap[ExportInfoMapKey, CachedSymbolExportInfo]{}, + globalTypingsCacheLocation: globalsTypingCacheLocation, + } +} + +func (l *LanguageService) isImportable( + fromFile *ast.SourceFile, + toFile *ast.SourceFile, + toModule *ast.Symbol, + preferences *UserPreferences, + packageJsonFilter *packageJsonImportFilter, + // moduleSpecifierResolutionHost ModuleSpecifierResolutionHost, + // moduleSpecifierCache ModuleSpecifierCache, +) bool { + // !!! moduleSpecifierResolutionHost := l.GetModuleSpecifierResolutionHost() + moduleSpecifierResolutionHost := l.GetProgram() + + // Ambient module + if toFile == nil { + moduleName := stringutil.StripQuotes(toModule.Name) + if _, ok := core.NodeCoreModules()[moduleName]; ok { + if useNodePrefix := shouldUseUriStyleNodeCoreModules(fromFile, l.GetProgram()); useNodePrefix { + return useNodePrefix == strings.HasPrefix(moduleName, "node:") + } + } + return packageJsonFilter == nil || + packageJsonFilter.allowsImportingAmbientModule(toModule, moduleSpecifierResolutionHost) || + fileContainsPackageImport(fromFile, moduleName) + } + + if fromFile == toFile { + return false + } + + // !!! moduleSpecifierCache + // cachedResult := moduleSpecifierCache?.get(fromFile.path, toFile.path, preferences, {}) + // if cachedResult?.isBlockedByPackageJsonDependencies != nil { + // return !cachedResult.isBlockedByPackageJsonDependencies || cachedResult.packageName != nil && fileContainsPackageImport(fromFile, cachedResult.packageName) + // } + + fromPath := fromFile.FileName() + useCaseSensitiveFileNames := moduleSpecifierResolutionHost.UseCaseSensitiveFileNames() + globalTypingsCache := l.GetProgram().GetGlobalTypingsCacheLocation() + modulePaths := modulespecifiers.GetEachFileNameOfModule( + fromPath, + toFile.FileName(), + moduleSpecifierResolutionHost, + /*preferSymlinks*/ false, + ) + hasImportablePath := false + for _, module := range modulePaths { + file := l.GetProgram().GetSourceFile(module.FileName) + + // Determine to import using toPath only if toPath is what we were looking at + // or there doesnt exist the file in the program by the symlink + if file == nil || file != toFile { + continue + } + + // If it's in a `node_modules` but is not reachable from here via a global import, don't bother. + toNodeModules := tspath.ForEachAncestorDirectoryStoppingAtGlobalCache( + globalTypingsCache, + module.FileName, + func(ancestor string) (string, bool) { + if tspath.GetBaseFileName(ancestor) == "node_modules" { + return ancestor, true + } else { + return "", false + } + }, + ) + toNodeModulesParent := "" + if toNodeModules != "" { + toNodeModulesParent = tspath.GetDirectoryPath(tspath.GetCanonicalFileName(toNodeModules, useCaseSensitiveFileNames)) + } + hasImportablePath = toNodeModulesParent != "" || + strings.HasPrefix(tspath.GetCanonicalFileName(fromPath, useCaseSensitiveFileNames), toNodeModulesParent) || + (globalTypingsCache != "" && strings.HasPrefix(tspath.GetCanonicalFileName(globalTypingsCache, useCaseSensitiveFileNames), toNodeModulesParent)) + if hasImportablePath { + break + } + } + + if packageJsonFilter != nil { + if hasImportablePath { + importInfo := packageJsonFilter.getSourceFileInfo(toFile, moduleSpecifierResolutionHost) + // moduleSpecifierCache?.setBlockedByPackageJsonDependencies(fromFile.path, toFile.path, preferences, {}, importInfo?.packageName, !importInfo?.importable) + return importInfo.importable || hasImportablePath && importInfo.packageName != "" && fileContainsPackageImport(fromFile, importInfo.packageName) + } + return false + } + + return hasImportablePath +} + +func fileContainsPackageImport(sourceFile *ast.SourceFile, packageName string) bool { + return core.Some(sourceFile.Imports(), func(i *ast.Node) bool { + text := i.Text() + return text == packageName || strings.HasPrefix(text, packageName+"/") + }) +} + +func isImportableSymbol(symbol *ast.Symbol, ch *checker.Checker) bool { + return !ch.IsUndefinedSymbol(symbol) && !ch.IsUnknownSymbol(symbol) && !checker.IsKnownSymbol(symbol) // !!! && !checker.IsPrivateIdentifierSymbol(symbol); +} + +func getDefaultLikeExportInfo(moduleSymbol *ast.Symbol, ch *checker.Checker) *ExportInfo { + exportEquals := ch.ResolveExternalModuleSymbol(moduleSymbol) + if exportEquals != moduleSymbol { + if defaultExport := ch.TryGetMemberInModuleExports(ast.InternalSymbolNameDefault, exportEquals); defaultExport != nil { + return &ExportInfo{defaultExport, ExportKindDefault} + } + return &ExportInfo{exportEquals, ExportKindExportEquals} + } + if defaultExport := ch.TryGetMemberInModuleExports(ast.InternalSymbolNameDefault, moduleSymbol); defaultExport != nil { + return &ExportInfo{defaultExport, ExportKindDefault} + } + return nil +} + +type importSpecifierResolverForCompletions struct { + *ast.SourceFile // importingFile + *UserPreferences + l *LanguageService + filter *packageJsonImportFilter +} + +func (r *importSpecifierResolverForCompletions) packageJsonImportFilter() *packageJsonImportFilter { + if r.filter == nil { + r.filter = r.l.createPackageJsonImportFilter(r.SourceFile, *r.UserPreferences) + } + return r.filter +} + +func (i *importSpecifierResolverForCompletions) getModuleSpecifierForBestExportInfo( + ch *checker.Checker, + exportInfo []*SymbolExportInfo, + position int, + isValidTypeOnlyUseSite bool, +) *ImportFix { + // !!! caching + // used in completions, usually calculated once per `getCompletionData` call + var userPreferences UserPreferences + if i.UserPreferences == nil { + userPreferences = UserPreferences{} + } else { + userPreferences = *i.UserPreferences + } + packageJsonImportFilter := i.packageJsonImportFilter() + _, fixes := i.l.getImportFixes(ch, exportInfo, ptrTo(i.l.converters.PositionToLineAndCharacter(i.SourceFile, core.TextPos(position))), ptrTo(isValidTypeOnlyUseSite), ptrTo(false), i.SourceFile, userPreferences, false /* fromCacheOnly */) + return i.l.getBestFix(fixes, i.SourceFile, packageJsonImportFilter.allowsImportingSpecifier, userPreferences) +} + +func (l *LanguageService) getImportFixForSymbol( + ch *checker.Checker, + sourceFile *ast.SourceFile, + exportInfos []*SymbolExportInfo, + position int, + isValidTypeOnlySite *bool, + preferences *UserPreferences, +) *ImportFix { + var userPreferences UserPreferences + if preferences != nil { + userPreferences = *preferences + } + + if isValidTypeOnlySite == nil { + isValidTypeOnlySite = ptrTo(ast.IsValidTypeOnlyAliasUseSite(astnav.GetTokenAtPosition(sourceFile, position))) + } + useRequire := getShouldUseRequire(sourceFile, l.GetProgram()) + packageJsonImportFilter := l.createPackageJsonImportFilter(sourceFile, userPreferences) + _, fixes := l.getImportFixes(ch, exportInfos, ptrTo(l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(position))), isValidTypeOnlySite, &useRequire, sourceFile, userPreferences, false /* fromCacheOnly */) + return l.getBestFix(fixes, sourceFile, packageJsonImportFilter.allowsImportingSpecifier, userPreferences) +} + +func (l *LanguageService) getBestFix(fixes []*ImportFix, sourceFile *ast.SourceFile, allowsImportingSpecifier func(moduleSpecifier string) bool, preferences UserPreferences) *ImportFix { + if len(fixes) == 0 { + return nil + } + + // These will always be placed first if available, and are better than other kinds + if fixes[0].kind == ImportFixKindUseNamespace || fixes[0].kind == ImportFixKindAddToExisting { + return fixes[0] + } + + best := fixes[0] + for _, fix := range fixes { + // Takes true branch of conditional if `fix` is better than `best` + if compareModuleSpecifiers( + fix, + best, + sourceFile, + l.GetProgram(), + preferences, + allowsImportingSpecifier, + func(fileName string) tspath.Path { + return tspath.ToPath(fileName, l.GetProgram().GetCurrentDirectory(), l.GetProgram().UseCaseSensitiveFileNames()) + }, + ) < 0 { + best = fix + } + } + + return best +} + +func (l *LanguageService) getImportFixes( + ch *checker.Checker, + exportInfos []*SymbolExportInfo, // | FutureSymbolExportInfo[], + usagePosition *lsproto.Position, + isValidTypeOnlyUseSite *bool, + useRequire *bool, + sourceFile *ast.SourceFile, // | FutureSourceFile, + preferences UserPreferences, + // importMap *importMap, + fromCacheOnly bool, +) (int, []*ImportFix) { + // if importMap == nil { && !!! isFullSourceFile(sourceFile) + importMap := createExistingImportMap(sourceFile, l.GetProgram(), ch) + var existingImports []*FixAddToExistingImportInfo + if importMap != nil { + existingImports = core.FlatMap(exportInfos, importMap.getImportsForExportInfo) + } + var useNamespace []*ImportFix + if usagePosition != nil { + if namespaceImport := tryUseExistingNamespaceImport(existingImports, *usagePosition); namespaceImport != nil { + useNamespace = append(useNamespace, namespaceImport) + } + } + if addToExisting := tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, ch, l.GetProgram().Options()); addToExisting != nil { + // Don't bother providing an action to add a new import if we can add to an existing one. + return 0, append(useNamespace, addToExisting) + } + + result := l.getFixesForAddImport( + ch, + exportInfos, + existingImports, + sourceFile, + usagePosition, + *isValidTypeOnlyUseSite, + *useRequire, + preferences, + fromCacheOnly, + ) + computedWithoutCacheCount := 0 + // if result.computedWithoutCacheCount != nil { + // computedWithoutCacheCount = *result.computedWithoutCacheCount + // } + return computedWithoutCacheCount, append(useNamespace, result...) +} + +func (l *LanguageService) createPackageJsonImportFilter(fromFile *ast.SourceFile, preferences UserPreferences) *packageJsonImportFilter { + packageJsons := []*projectPackageJsonInfo{} + // packageJsons := ( + // (host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host) + // ).filter(p => p.parseable); + + var usesNodeCoreModules *bool + ambientModuleCache := map[*ast.Symbol]bool{} + sourceFileCache := map[*ast.SourceFile]packageJsonFilterResult{} + + getNodeModuleRootSpecifier := func(fullSpecifier string) string { + components := tspath.GetPathComponents(modulespecifiers.GetPackageNameFromTypesPackageName(fullSpecifier), "")[1:] + // Scoped packages + if strings.HasPrefix(components[0], "@") { + return fmt.Sprintf("%s/%s", components[0], components[1]) + } + return components[0] + } + + moduleSpecifierIsCoveredByPackageJson := func(specifier string) bool { + packageName := getNodeModuleRootSpecifier(specifier) + for _, packageJson := range packageJsons { + if packageJson.has(packageName) || packageJson.has(module.GetTypesPackageName(packageName)) { + return true + } + } + return false + } + + isAllowedCoreNodeModulesImport := func(moduleSpecifier string) bool { + // If we're in JavaScript, it can be difficult to tell whether the user wants to import + // from Node core modules or not. We can start by seeing if the user is actually using + // any node core modules, as opposed to simply having @types/node accidentally as a + // dependency of a dependency. + if /*isFullSourceFile(fromFile) &&*/ ast.IsSourceFileJS(fromFile) && core.NodeCoreModules()[moduleSpecifier] { + if usesNodeCoreModules == nil { + usesNodeCoreModules = ptrTo(consumesNodeCoreModules(fromFile)) + } + if *usesNodeCoreModules { + return true + } + } + return false + } + + getNodeModulesPackageNameFromFileName := func(importedFileName string, moduleSpecifierResolutionHost modulespecifiers.ModuleSpecifierGenerationHost) *string { + if !strings.Contains(importedFileName, "node_modules") { + return nil + } + specifier := modulespecifiers.GetNodeModulesPackageName( + l.host.GetProgram().Options(), + fromFile, + importedFileName, + moduleSpecifierResolutionHost, + preferences.ModuleSpecifierPreferences(), + modulespecifiers.ModuleSpecifierOptions{}, + ) + if specifier == "" { + return nil + } + // Paths here are not node_modules, so we don't care about them; + // returning anything will trigger a lookup in package.json. + if !tspath.PathIsRelative(specifier) && !tspath.IsRootedDiskPath(specifier) { + return ptrTo(getNodeModuleRootSpecifier(specifier)) + } + return nil + } + + allowsImportingAmbientModule := func(moduleSymbol *ast.Symbol, moduleSpecifierResolutionHost modulespecifiers.ModuleSpecifierGenerationHost) bool { + if len(packageJsons) > 0 || moduleSymbol.ValueDeclaration == nil { + return true + } + + if cached, ok := ambientModuleCache[moduleSymbol]; ok { + return cached + } + + declaredModuleSpecifier := stringutil.StripQuotes(moduleSymbol.Name) + if isAllowedCoreNodeModulesImport(declaredModuleSpecifier) { + ambientModuleCache[moduleSymbol] = true + return true + } + + declaringSourceFile := ast.GetSourceFileOfNode(moduleSymbol.ValueDeclaration) + declaringNodeModuleName := getNodeModulesPackageNameFromFileName(declaringSourceFile.FileName(), moduleSpecifierResolutionHost) + if declaringNodeModuleName == nil { + ambientModuleCache[moduleSymbol] = true + return true + } + + result := moduleSpecifierIsCoveredByPackageJson(*declaringNodeModuleName) + if !result { + result = moduleSpecifierIsCoveredByPackageJson(declaredModuleSpecifier) + } + ambientModuleCache[moduleSymbol] = result + return result + } + + getSourceFileInfo := func(sourceFile *ast.SourceFile, moduleSpecifierResolutionHost modulespecifiers.ModuleSpecifierGenerationHost) packageJsonFilterResult { + result := packageJsonFilterResult{ + importable: true, + packageName: "", + } + + if len(packageJsons) == 0 { + return result + } + if cached, ok := sourceFileCache[sourceFile]; ok { + return cached + } + + if packageName := getNodeModulesPackageNameFromFileName(sourceFile.FileName(), moduleSpecifierResolutionHost); packageName != nil { + result = packageJsonFilterResult{importable: moduleSpecifierIsCoveredByPackageJson(*packageName), packageName: *packageName} + } + sourceFileCache[sourceFile] = result + return result + } + + allowsImportingSpecifier := func(moduleSpecifier string) bool { + if len(packageJsons) == 0 || isAllowedCoreNodeModulesImport(moduleSpecifier) { + return true + } + if tspath.PathIsRelative(moduleSpecifier) || tspath.IsRootedDiskPath(moduleSpecifier) { + return true + } + return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier) + } + + return &packageJsonImportFilter{ + allowsImportingAmbientModule, + getSourceFileInfo, + allowsImportingSpecifier, + } +} + +func consumesNodeCoreModules(sourceFile *ast.SourceFile) bool { + for _, importStatement := range sourceFile.Imports() { + if core.NodeCoreModules()[importStatement.Text()] { + return true + } + } + return false +} + +func createExistingImportMap(importingFile *ast.SourceFile, program *compiler.Program, ch *checker.Checker) *importMap { + m := collections.MultiMap[ast.SymbolId, *ast.Statement]{} + for _, moduleSpecifier := range importingFile.Imports() { + i := tryGetImportFromModuleSpecifier(moduleSpecifier) + if i == nil { + panic("error: did not expect node kind " + moduleSpecifier.Kind.String()) + } else if ast.IsVariableDeclarationInitializedToRequire(i.Parent) { + if moduleSymbol := ch.ResolveExternalModuleName(moduleSpecifier); moduleSymbol != nil { + m.Add(ast.GetSymbolId(moduleSymbol), i.Parent) + } + } else if i.Kind == ast.KindImportDeclaration || i.Kind == ast.KindImportEqualsDeclaration || i.Kind == ast.KindJSDocImportTag { + if moduleSymbol := ch.GetSymbolAtLocation(moduleSpecifier); moduleSymbol != nil { + m.Add(ast.GetSymbolId(moduleSymbol), i) + } + } + } + return &importMap{importingFile: importingFile, program: program, m: m} +} + +type importMap struct { + importingFile *ast.SourceFile + program *compiler.Program + m collections.MultiMap[ast.SymbolId, *ast.Statement] // !!! anyImportOrRequire +} + +func (i *importMap) getImportsForExportInfo(info *SymbolExportInfo /* | FutureSymbolExportInfo*/) []*FixAddToExistingImportInfo { + matchingDeclarations := i.m.Get(ast.GetSymbolId(info.moduleSymbol)) + if len(matchingDeclarations) == 0 { + return nil + } + + // Can't use an es6 import for a type in JS. + if ast.IsSourceFileJS(i.importingFile) && info.targetFlags&ast.SymbolFlagsValue == 0 && !core.Every(matchingDeclarations, ast.IsJSDocImportTag) { + return nil + } + + importKind := getImportKind(i.importingFile, info.exportKind, i.program, false) + return core.Map(matchingDeclarations, func(d *ast.Statement) *FixAddToExistingImportInfo { + return &FixAddToExistingImportInfo{declaration: d, importKind: importKind, symbol: info.symbol, targetFlags: info.targetFlags} + }) +} + +func tryUseExistingNamespaceImport(existingImports []*FixAddToExistingImportInfo, position lsproto.Position) *ImportFix { + // It is possible that multiple import statements with the same specifier exist in the file. + // e.g. + // + // import * as ns from "foo"; + // import { member1, member2 } from "foo"; + // + // member3/**/ <-- cusor here + // + // in this case we should provie 2 actions: + // 1. change "member3" to "ns.member3" + // 2. add "member3" to the second import statement's import list + // and it is up to the user to decide which one fits best. + for _, existingImport := range existingImports { + if existingImport.importKind != ImportKindNamed { + continue + } + var namespacePrefix string + declaration := existingImport.declaration + switch declaration.Kind { + case ast.KindVariableDeclaration, ast.KindImportEqualsDeclaration: + if declaration.Kind == ast.KindVariableDeclaration && declaration.Name().Kind != ast.KindIdentifier { + continue + } + namespacePrefix = declaration.Name().Text() + case ast.KindJSDocImportTag, ast.KindImportDeclaration: + importClause := ast.GetImportClauseOfDeclaration(declaration) + if importClause == nil || importClause.NamedBindings.Kind != ast.KindNamespaceImport { + continue + } + namespacePrefix = importClause.NamedBindings.Name().Text() + default: + debug.AssertNever(declaration) + } + if namespacePrefix == "" { + continue + } + moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(declaration) + if moduleSpecifier != nil && moduleSpecifier.Text() != "" { + return getUseNamespaceImport( + moduleSpecifier.Text(), + modulespecifiers.ResultKindNone, + namespacePrefix, + position, + ) + } + } + return nil +} + +func tryAddToExistingImport(existingImports []*FixAddToExistingImportInfo, isValidTypeOnlyUseSite *bool, ch *checker.Checker, compilerOptions *core.CompilerOptions) *ImportFix { + var best *ImportFix + + typeOnly := false + if isValidTypeOnlyUseSite != nil { + typeOnly = *isValidTypeOnlyUseSite + } + + for _, existingImport := range existingImports { + fix := existingImport.getAddToExistingImportFix(typeOnly, ch, compilerOptions) + if fix == nil { + continue + } + isTypeOnly := ast.IsTypeOnlyImportDeclaration(fix.importClauseOrBindingPattern) + if (fix.addAsTypeOnly != AddAsTypeOnlyNotAllowed && isTypeOnly) || (fix.addAsTypeOnly == AddAsTypeOnlyNotAllowed && !isTypeOnly) { + // Give preference to putting types in existing type-only imports and avoiding conversions + // of import statements to/from type-only. + return fix + } + if best == nil { + best = fix + } + } + return best +} + +func (info *FixAddToExistingImportInfo) getAddToExistingImportFix(isValidTypeOnlyUseSite bool, ch *checker.Checker, compilerOptions *core.CompilerOptions) *ImportFix { + if info.importKind == ImportKindCommonJS || info.importKind == ImportKindNamespace || info.declaration.Kind == ast.KindImportEqualsDeclaration { + // These kinds of imports are not combinable with anything + return nil + } + + if info.declaration.Kind == ast.KindVariableDeclaration { + if (info.importKind == ImportKindNamed || info.importKind == ImportKindDefault) && info.declaration.Name().Kind == ast.KindObjectBindingPattern { + return getAddToExistingImport( + info.declaration.Name(), + info.importKind, + info.declaration.Initializer().Arguments()[0].Text(), + modulespecifiers.ResultKindNone, + AddAsTypeOnlyNotAllowed, + ) + } + return nil + } + + importClause := ast.GetImportClauseOfDeclaration(info.declaration) + if importClause == nil || !ast.IsStringLiteralLike(info.declaration.ModuleSpecifier()) { + return nil + } + namedBindings := importClause.NamedBindings + // A type-only import may not have both a default and named imports, so the only way a name can + // be added to an existing type-only import is adding a named import to existing named bindings. + if importClause.IsTypeOnly && !(info.importKind == ImportKindNamed && namedBindings != nil) { + return nil + } + + // N.B. we don't have to figure out whether to use the main program checker + // or the AutoImportProvider checker because we're adding to an existing import; the existence of + // the import guarantees the symbol came from the main program. + addAsTypeOnly := getAddAsTypeOnly(isValidTypeOnlyUseSite, info.symbol, info.targetFlags, ch, compilerOptions) + + if info.importKind == ImportKindDefault && (importClause.Name() != nil || // Cannot add a default import to a declaration that already has one + addAsTypeOnly == AddAsTypeOnlyRequired && namedBindings != nil) { // Cannot add a default import as type-only if the import already has named bindings + + return nil + } + + // Cannot add a named import to a declaration that has a namespace import + if info.importKind == ImportKindNamed && namedBindings != nil && namedBindings.Kind == ast.KindNamespaceImport { + return nil + } + + return getAddToExistingImport( + importClause.AsNode(), + info.importKind, + info.declaration.ModuleSpecifier().Text(), + modulespecifiers.ResultKindNone, + addAsTypeOnly, + ) +} + +func (l *LanguageService) getFixesForAddImport( + ch *checker.Checker, + exportInfos []*SymbolExportInfo, // !!! | readonly FutureSymbolExportInfo[], + existingImports []*FixAddToExistingImportInfo, + sourceFile *ast.SourceFile, // !!! | FutureSourceFile, + usagePosition *lsproto.Position, + isValidTypeOnlyUseSite bool, + useRequire bool, + preferences UserPreferences, + fromCacheOnly bool, +) []*ImportFix { + // tries to create a new import statement using an existing import specifier + var importWithExistingSpecifier *ImportFix + + for _, existingImport := range existingImports { + if fix := existingImport.getNewImportFromExistingSpecifier(isValidTypeOnlyUseSite, useRequire, ch, l.GetProgram().Options()); fix != nil { + importWithExistingSpecifier = fix + break + } + } + + if importWithExistingSpecifier != nil { + return []*ImportFix{importWithExistingSpecifier} + } + + return l.getNewImportFixes(ch, sourceFile, usagePosition, isValidTypeOnlyUseSite, useRequire, exportInfos, preferences, fromCacheOnly) +} + +func (l *LanguageService) getNewImportFixes( + ch *checker.Checker, + sourceFile *ast.SourceFile, // | FutureSourceFile, + usagePosition *lsproto.Position, + isValidTypeOnlyUseSite bool, + useRequire bool, + exportInfos []*SymbolExportInfo, // !!! (SymbolExportInfo | FutureSymbolExportInfo)[], + preferences UserPreferences, + fromCacheOnly bool, +) []*ImportFix /* FixAddNewImport | FixAddJsdocTypeImport */ { + isJs := tspath.HasJSFileExtension(sourceFile.FileName()) + compilerOptions := l.GetProgram().Options() + // !!! packagejsonAutoimportProvider + // getChecker := createGetChecker(program, host)// memoized typechecker based on `isFromPackageJson` bool + + getModuleSpecifiers := func(moduleSymbol *ast.Symbol, checker *checker.Checker) ([]string, modulespecifiers.ResultKind) { + return modulespecifiers.GetModuleSpecifiersWithInfo(moduleSymbol, checker, compilerOptions, sourceFile, l.GetProgram(), preferences.ModuleSpecifierPreferences(), modulespecifiers.ModuleSpecifierOptions{}, true /*forAutoImport*/) + } + // fromCacheOnly + // ? (exportInfo: SymbolExportInfo | FutureSymbolExportInfo) => moduleSpecifiers.tryGetModuleSpecifiersFromCache(exportInfo.moduleSymbol, sourceFile, moduleSpecifierResolutionHost, preferences) + // : (exportInfo: SymbolExportInfo | FutureSymbolExportInfo, checker: TypeChecker) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(exportInfo.moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences, /*options*/ nil, /*forAutoImport*/ true); + + // computedWithoutCacheCount = 0; + var fixes []*ImportFix /* FixAddNewImport | FixAddJsdocTypeImport */ + for i, exportInfo := range exportInfos { + moduleSpecifiers, moduleSpecifierKind := getModuleSpecifiers(exportInfo.moduleSymbol, ch) + importedSymbolHasValueMeaning := exportInfo.targetFlags&ast.SymbolFlagsValue != 0 + addAsTypeOnly := getAddAsTypeOnly(isValidTypeOnlyUseSite, exportInfo.symbol, exportInfo.targetFlags, ch, compilerOptions) + // computedWithoutCacheCount += computedWithoutCache ? 1 : 0; + for _, moduleSpecifier := range moduleSpecifiers { + if modulespecifiers.ContainsNodeModules(moduleSpecifier) { + continue + } + if !importedSymbolHasValueMeaning && isJs && usagePosition != nil { + // `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types. + fixes = append(fixes, getAddJsdocTypeImport( + moduleSpecifier, + moduleSpecifierKind, + usagePosition, + exportInfo, + ptrTo(i > 0)), // isReExport + ) + continue + } + importKind := getImportKind(sourceFile, exportInfo.exportKind, l.GetProgram(), false) + var qualification *Qualification + if usagePosition != nil && importKind == ImportKindCommonJS && exportInfo.exportKind == ExportKindNamed { + // Compiler options are restricting our import options to a require, but we need to access + // a named export or property of the exporting module. We need to import the entire module + // and insert a property access, e.g. `writeFile` becomes + // + // import fs = require("fs"); // or const in JS + // fs.writeFile + exportEquals := ch.ResolveExternalModuleSymbol(exportInfo.moduleSymbol) + var namespacePrefix *string + if exportEquals != exportInfo.moduleSymbol { + namespacePrefix = strPtrTo(forEachNameOfDefaultExport( + exportEquals, + ch, + compilerOptions.GetEmitScriptTarget(), + func(a, _ string) string { return a }, // Identity + )) + } + if namespacePrefix == nil { + namespacePrefix = ptrTo(moduleSymbolToValidIdentifier( + exportInfo.moduleSymbol, + compilerOptions.GetEmitScriptTarget(), + /*forceCapitalize*/ false, + )) + } + qualification = &Qualification{*usagePosition, *namespacePrefix} + } + fixes = append(fixes, getNewAddNewImport( + moduleSpecifier, + moduleSpecifierKind, + importKind, + useRequire, + addAsTypeOnly, + exportInfo, + ptrTo(i > 0), // isReExport + qualification, + )) + } + } + + return fixes +} + +func getAddAsTypeOnly( + isValidTypeOnlyUseSite bool, + symbol *ast.Symbol, + targetFlags ast.SymbolFlags, + ch *checker.Checker, + compilerOptions *core.CompilerOptions, +) AddAsTypeOnly { + if !isValidTypeOnlyUseSite { + // Can't use a type-only import if the usage is an emitting position + return AddAsTypeOnlyNotAllowed + } + if symbol != nil && compilerOptions.VerbatimModuleSyntax.IsTrue() && + (targetFlags&ast.SymbolFlagsValue == 0 || ch.GetTypeOnlyAliasDeclaration(symbol) != nil) { + // A type-only import is required for this symbol if under these settings if the symbol will + // be erased, which will happen if the target symbol is purely a type or if it was exported/imported + // as type-only already somewhere between this import and the target. + return AddAsTypeOnlyRequired + } + return AddAsTypeOnlyAllowed +} + +func getShouldUseRequire( + sourceFile *ast.SourceFile, // !!! | FutureSourceFile + program *compiler.Program, +) bool { + // 1. TypeScript files don't use require variable declarations + if !tspath.HasJSFileExtension(sourceFile.FileName()) { + return false + } + + // 2. If the current source file is unambiguously CJS or ESM, go with that + switch { + case sourceFile.CommonJSModuleIndicator != nil && sourceFile.ExternalModuleIndicator == nil: + return true + case sourceFile.ExternalModuleIndicator != nil && sourceFile.CommonJSModuleIndicator == nil: + return false + } + + // 3. If there's a tsconfig/jsconfig, use its module setting + if program.Options().ConfigFilePath != "" { + return program.Options().GetEmitModuleKind() < core.ModuleKindES2015 + } + + // 4. In --module nodenext, assume we're not emitting JS -> JS, so use + // whatever syntax Node expects based on the detected module kind + // TODO: consider removing `impliedNodeFormatForEmit` + switch program.GetImpliedNodeFormatForEmit(sourceFile) { + case core.ModuleKindCommonJS: + return true + case core.ModuleKindESNext: + return false + } + + // 5. Match the first other JS file in the program that's unambiguously CJS or ESM + for _, otherFile := range program.GetSourceFiles() { + switch { + case otherFile == sourceFile, !ast.IsSourceFileJS(otherFile), program.IsSourceFileFromExternalLibrary(otherFile): + continue + case otherFile.CommonJSModuleIndicator != nil && otherFile.ExternalModuleIndicator == nil: + return true + case otherFile.ExternalModuleIndicator != nil && otherFile.CommonJSModuleIndicator == nil: + return false + } + } + + // 6. Literally nothing to go on + return true +} + +/** + * @param forceImportKeyword Indicates that the user has already typed `import`, so the result must start with `import`. + * (In other words, do not allow `const x = require("...")` for JS files.) + * + * @internal + */ +func getImportKind(importingFile *ast.SourceFile /*| FutureSourceFile*/, exportKind ExportKind, program *compiler.Program, forceImportKeyword bool) ImportKind { + if program.Options().VerbatimModuleSyntax.IsTrue() && program.GetEmitModuleFormatOfFile(importingFile) == core.ModuleKindCommonJS { + // TODO: if the exporting file is ESM under nodenext, or `forceImport` is given in a JS file, this is impossible + return ImportKindCommonJS + } + switch exportKind { + case ExportKindNamed: + return ImportKindNamed + case ExportKindDefault: + return ImportKindDefault + case ExportKindExportEquals: + return getExportEqualsImportKind(importingFile, program.Options(), forceImportKeyword) + case ExportKindUMD: + return getUmdImportKind(importingFile, program, forceImportKeyword) + case ExportKindModule: + return ImportKindNamespace + } + panic("unexpected export kind: " + exportKind.String()) +} + +func getExportEqualsImportKind(importingFile *ast.SourceFile /* | FutureSourceFile*/, compilerOptions *core.CompilerOptions, forceImportKeyword bool) ImportKind { + allowSyntheticDefaults := compilerOptions.GetAllowSyntheticDefaultImports() + isJS := tspath.HasJSFileExtension(importingFile.FileName()) + // 1. 'import =' will not work in es2015+ TS files, so the decision is between a default + // and a namespace import, based on allowSyntheticDefaultImports/esModuleInterop. + if !isJS && compilerOptions.GetEmitModuleKind() >= core.ModuleKindES2015 { + if allowSyntheticDefaults { + return ImportKindDefault + } + return ImportKindNamespace + } + // 2. 'import =' will not work in JavaScript, so the decision is between a default import, + // a namespace import, and const/require. + if isJS { + if importingFile.ExternalModuleIndicator != nil || forceImportKeyword { + if allowSyntheticDefaults { + return ImportKindDefault + } + return ImportKindNamespace + } + return ImportKindCommonJS + } + // 3. At this point the most correct choice is probably 'import =', but people + // really hate that, so look to see if the importing file has any precedent + // on how to handle it. + for _, statement := range importingFile.Statements.Nodes { + // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration + if ast.IsImportEqualsDeclaration(statement) && !ast.NodeIsMissing(statement.AsImportEqualsDeclaration().ModuleReference) { + return ImportKindCommonJS + } + } + // 4. We have no precedent to go on, so just use a default import if + // allowSyntheticDefaultImports/esModuleInterop is enabled. + if allowSyntheticDefaults { + return ImportKindDefault + } + return ImportKindCommonJS +} + +func getUmdImportKind(importingFile *ast.SourceFile /* | FutureSourceFile */, program *compiler.Program, forceImportKeyword bool) ImportKind { + // Import a synthetic `default` if enabled. + if program.Options().GetAllowSyntheticDefaultImports() { + return ImportKindDefault + } + + // When a synthetic `default` is unavailable, use `import..require` if the module kind supports it. + moduleKind := program.Options().GetEmitModuleKind() + switch moduleKind { + case core.ModuleKindCommonJS: + if tspath.HasJSFileExtension(importingFile.FileName()) && (importingFile.ExternalModuleIndicator != nil || forceImportKeyword) { + return ImportKindNamespace + } + return ImportKindCommonJS + case core.ModuleKindES2015, core.ModuleKindES2020, core.ModuleKindES2022, core.ModuleKindESNext, core.ModuleKindNone, core.ModuleKindPreserve: + // Fall back to the `import * as ns` style import. + return ImportKindNamespace + case core.ModuleKindNode16, core.ModuleKindNode18, core.ModuleKindNodeNext: + if program.GetImpliedNodeFormatForEmit(importingFile) == core.ModuleKindESNext { + return ImportKindNamespace + } + return ImportKindCommonJS + default: + panic(`Unexpected moduleKind :` + moduleKind.String()) + } +} + +/** + * May call `cb` multiple times with the same name. + * Terminates when `cb` returns a truthy value. + */ +func forEachNameOfDefaultExport(defaultExport *ast.Symbol, ch *checker.Checker, scriptTarget core.ScriptTarget, cb func(name string, capitalizedName string) string) string { + var chain []*ast.Symbol + current := defaultExport + seen := collections.Set[*ast.Symbol]{} + + for current != nil { + // The predecessor to this function also looked for a name on the `localSymbol` + // of default exports, but I think `getDefaultLikeExportNameFromDeclaration` + // accomplishes the same thing via syntax - no tests failed when I removed it. + fromDeclaration := getDefaultLikeExportNameFromDeclaration(current) + if fromDeclaration != "" { + final := cb(fromDeclaration, "") + if final != "" { + return final + } + } + + if current.Name != ast.InternalSymbolNameDefault && current.Name != ast.InternalSymbolNameExportEquals { + if final := cb(current.Name, ""); final != "" { + return final + } + } + + chain = append(chain, current) + if !seen.AddIfAbsent(current) { + break + } + if current.Flags&ast.SymbolFlagsAlias != 0 { + current = ch.GetImmediateAliasedSymbol(current) + } else { + current = nil + } + } + + for _, symbol := range chain { + if symbol.Parent != nil && checker.IsExternalModuleSymbol(symbol.Parent) { + final := cb( + moduleSymbolToValidIdentifier(symbol.Parent, scriptTarget /*forceCapitalize*/, false), + moduleSymbolToValidIdentifier(symbol.Parent, scriptTarget /*forceCapitalize*/, true), + ) + if final != "" { + return final + } + } + } + return "" +} + +func getDefaultLikeExportNameFromDeclaration(symbol *ast.Symbol) string { + for _, d := range symbol.Declarations { + // "export default" in this case. See `ExportAssignment`for more details. + if ast.IsExportAssignment(d) { + if innerExpression := ast.SkipOuterExpressions(d.Expression(), ast.OEKAll); ast.IsIdentifier(innerExpression) { + return innerExpression.Text() + } + continue + } + // "export { ~ as default }" + if ast.IsExportSpecifier(d) && d.Symbol().Flags == ast.SymbolFlagsAlias && d.PropertyName() != nil { + if d.PropertyName().Kind == ast.KindIdentifier { + return d.PropertyName().Text() + } + continue + } + // GH#52694 + if name := ast.GetNameOfDeclaration(d); name != nil && name.Kind == ast.KindIdentifier { + return name.Text() + } + if symbol.Parent != nil && !checker.IsExternalModuleSymbol(symbol.Parent) { + return symbol.Parent.Name + } + } + return "" +} + +func forEachExternalModuleToImportFrom( + ch *checker.Checker, + program *compiler.Program, + preferences *UserPreferences, + // useAutoImportProvider bool, + cb func(module *ast.Symbol, moduleFile *ast.SourceFile, checker *checker.Checker, isFromPackageJson bool), +) { + // !!! excludePatterns + // excludePatterns := preferences.autoImportFileExcludePatterns && getIsExcludedPatterns(preferences, useCaseSensitiveFileNames) + + forEachExternalModule( + ch, + program.GetSourceFiles(), + // !!! excludePatterns, + func(module *ast.Symbol, file *ast.SourceFile) { + cb(module, file, ch, false) + }, + ) + + // !!! autoImportProvider + // if autoImportProvider := useAutoImportProvider && l.getPackageJsonAutoImportProvider(); autoImportProvider != nil { + // // start := timestamp(); + // forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), excludePatterns, host, func (module *ast.Symbol, file *ast.SourceFile) { + // if (file && !program.getSourceFile(file.FileName()) || !file && !checker.resolveName(module.Name, /*location*/ nil, ast.SymbolFlagsModule, /*excludeGlobals*/ false)) { + // // The AutoImportProvider filters files already in the main program out of its *root* files, + // // but non-root files can still be present in both programs, and already in the export info map + // // at this point. This doesn't create any incorrect behavior, but is a waste of time and memory, + // // so we filter them out here. + // cb(module, file, autoImportProvide.checker, /*isFromPackageJson*/ true); + // } + // }); + // // host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${timestamp() - start}`); + // } +} + +func forEachExternalModule( + ch *checker.Checker, + allSourceFiles []*ast.SourceFile, + // excludePatterns []RegExp, + cb func(moduleSymbol *ast.Symbol, sourceFile *ast.SourceFile), +) { + // !!! excludePatterns + // isExcluded := excludePatterns && getIsExcluded(excludePatterns, host) + + for _, ambient := range ch.GetAmbientModules() { + if !strings.Contains(ambient.Name, "*") /* && !(excludePatterns && ambient.Declarations.every(func (d){ return isExcluded(d.getSourceFile())})) */ { + cb(ambient, nil /*sourceFile*/) + } + } + for _, sourceFile := range allSourceFiles { + if ast.IsExternalOrCommonJSModule(sourceFile) /* && !isExcluded(sourceFile) */ { + cb(ch.GetMergedSymbol(sourceFile.Symbol), sourceFile) + } + } +} + +// ======================== generate code actions ======================= + +func (l *LanguageService) codeActionForFix( + ctx context.Context, + sourceFile *ast.SourceFile, + symbolName string, + fix *ImportFix, + includeSymbolNameInDescription bool, + preferences *UserPreferences, +) codeAction { + tracker := l.newChangeTracker(ctx) // !!! changetracker.with + diag := l.codeActionForFixWorker(tracker, sourceFile, symbolName, fix, includeSymbolNameInDescription, preferences) + changes := tracker.getChanges()[sourceFile.FileName()] + return codeAction{description: diag.Message(), changes: changes} +} + +func (l *LanguageService) codeActionForFixWorker( + changeTracker *changeTracker, + sourceFile *ast.SourceFile, + symbolName string, + fix *ImportFix, + includeSymbolNameInDescription bool, + preferences *UserPreferences, +) *diagnostics.Message { + switch fix.kind { + case ImportFixKindUseNamespace: + changeTracker.addNamespaceQualifier(sourceFile, fix.qualification()) + return diagnostics.FormatMessage(diagnostics.Change_0_to_1, symbolName, `${fix.namespacePrefix}.${symbolName}`) + case ImportFixKindJsdocTypeImport: + // !!! not implemented + // changeTracker.addImportType(changeTracker, sourceFile, fix, quotePreference); + // return diagnostics.FormatMessage(diagnostics.Change_0_to_1, symbolName, getImportTypePrefix(fix.moduleSpecifier, quotePreference) + symbolName); + case ImportFixKindAddToExisting: + changeTracker.doAddExistingFix( + sourceFile, + fix.importClauseOrBindingPattern, + core.IfElse(fix.importKind == ImportKindDefault, &Import{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly}, nil), + core.IfElse(fix.importKind == ImportKindNamed, []*Import{{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly}}, nil), + // nil /*removeExistingImportSpecifiers*/, + preferences, + ) + moduleSpecifierWithoutQuotes := stringutil.StripQuotes(fix.moduleSpecifier) + if includeSymbolNameInDescription { + return diagnostics.FormatMessage(diagnostics.Import_0_from_1, symbolName, moduleSpecifierWithoutQuotes) + } + return diagnostics.FormatMessage(diagnostics.Update_import_from_0, moduleSpecifierWithoutQuotes) + case ImportFixKindAddNew: + var declarations []*ast.Statement + defaultImport := core.IfElse(fix.importKind == ImportKindDefault, &Import{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly}, nil) + namedImports := core.IfElse(fix.importKind == ImportKindNamed, []*Import{{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly}}, nil) + var namespaceLikeImport *Import + qualification := fix.qualification() + if fix.importKind == ImportKindNamespace || fix.importKind == ImportKindCommonJS { + namespaceLikeImport = &Import{kind: fix.importKind, addAsTypeOnly: fix.addAsTypeOnly, name: symbolName} + if qualification != nil && qualification.namespacePrefix != "" { + namespaceLikeImport.name = qualification.namespacePrefix + } + } + + if fix.useRequire { + // !!! require + // declarations = getNewRequires(fixAddNew.moduleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options(), preferences) + } else { + declarations = changeTracker.getNewImports(fix.moduleSpecifier, defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options(), preferences) + } + + changeTracker.insertImports( + sourceFile, + declarations, + /*blankLineBetween*/ true, + preferences, + ) + if qualification != nil { + changeTracker.addNamespaceQualifier(sourceFile, qualification) + } + if includeSymbolNameInDescription { + return diagnostics.FormatMessage(diagnostics.Import_0_from_1, symbolName, fix.moduleSpecifier) + } + return diagnostics.FormatMessage(diagnostics.Add_import_from_0, fix.moduleSpecifier) + case ImportFixKindPromoteTypeOnly: + // !!! type only + // promotedDeclaration := promoteFromTypeOnly(changes, fix.typeOnlyAliasDeclaration, program, sourceFile, preferences); + // if promotedDeclaration.Kind == ast.KindImportSpecifier { + // return diagnostics.FormatMessage(diagnostics.Remove_type_from_import_of_0_from_1, symbolName, getModuleSpecifierText(promotedDeclaration.parent.parent)) + // } + // return diagnostics.FormatMessage(diagnostics.Remove_type_from_import_declaration_from_0, getModuleSpecifierText(promotedDeclaration)); + default: + panic(fmt.Sprintf(`Unexpected fix kind %v`, fix.kind)) + } + return nil +} + +func getModuleSpecifierText(promotedDeclaration *ast.ImportDeclaration) string { + if promotedDeclaration.Kind == ast.KindImportEqualsDeclaration { + importEqualsDeclaration := promotedDeclaration.AsImportEqualsDeclaration() + if ast.IsExternalModuleReference(importEqualsDeclaration.ModuleReference) { + expr := importEqualsDeclaration.ModuleReference.Expression() + if expr != nil && expr.Kind == ast.KindStringLiteral { + return expr.Text() + } + + } + return importEqualsDeclaration.ModuleReference.Text() + } + return promotedDeclaration.Parent.ModuleSpecifier().Text() +} diff --git a/internal/ls/autoimportsexportinfo.go b/internal/ls/autoimportsexportinfo.go new file mode 100644 index 0000000000..65de5e82ad --- /dev/null +++ b/internal/ls/autoimportsexportinfo.go @@ -0,0 +1,181 @@ +package ls + +import ( + "context" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/scanner" +) + +func (l *LanguageService) getExportInfos( + ctx context.Context, + ch *checker.Checker, + importingFile *ast.SourceFile, + preferences *UserPreferences, + exportMapKey ExportInfoMapKey, +) []*SymbolExportInfo { + expInfoMap := NewExportInfoMap(l.GetProgram().GetGlobalTypingsCacheLocation()) + moduleCount := 0 + symbolNameMatch := func(symbolName string) bool { + return symbolName == exportMapKey.SymbolName + } + forEachExternalModuleToImportFrom( + ch, + l.GetProgram(), + preferences, + // /*useAutoImportProvider*/ true, + func(moduleSymbol *ast.Symbol, moduleFile *ast.SourceFile, ch *checker.Checker, isFromPackageJson bool) { + if moduleCount = moduleCount + 1; moduleCount%100 == 0 && ctx.Err() != nil { + return + } + if moduleFile == nil && moduleSymbol.Name != exportMapKey.AmbientModuleName { + return + } + seenExports := collections.Set[string]{} + defaultInfo := getDefaultLikeExportInfo(moduleSymbol, ch) + var exportingModuleSymbol *ast.Symbol + if defaultInfo != nil { + exportingModuleSymbol = defaultInfo.exportingModuleSymbol + // Note: I think we shouldn't actually see resolved module symbols here, but weird merges + // can cause it to happen: see 'completionsImport_mergedReExport.ts' + if isImportableSymbol(exportingModuleSymbol, ch) { + expInfoMap.add( + importingFile.Path(), + exportingModuleSymbol, + core.IfElse(defaultInfo.exportKind == ExportKindDefault, ast.InternalSymbolNameDefault, ast.InternalSymbolNameExportEquals), + moduleSymbol, + moduleFile, + defaultInfo.exportKind, + isFromPackageJson, + ch, + symbolNameMatch, + nil, + ) + } + } + ch.ForEachExportAndPropertyOfModule(moduleSymbol, func(exported *ast.Symbol, key string) { + if exported != exportingModuleSymbol && isImportableSymbol(exported, ch) && seenExports.AddIfAbsent(key) { + expInfoMap.add( + importingFile.Path(), + exported, + key, + moduleSymbol, + moduleFile, + ExportKindNamed, + isFromPackageJson, + ch, + symbolNameMatch, + nil, + ) + } + }) + }) + return expInfoMap.get(importingFile.Path(), ch, exportMapKey) +} + +func (l *LanguageService) searchExportInfosForCompletions( + ctx context.Context, + ch *checker.Checker, + importingFile *ast.SourceFile, + preferences *UserPreferences, + isForImportStatementCompletion bool, + isRightOfOpenTag bool, + isTypeOnlyLocation bool, + lowerCaseTokenText string, + action func([]*SymbolExportInfo, string, bool, ExportInfoMapKey) []*SymbolExportInfo, +) { + symbolNameMatches := map[string]bool{} + symbolNameMatch := func(symbolName string) bool { + if !scanner.IsIdentifierText(symbolName, importingFile.LanguageVariant) { + return false + } + if b, ok := symbolNameMatches[symbolName]; ok { + return b + } + if isNonContextualKeyword(scanner.StringToToken(symbolName)) { + symbolNameMatches[symbolName] = false + return false + } + // Do not try to auto-import something with a lowercase first letter for a JSX tag + firstChar := rune(symbolName[0]) + if isRightOfOpenTag && (firstChar < 'A' || firstChar > 'Z') { + symbolNameMatches[symbolName] = false + return false + } + + symbolNameMatches[symbolName] = charactersFuzzyMatchInString(symbolName, lowerCaseTokenText) + return symbolNameMatches[symbolName] + } + flagMatch := func(targetFlags ast.SymbolFlags) bool { + if !isTypeOnlyLocation && !isForImportStatementCompletion && (targetFlags&ast.SymbolFlagsValue) == 0 { + return false + } + if isTypeOnlyLocation && (targetFlags&(ast.SymbolFlagsModule|ast.SymbolFlagsType) == 0) { + return false + } + return true + } + + expInfoMap := NewExportInfoMap(l.GetProgram().GetGlobalTypingsCacheLocation()) + moduleCount := 0 + forEachExternalModuleToImportFrom( + ch, + l.GetProgram(), + preferences, + // /*useAutoImportProvider*/ true, + func(moduleSymbol *ast.Symbol, moduleFile *ast.SourceFile, ch *checker.Checker, isFromPackageJson bool) { + if moduleCount = moduleCount + 1; moduleCount%100 == 0 && ctx.Err() != nil { + return + } + seenExports := collections.Set[string]{} + defaultInfo := getDefaultLikeExportInfo(moduleSymbol, ch) + // Note: I think we shouldn't actually see resolved module symbols here, but weird merges + // can cause it to happen: see 'completionsImport_mergedReExport.ts' + if defaultInfo != nil && isImportableSymbol(defaultInfo.exportingModuleSymbol, ch) { + expInfoMap.add( + importingFile.Path(), + defaultInfo.exportingModuleSymbol, + core.IfElse(defaultInfo.exportKind == ExportKindDefault, ast.InternalSymbolNameDefault, ast.InternalSymbolNameExportEquals), + moduleSymbol, + moduleFile, + defaultInfo.exportKind, + isFromPackageJson, + ch, + symbolNameMatch, + flagMatch, + ) + } + var exportingModuleSymbol *ast.Symbol + if defaultInfo != nil { + exportingModuleSymbol = defaultInfo.exportingModuleSymbol + } + ch.ForEachExportAndPropertyOfModule(moduleSymbol, func(exported *ast.Symbol, key string) { + if exported != exportingModuleSymbol && isImportableSymbol(exported, ch) && seenExports.AddIfAbsent(key) { + expInfoMap.add( + importingFile.Path(), + exported, + key, + moduleSymbol, + moduleFile, + ExportKindNamed, + isFromPackageJson, + ch, + symbolNameMatch, + flagMatch, + ) + } + }) + }) + expInfoMap.search( + ch, + importingFile.Path(), + /*preferCapitalized*/ isRightOfOpenTag, + func(symbolName string, targetFlags ast.SymbolFlags) bool { + return symbolNameMatch(symbolName) && flagMatch(targetFlags) + }, + action, + ) +} diff --git a/internal/ls/autoimportstypes.go b/internal/ls/autoimportstypes.go new file mode 100644 index 0000000000..aeb9ddf543 --- /dev/null +++ b/internal/ls/autoimportstypes.go @@ -0,0 +1,215 @@ +package ls + +import ( + "fmt" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/modulespecifiers" +) + +//go:generate go tool golang.org/x/tools/cmd/stringer -type=ExportKind -output=autoImports_stringer_generated.go +//go:generate go tool mvdan.cc/gofumpt -lang=go1.25 -w autoImports_stringer_generated.go + +type ImportKind int + +const ( + ImportKindNamed ImportKind = 0 + ImportKindDefault ImportKind = 1 + ImportKindNamespace ImportKind = 2 + ImportKindCommonJS ImportKind = 3 +) + +type ExportKind int + +const ( + ExportKindNamed ExportKind = 0 + ExportKindDefault ExportKind = 1 + ExportKindExportEquals ExportKind = 2 + ExportKindUMD ExportKind = 3 + ExportKindModule ExportKind = 4 +) + +type ImportFixKind int + +const ( + // Sorted with the preferred fix coming first. + ImportFixKindUseNamespace ImportFixKind = 0 + ImportFixKindJsdocTypeImport ImportFixKind = 1 + ImportFixKindAddToExisting ImportFixKind = 2 + ImportFixKindAddNew ImportFixKind = 3 + ImportFixKindPromoteTypeOnly ImportFixKind = 4 +) + +type AddAsTypeOnly int + +const ( + // These should not be combined as bitflags, but are given powers of 2 values to + // easily detect conflicts between `NotAllowed` and `Required` by giving them a unique sum. + // They're also ordered in terms of increasing priority for a fix-all scenario (see + // `reduceAddAsTypeOnlyValues`). + AddAsTypeOnlyAllowed AddAsTypeOnly = 1 << 0 + AddAsTypeOnlyRequired AddAsTypeOnly = 1 << 1 + AddAsTypeOnlyNotAllowed AddAsTypeOnly = 1 << 2 +) + +type ImportFix struct { + kind ImportFixKind + isReExport *bool + exportInfo *SymbolExportInfo // !!! | FutureSymbolExportInfo | undefined + moduleSpecifierKind modulespecifiers.ResultKind + moduleSpecifier string + usagePosition *lsproto.Position + namespacePrefix *string + + importClauseOrBindingPattern *ast.Node // ImportClause | ObjectBindingPattern + importKind ImportKind // ImportKindDefault | ImportKindNamed + addAsTypeOnly AddAsTypeOnly + propertyName string // !!! not implemented + + useRequire bool + + typeOnlyAliasDeclaration *ast.Declaration // TypeOnlyAliasDeclaration +} + +func (i *ImportFix) qualification() *Qualification { + switch i.kind { + case ImportFixKindAddNew: + if i.usagePosition == nil || strPtrIsEmpty(i.namespacePrefix) { + return nil + } + fallthrough + case ImportFixKindUseNamespace: + return &Qualification{ + usagePosition: *i.usagePosition, + namespacePrefix: *i.namespacePrefix, + } + } + panic(fmt.Sprintf("no qualification with ImportFixKind %v", i.kind)) +} + +type Qualification struct { + usagePosition lsproto.Position + namespacePrefix string +} + +func getUseNamespaceImport( + moduleSpecifier string, + moduleSpecifierKind modulespecifiers.ResultKind, + namespacePrefix string, + usagePosition lsproto.Position, +) *ImportFix { + return &ImportFix{ + kind: ImportFixKindUseNamespace, + moduleSpecifierKind: moduleSpecifierKind, + moduleSpecifier: moduleSpecifier, + + usagePosition: ptrTo(usagePosition), + namespacePrefix: strPtrTo(namespacePrefix), + } +} + +func getAddJsdocTypeImport( + moduleSpecifier string, + moduleSpecifierKind modulespecifiers.ResultKind, + usagePosition *lsproto.Position, + exportInfo *SymbolExportInfo, + isReExport *bool, +) *ImportFix { + return &ImportFix{ + kind: ImportFixKindJsdocTypeImport, + isReExport: isReExport, + exportInfo: exportInfo, + moduleSpecifierKind: moduleSpecifierKind, + moduleSpecifier: moduleSpecifier, + usagePosition: usagePosition, + } +} + +func getAddToExistingImport( + importClauseOrBindingPattern *ast.Node, + importKind ImportKind, + moduleSpecifier string, + moduleSpecifierKind modulespecifiers.ResultKind, + addAsTypeOnly AddAsTypeOnly, +) *ImportFix { + return &ImportFix{ + kind: ImportFixKindAddToExisting, + moduleSpecifierKind: moduleSpecifierKind, + moduleSpecifier: moduleSpecifier, + importClauseOrBindingPattern: importClauseOrBindingPattern, + importKind: importKind, + addAsTypeOnly: addAsTypeOnly, + } +} + +func getNewAddNewImport( + moduleSpecifier string, + moduleSpecifierKind modulespecifiers.ResultKind, + importKind ImportKind, + useRequire bool, + addAsTypeOnly AddAsTypeOnly, + exportInfo *SymbolExportInfo, // !!! | FutureSymbolExportInfo + isReExport *bool, + qualification *Qualification, +) *ImportFix { + return &ImportFix{ + kind: ImportFixKindAddNew, + isReExport: isReExport, + exportInfo: exportInfo, + moduleSpecifierKind: modulespecifiers.ResultKindNone, + moduleSpecifier: moduleSpecifier, + importKind: importKind, + addAsTypeOnly: addAsTypeOnly, + useRequire: useRequire, + } +} + +func getNewPromoteTypeOnlyImport(typeOnlyAliasDeclaration *ast.Declaration) *ImportFix { + // !!! function stub + return &ImportFix{ + kind: ImportFixKindPromoteTypeOnly, + // isReExport *bool + // exportInfo *SymbolExportInfo // !!! | FutureSymbolExportInfo | undefined + // moduleSpecifierKind modulespecifiers.ResultKind + // moduleSpecifier string + typeOnlyAliasDeclaration: typeOnlyAliasDeclaration, + } +} + +/** Information needed to augment an existing import declaration. */ +// !!! after full implementation, rename to AddToExistingImportInfo +type FixAddToExistingImportInfo struct { + declaration *ast.Declaration + importKind ImportKind + targetFlags ast.SymbolFlags + symbol *ast.Symbol +} + +func (info *FixAddToExistingImportInfo) getNewImportFromExistingSpecifier( + isValidTypeOnlyUseSite bool, + useRequire bool, + ch *checker.Checker, + compilerOptions *core.CompilerOptions, +) *ImportFix { + moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(info.declaration) + if moduleSpecifier == nil || moduleSpecifier.Text() == "" { + return nil + } + addAsTypeOnly := AddAsTypeOnlyNotAllowed + if !useRequire { + addAsTypeOnly = getAddAsTypeOnly(isValidTypeOnlyUseSite, info.symbol, info.targetFlags, ch, compilerOptions) + } + return getNewAddNewImport( + moduleSpecifier.Text(), + modulespecifiers.ResultKindNone, + info.importKind, + useRequire, + addAsTypeOnly, + nil, // exportInfo + nil, // isReExport + nil, // qualification + ) +} diff --git a/internal/ls/changetracker.go b/internal/ls/changetracker.go new file mode 100644 index 0000000000..34397aa546 --- /dev/null +++ b/internal/ls/changetracker.go @@ -0,0 +1,336 @@ +package ls + +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/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/printer" + "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/stringutil" +) + +type changeNodeOptions 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 changeNodeOptions +} + +type changeTracker struct { + // initialized with + formatSettings *format.FormatCodeSettings + newLine string + ls *LanguageService + ctx context.Context + *printer.EmitContext + + *ast.NodeFactory + changes *collections.MultiMap[*ast.SourceFile, *trackerEdit] + + // created during call to getChanges + writer *printer.ChangeTrackerWriter + // printer +} + +func (ls *LanguageService) newChangeTracker(ctx context.Context) *changeTracker { + emitContext := printer.NewEmitContext() + newLine := ls.GetProgram().Options().NewLine.GetNewLineCharacter() + formatCodeSettings := format.GetDefaultFormatCodeSettings(newLine) // !!! format.GetFormatCodeSettingsFromContext(ctx), + ctx = format.WithFormatCodeSettings(ctx, formatCodeSettings, newLine) + return &changeTracker{ + ls: ls, + EmitContext: emitContext, + NodeFactory: &emitContext.Factory.NodeFactory, + changes: &collections.MultiMap[*ast.SourceFile, *trackerEdit]{}, + ctx: ctx, + formatSettings: formatCodeSettings, + newLine: newLine, + } +} + +// !!! address strada note +// - Note: after calling this, the TextChanges object must be discarded! +func (ct *changeTracker) getChanges() map[string][]*lsproto.TextEdit { + // !!! finishDeleteDeclarations + // !!! finishClassesWithNodesInsertedAtStart + changes := ct.getTextChangesFromChanges() + // !!! changes for new files + return changes +} + +func (ct *changeTracker) replaceNode(sourceFile *ast.SourceFile, oldNode *ast.Node, newNode *ast.Node, options *changeNodeOptions) { + if options == nil { + // defaults to `useNonAdjustedPositions` + options = &changeNodeOptions{ + leadingTriviaOption: leadingTriviaOptionExclude, + trailingTriviaOption: trailingTriviaOptionExclude, + } + } + ct.replaceRange(sourceFile, ct.getAdjustedRange(sourceFile, oldNode, oldNode, options.leadingTriviaOption, options.trailingTriviaOption), newNode, *options) +} + +func (ct *changeTracker) replaceRange(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNode *ast.Node, options changeNodeOptions) { + ct.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithSingleNode, Range: lsprotoRange, options: options, Node: newNode}) +} + +func (ct *changeTracker) replaceRangeWithText(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, text string) { + ct.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindText, Range: lsprotoRange, NewText: text}) +} + +func (ct *changeTracker) replaceRangeWithNodes(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNodes []*ast.Node, options changeNodeOptions) { + if len(newNodes) == 1 { + ct.replaceRange(sourceFile, lsprotoRange, newNodes[0], options) + return + } + ct.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithMultipleNodes, Range: lsprotoRange, nodes: newNodes, options: options}) +} + +func (ct *changeTracker) insertText(sourceFile *ast.SourceFile, pos lsproto.Position, text string) { + ct.replaceRangeWithText(sourceFile, lsproto.Range{Start: pos, End: pos}, text) +} + +func (ct *changeTracker) insertNodeAt(sourceFile *ast.SourceFile, pos core.TextPos, newNode *ast.Node, options changeNodeOptions) { + lsPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, pos) + ct.replaceRange(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNode, options) +} + +func (ct *changeTracker) insertNodesAt(sourceFile *ast.SourceFile, pos core.TextPos, newNodes []*ast.Node, options changeNodeOptions) { + lsPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, pos) + ct.replaceRangeWithNodes(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNodes, options) +} + +func (ct *changeTracker) insertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) { + endPosition := ct.endPosForInsertNodeAfter(sourceFile, after, newNode) + ct.insertNodeAt(sourceFile, endPosition, newNode, ct.getInsertNodeAfterOptions(sourceFile, after)) +} + +func (ct *changeTracker) insertNodesAfter(sourceFile *ast.SourceFile, after *ast.Node, newNodes []*ast.Node) { + endPosition := ct.endPosForInsertNodeAfter(sourceFile, after, newNodes[0]) + ct.insertNodesAt(sourceFile, endPosition, newNodes, ct.getInsertNodeAfterOptions(sourceFile, after)) +} + +func (ct *changeTracker) 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 := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End())) + ct.replaceRange(sourceFile, + lsproto.Range{Start: endPos, End: endPos}, + sourceFile.GetOrCreateToken(ast.KindSemicolonToken, after.End(), after.End(), after.Parent), + changeNodeOptions{}, + ) + } + return core.TextPos(ct.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 (ct *changeTracker) insertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node, containingList []*ast.Node) { + if len(containingList) == 0 { + containingList = format.GetContainingList(after, sourceFile).Nodes + } + index := slices.Index(containingList, after) + if index < 0 { + return + } + if index != len(containingList)-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# + // ###b, + // c, + nextNode := containingList[index+1] + startPos := scanner.SkipTriviaEx(sourceFile.Text(), nextNode.Pos(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: false}) + + // write separator and leading trivia of the next element as suffix + suffix := scanner.TokenToString(nextToken.Kind) + sourceFile.Text()[nextNode.End():startPos] + ct.insertNodeAt(sourceFile, core.TextPos(startPos), newNode, changeNodeOptions{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) != 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[index-1], sourceFile, false), sourceFile) + multilineList = afterMinusOneStartLinePosition != afterStartLinePosition + } + if hasCommentsBeforeLineBreak(sourceFile.Text(), after.End()) || printer.GetLinesBetweenPositions(sourceFile, containingList[0].Pos(), containingList[len(containingList)-1].End()) != 0 { + // in this case we'll always treat containing list as multiline + multilineList = true + } + + separatorString := scanner.TokenToString(separator) + end := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End())) + if !multilineList { + ct.replaceRange(sourceFile, lsproto.Range{Start: end, End: end}, newNode, changeNodeOptions{prefix: separatorString}) + return + } + + // insert separator immediately following the 'after' node to preserve comments in trailing trivia + // !!! formatcontext + ct.replaceRange(sourceFile, lsproto.Range{Start: end, End: end}, sourceFile.GetOrCreateToken(separator, after.End(), after.End()+len(separatorString), after.Parent), changeNodeOptions{}) + // use the same indentation as 'after' item + indentation := format.FindFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, ct.formatSettings) + // insert element before the line break on the line that contains 'after' element + insertPos := scanner.SkipTriviaEx(sourceFile.Text(), after.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: false}) + // find position before "\n" or "\r\n" + for insertPos != after.End() && stringutil.IsLineBreak(rune(sourceFile.Text()[insertPos-1])) { + insertPos-- + } + insertLSPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(insertPos)) + ct.replaceRange( + sourceFile, + lsproto.Range{Start: insertLSPos, End: insertLSPos}, + newNode, + changeNodeOptions{ + indentation: ptrTo(indentation), + prefix: ct.newLine, + }, + ) +} + +func (ct *changeTracker) insertAtTopOfFile(sourceFile *ast.SourceFile, insert []*ast.Statement, blankLineBetween bool) { + if len(insert) == 0 { + return + } + + pos := ct.getInsertionPositionAtSourceFileTop(sourceFile) + options := changeNodeOptions{} + if pos != 0 { + options.prefix = ct.newLine + } + if !stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) { + options.suffix = ct.newLine + } + if blankLineBetween { + options.suffix += ct.newLine + } + + if len(insert) == 1 { + ct.insertNodeAt(sourceFile, core.TextPos(pos), insert[0], options) + } else { + ct.insertNodesAt(sourceFile, core.TextPos(pos), insert, options) + } +} + +func (ct *changeTracker) getInsertNodeAfterOptions(sourceFile *ast.SourceFile, node *ast.Node) changeNodeOptions { + newLineChar := ct.newLine + var options changeNodeOptions + switch node.Kind { + case ast.KindParameter: + // default opts + options = changeNodeOptions{} + case ast.KindClassDeclaration, ast.KindModuleDeclaration: + options = changeNodeOptions{prefix: newLineChar, suffix: newLineChar} + + case ast.KindVariableDeclaration, ast.KindStringLiteral, ast.KindIdentifier: + options = changeNodeOptions{prefix: ", "} + + case ast.KindPropertyAssignment: + options = changeNodeOptions{suffix: "," + newLineChar} + + case ast.KindExportKeyword: + options = changeNodeOptions{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 = changeNodeOptions{suffix: newLineChar} + } + if node.End() == sourceFile.End() && ast.IsStatement(node) { + options.prefix = "\n" + options.prefix + } + + return options +} diff --git a/internal/ls/changetrackerimpl.go b/internal/ls/changetrackerimpl.go new file mode 100644 index 0000000000..39e073fad3 --- /dev/null +++ b/internal/ls/changetrackerimpl.go @@ -0,0 +1,413 @@ +package ls + +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/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 (ct *changeTracker) getTextChangesFromChanges() map[string][]*lsproto.TextEdit { + changes := map[string][]*lsproto.TextEdit{} + for sourceFile, changesInFile := range ct.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 CompareRanges(ptrTo(a.Range), ptrTo(b.Range)) }) + // verify that change intervals do not overlap, except possibly at end points. + for i := range len(changesInFile) - 1 { + if 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 := ct.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 (ct *changeTracker) computeNewText(change *trackerEdit, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile) string { + switch change.kind { + case trackerEditKindRemove: + return "" + case trackerEditKindText: + return change.NewText + } + + pos := int(ct.ls.converters.LineAndCharacterToPosition(sourceFile, change.Range.Start)) + formatNode := func(n *ast.Node) string { + return ct.getFormattedTextOfNode(n, targetSourceFile, sourceFile, pos, change.options) + } + + var text string + switch change.kind { + + case trackerEditKindReplaceWithMultipleNodes: + if change.options.joiner == "" { + change.options.joiner = ct.newLine + } + text = strings.Join(core.Map(change.nodes, func(n *ast.Node) string { return strings.TrimSuffix(formatNode(n), ct.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 && *change.options.indentation != 0 || format.GetLineStartPositionForPosition(pos, targetSourceFile) == pos) { + noIndent = strings.TrimLeftFunc(text, unicode.IsSpace) + } + return change.options.prefix + noIndent // !!! +((!options.suffix || endsWith(noIndent, options.suffix)) ? "" : options.suffix); +} + +/** Note: this may mutate `nodeIn`. */ +func (ct *changeTracker) getFormattedTextOfNode(nodeIn *ast.Node, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile, pos int, options changeNodeOptions) string { + text, sourceFileLike := ct.getNonformattedText(nodeIn, targetSourceFile) + // !!! if (validate) validate(node, text); + formatOptions := getFormatCodeSettingsForWriting(ct.formatSettings, targetSourceFile) + + var initialIndentation, delta int + if options.indentation == nil { + // !!! indentation for position + // initialIndentation = format.GetIndentationForPos(pos, sourceFile, formatOptions, options.prefix == ct.newLine || scanner.GetLineStartPositionForPosition(pos, targetFileLineMap) == 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(ct.ctx, sourceFileLike, sourceFileLike.AsSourceFile(), targetSourceFile.LanguageVariant, initialIndentation, delta) + return core.ApplyBulkEdits(text, changes) +} + +func getFormatCodeSettingsForWriting(options *format.FormatCodeSettings, sourceFile *ast.SourceFile) *format.FormatCodeSettings { + shouldAutoDetectSemicolonPreference := options.Semicolons == format.SemicolonPreferenceIgnore + shouldRemoveSemicolons := options.Semicolons == format.SemicolonPreferenceRemove || shouldAutoDetectSemicolonPreference && !probablyUsesSemicolons(sourceFile) + if shouldRemoveSemicolons { + options.Semicolons = format.SemicolonPreferenceRemove + } + + return options +} + +/** Note: output node may be mutated input node. */ +func (ct *changeTracker) getNonformattedText(node *ast.Node, sourceFile *ast.SourceFile) (string, *ast.Node) { + nodeIn := node + eofToken := ct.Factory.NewToken(ast.KindEndOfFile) + if ast.IsStatement(node) { + nodeIn = ct.Factory.NewSourceFile( + ast.SourceFileParseOptions{FileName: sourceFile.FileName(), Path: sourceFile.Path()}, + "", + ct.Factory.NewNodeList([]*ast.Node{node}), + ct.Factory.NewToken(ast.KindEndOfFile), + ) + } + writer := printer.NewChangeTrackerWriter(ct.newLine) + printer.NewPrinter( + printer.PrinterOptions{ + NewLine: core.GetNewLineKind(ct.newLine), + NeverAsciiEscape: true, + PreserveSourceNewlines: true, + TerminateUnterminatedLiterals: true, + }, + writer.GetPrintHandlers(), + ct.EmitContext, + ).Write(nodeIn, sourceFile, writer, nil) + + text := writer.String() + + nodeOut := writer.AssignPositionsToNode(nodeIn, ct.NodeFactory) + var sourceFileLike *ast.Node + if !ast.IsStatement(node) { + nodeList := ct.Factory.NewNodeList([]*ast.Node{nodeOut}) + nodeList.Loc = nodeOut.Loc + eofToken.Loc = core.NewTextRange(nodeOut.End(), nodeOut.End()) + sourceFileLike = ct.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 + } else { + sourceFileLike = nodeOut + } + return text, sourceFileLike +} + +// method on the changeTracker because use of converters +func (ct *changeTracker) getAdjustedRange(sourceFile *ast.SourceFile, startNode *ast.Node, endNode *ast.Node, leadingOption leadingTriviaOption, trailingOption trailingTriviaOption) lsproto.Range { + return *ct.ls.createLspRangeFromBounds( + ct.getAdjustedStartPosition(sourceFile, startNode, leadingOption, false), + ct.getAdjustedEndPosition(sourceFile, endNode, trailingOption), + sourceFile, + ) +} + +// method on the changeTracker because use of converters +func (ct *changeTracker) getAdjustedStartPosition(sourceFile *ast.SourceFile, node *ast.Node, leadingOption leadingTriviaOption, hasTrailingComment bool) int { + if leadingOption == leadingTriviaOptionJSDoc { + if JSDocComments := parser.GetJSDocCommentRanges(ct.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.LineMap() + 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(ct.NodeFactory, sourceFile.Text(), fullStart)) + if len(comments) == 0 { + comments = slices.Collect(scanner.GetTrailingCommentRanges(ct.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 (ct *changeTracker) 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.LineMap() + nodeEndLine := scanner.ComputeLineOfPosition(lineStarts, node.End()) + for comment := range scanner.GetTrailingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End()) { + // Single line can break the loop as trivia will only be this line. + // Comments on subsequest 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 (ct *changeTracker) 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(ct.NodeFactory, sourceFile.Text(), node.End())), + scanner.GetLeadingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End()), + ); len(comments) > 0 { + if realEnd := comments[len(comments)-1].End(); realEnd != 0 { + return realEnd + } + } + return node.End() + } + + if multilineEndPosition := ct.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 (ct *changeTracker) 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(ct.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.LineMap() + 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 +} diff --git a/internal/ls/completions.go b/internal/ls/completions.go index ea82d45206..c42bb1ccc8 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -14,6 +14,7 @@ import ( "github.com/go-json-experiment/json" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/binder" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" @@ -27,6 +28,7 @@ import ( "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/tspath" "golang.org/x/text/collate" "golang.org/x/text/language" ) @@ -39,7 +41,7 @@ func (l *LanguageService) ProvideCompletion( clientOptions *lsproto.CompletionClientCapabilities, preferences *UserPreferences, ) (lsproto.CompletionResponse, error) { - program, file := l.getProgramAndFile(documentURI) + _, file := l.getProgramAndFile(documentURI) var triggerCharacter *string if context != nil { triggerCharacter = context.TriggerCharacter @@ -47,7 +49,6 @@ func (l *LanguageService) ProvideCompletion( position := int(l.converters.LineAndCharacterToPosition(file, LSPPosition)) completionList := l.getCompletionsAtPosition( ctx, - program, file, position, triggerCharacter, @@ -100,8 +101,8 @@ type completionDataData struct { isJsxIdentifierExpected bool isRightOfOpenTag bool isRightOfDotOrQuestionDot bool - importStatementCompletion any // !!! - hasUnresolvedAutoImports bool // !!! + importStatementCompletion *importStatementCompletionInfo // !!! + hasUnresolvedAutoImports bool // !!! // flags CompletionInfoFlags // !!! defaultCommitCharacters []string } @@ -120,7 +121,12 @@ type completionDataJSDocParameterName struct { } type importStatementCompletionInfo struct { - // !!! + isKeywordOnlyCompletion bool + keywordCompletion ast.Kind // TokenKind + isNewIdentifierLocation bool + isTopLevelTypeOnly bool + couldBeTypeOnlyImportSpecifier bool + replacementSpan *lsproto.Range } // If we're after the `=` sign but no identifier has been typed yet, @@ -145,6 +151,15 @@ const ( KeywordCompletionFiltersLast = KeywordCompletionFiltersTypeKeyword ) +func keywordFiltersFromSyntaxKind(keywordCompletion ast.Kind) KeywordCompletionFilters { + switch keywordCompletion { + case ast.KindTypeKeyword: + return KeywordCompletionFiltersTypeKeyword + default: + panic("Unknown mapping from ast.Kind `" + keywordCompletion.String() + "` to KeywordCompletionFilters") + } +} + type CompletionKind int const ( @@ -196,7 +211,6 @@ const ( symbolOriginInfoKindExport symbolOriginInfoKindPromise symbolOriginInfoKindNullable - symbolOriginInfoKindResolvedExport symbolOriginInfoKindTypeOnlyAlias symbolOriginInfoKindObjectLiteralMethod symbolOriginInfoKindIgnore @@ -214,39 +228,58 @@ type symbolOriginInfo struct { data any } -func (s *symbolOriginInfo) symbolName() string { - switch s.data.(type) { +func (origin *symbolOriginInfo) symbolName() string { + switch origin.data.(type) { case *symbolOriginInfoExport: - return s.data.(*symbolOriginInfoExport).symbolName - case *symbolOriginInfoResolvedExport: - return s.data.(*symbolOriginInfoResolvedExport).symbolName + return origin.data.(*symbolOriginInfoExport).symbolName + case *symbolOriginInfoComputedPropertyName: + return origin.data.(*symbolOriginInfoComputedPropertyName).symbolName default: - panic(fmt.Sprintf("symbolOriginInfo: unknown data type for symbolName(): %T", s.data)) + panic(fmt.Sprintf("symbolOriginInfo: unknown data type for symbolName(): %T", origin.data)) } } -type symbolOriginInfoExport struct { - symbolName string - moduleSymbol *ast.Symbol - isDefaultExport bool - exporName string - // exportMapKey ExportMapInfoKey // !!! +func (origin *symbolOriginInfo) moduleSymbol() *ast.Symbol { + switch origin.data.(type) { + case *symbolOriginInfoExport: + return origin.data.(*symbolOriginInfoExport).moduleSymbol + default: + panic(fmt.Sprintf("symbolOriginInfo: unknown data type for moduleSymbol(): %T", origin.data)) + } } -func (s *symbolOriginInfo) asExport() *symbolOriginInfoExport { - return s.data.(*symbolOriginInfoExport) +func (origin *symbolOriginInfo) toCompletionEntryData() *completionEntryData { + debug.Assert(origin.kind&symbolOriginInfoKindExport != 0, fmt.Sprintf("completionEntryData is not generated for symbolOriginInfo of type %T", origin.data)) + var ambientModuleName *string + if origin.fileName == "" { + ambientModuleName = strPtrTo(stringutil.StripQuotes(origin.moduleSymbol().Name)) + } + var isPackageJsonImport core.Tristate + if origin.isFromPackageJson { + isPackageJsonImport = core.TSTrue + } + + data := origin.data.(*symbolOriginInfoExport) + return &completionEntryData{ + ExportName: data.exportName, + ExportMapKey: data.exportMapKey, + ModuleSpecifier: data.moduleSpecifier, + AmbientModuleName: ambientModuleName, + FileName: strPtrTo(origin.fileName), + IsPackageJsonImport: isPackageJsonImport, + } } -type symbolOriginInfoResolvedExport struct { - symbolName string - moduleSymbol *ast.Symbol - exportName string - // exportMapKey ExportMapInfoKey // !!! +type symbolOriginInfoExport struct { + symbolName string + moduleSymbol *ast.Symbol + exportName string + exportMapKey ExportInfoMapKey moduleSpecifier string } -func (s *symbolOriginInfo) asResolvedExport() *symbolOriginInfoResolvedExport { - return s.data.(*symbolOriginInfoResolvedExport) +func (s *symbolOriginInfo) asExport() *symbolOriginInfoExport { + return s.data.(*symbolOriginInfoExport) } type symbolOriginInfoObjectLiteralMethod struct { @@ -310,7 +343,6 @@ const ( func (l *LanguageService) getCompletionsAtPosition( ctx context.Context, - program *compiler.Program, file *ast.SourceFile, position int, triggerCharacter *string, @@ -332,7 +364,7 @@ func (l *LanguageService) getCompletionsAtPosition( return nil } - compilerOptions := program.Options() + compilerOptions := l.GetProgram().Options() // !!! see if incomplete completion list and continue or clean @@ -342,7 +374,6 @@ func (l *LanguageService) getCompletionsAtPosition( position, previousToken, compilerOptions, - program, preferences, clientOptions, ) @@ -363,9 +394,9 @@ func (l *LanguageService) getCompletionsAtPosition( ) } - checker, done := program.GetTypeCheckerForFile(ctx, file) + checker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) defer done() - data := getCompletionData(program, checker, file, position, preferences) + data := l.getCompletionData(ctx, checker, file, position, preferences) if data == nil { return nil } @@ -375,8 +406,8 @@ func (l *LanguageService) getCompletionsAtPosition( optionalReplacementSpan := l.getOptionalReplacementSpan(data.location, file) response := l.completionInfoFromData( ctx, + checker, file, - program, compilerOptions, data, preferences, @@ -429,14 +460,14 @@ func (l *LanguageService) getCompletionsAtPosition( } } -func getCompletionData( - program *compiler.Program, +func (l *LanguageService) getCompletionData( + ctx context.Context, typeChecker *checker.Checker, file *ast.SourceFile, position int, preferences *UserPreferences, ) completionData { - inCheckedFile := isCheckedFile(file, program.Options()) + inCheckedFile := isCheckedFile(file, l.GetProgram().Options()) currentToken := astnav.GetTokenAtPosition(file, position) @@ -539,14 +570,30 @@ func getCompletionData( location := astnav.GetTouchingPropertyName(file, position) keywordFilters := KeywordCompletionFiltersNone isNewIdentifierLocation := false - // !!! - // flags := CompletionInfoFlagsNone + // !!! flags := CompletionInfoFlagsNone var defaultCommitCharacters []string if contextToken != nil { - // !!! import completions + importStatementCompletionInfo := l.getImportStatementCompletionInfo(contextToken, file) + if importStatementCompletionInfo.keywordCompletion != ast.KindUnknown { + if importStatementCompletionInfo.isKeywordOnlyCompletion { + return &completionDataKeyword{ + keywordCompletions: []*lsproto.CompletionItem{{ + Label: scanner.TokenToString(importStatementCompletionInfo.keywordCompletion), + Kind: ptrTo(lsproto.CompletionItemKindKeyword), + SortText: ptrTo(string(SortTextGlobalsOrKeywords)), + }}, + isNewIdentifierLocation: importStatementCompletionInfo.isNewIdentifierLocation, + } + } + keywordFilters = keywordFiltersFromSyntaxKind(importStatementCompletionInfo.keywordCompletion) + } + if importStatementCompletionInfo.replacementSpan != nil && ptrIsTrue(preferences.IncludeCompletionsForImportStatements) { + // !!! flags |= CompletionInfoFlags.IsImportStatementCompletion; + importStatementCompletion = &importStatementCompletionInfo + isNewIdentifierLocation = importStatementCompletionInfo.isNewIdentifierLocation + } // Bail out if this is a known invalid completion location. - // !!! if (!importStatementCompletionInfo.replacementSpan && ...) if isCompletionListBlocker(contextToken, previousToken, location, file, position, typeChecker) { if keywordFilters != KeywordCompletionFiltersNone { isNewIdentifierLocation, _ := computeCommitCharactersAndIsNewIdentifier(contextToken, file, position) @@ -668,15 +715,14 @@ func getCompletionData( var symbols []*ast.Symbol symbolToOriginInfoMap := map[ast.SymbolId]*symbolOriginInfo{} symbolToSortTextMap := map[ast.SymbolId]sortText{} - // var importSpecifierResolver any // !!! import var seenPropertySymbols collections.Set[ast.SymbolId] + importSpecifierResolver := &importSpecifierResolverForCompletions{SourceFile: file, UserPreferences: preferences, l: l} isTypeOnlyLocation := insideJSDocTagTypeExpression || insideJsDocImportTag || importStatementCompletion != nil && ast.IsTypeOnlyImportOrExportDeclaration(location.Parent) || !isContextTokenValueLocation(contextToken) && (isPossiblyTypeArgumentPosition(contextToken, file, typeChecker) || ast.IsPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken)) - // var getModuleSpecifierResolutionHost any // !!! auto import addSymbolOriginInfo := func(symbol *ast.Symbol, insertQuestionDot bool, insertAwait bool) { symbolId := ast.GetSymbolId(symbol) @@ -729,38 +775,37 @@ func getCompletionData( typeChecker.TryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.Name, moduleSymbol) != firstAccessibleSymbol { symbolToOriginInfoMap[firstAccessibleSymbolId] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberNoExport, insertQuestionDot)} } else { - // !!! imports - // var fileName string - // if tspath.IsExternalModuleNameRelative(core.StripQuotes(moduleSymbol.Name)) { - // fileName = ast.GetSourceFileOfModule(moduleSymbol).FileName() - // } - // if importSpecifierResolver == nil { - // importSpecifierResolver ||= codefix.createImportSpecifierResolver(sourceFile, program, host, preferences)) - // } - // const { moduleSpecifier } = importSpecifier.getModuleSpecifierForBestExportInfo( - // [{ - // exportKind: ExportKind.Named, - // moduleFileName: fileName, - // isFromPackageJson: false, - // moduleSymbol, - // symbol: firstAccessibleSymbol, - // targetFlags: skipAlias(firstAccessibleSymbol, typeChecker).flags, - // }], - // position, - // isValidTypeOnlyAliasUseSite(location), - // ) || {}; - // if (moduleSpecifier) { - // const origin: SymbolOriginInfoResolvedExport = { - // kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), - // moduleSymbol, - // isDefaultExport: false, - // symbolName: firstAccessibleSymbol.name, - // exportName: firstAccessibleSymbol.name, - // fileName, - // moduleSpecifier, - // }; - // symbolToOriginInfoMap[index] = origin; - // } + var fileName string + if tspath.IsExternalModuleNameRelative(stringutil.StripQuotes(moduleSymbol.Name)) { + fileName = ast.GetSourceFileOfModule(moduleSymbol).FileName() + } + result := importSpecifierResolver.getModuleSpecifierForBestExportInfo( + typeChecker, + []*SymbolExportInfo{{ + exportKind: ExportKindNamed, + moduleFileName: fileName, + isFromPackageJson: false, + moduleSymbol: moduleSymbol, + symbol: firstAccessibleSymbol, + targetFlags: typeChecker.SkipAlias(firstAccessibleSymbol).Flags, + }}, + position, + ast.IsValidTypeOnlyAliasUseSite(location), + ) + + if result != nil { + symbolToOriginInfoMap[ast.GetSymbolId(symbol)] = &symbolOriginInfo{ + kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberExport, insertQuestionDot), + isDefaultExport: false, + fileName: fileName, + data: symbolOriginInfoExport{ + moduleSymbol: moduleSymbol, + symbolName: firstAccessibleSymbol.Name, + exportName: firstAccessibleSymbol.Name, + moduleSpecifier: result.moduleSpecifier, + }, + } + } } } else if firstAccessibleSymbolId == 0 || !seenPropertySymbols.Has(firstAccessibleSymbolId) { symbols = append(symbols, symbol) @@ -1107,13 +1152,143 @@ func getCompletionData( return globalsSearchSuccess } + shouldOfferImportCompletions := func() bool { + // If already typing an import statement, provide completions for it. + if importStatementCompletion != nil { + return true + } + // If not already a module, must have modules enabled. + if !ptrIsTrue(preferences.IncludeCompletionsForModuleExports) { + return false + } + // Always using ES modules in 6.0+ + return true + } + + // Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` + collectAutoImports := func() { + if !shouldOfferImportCompletions() { + return + } + // !!! CompletionInfoFlags + + // import { type | -> token text should be blank + var lowerCaseTokenText string + if previousToken != nil && ast.IsIdentifier(previousToken) && !(previousToken == contextToken && importStatementCompletion != nil) { + lowerCaseTokenText = strings.ToLower(previousToken.Text()) + } + + // !!! timestamp + // Under `--moduleResolution nodenext` or `bundler`, we have to resolve module specifiers up front, because + // package.json exports can mean we *can't* resolve a module specifier (that doesn't include a + // relative path into node_modules), and we want to filter those completions out entirely. + // Import statement completions always need specifier resolution because the module specifier is + // part of their `insertText`, not the `codeActions` creating edits away from the cursor. + // Finally, `autoImportSpecifierExcludeRegexes` necessitates eagerly resolving module specifiers + // because completion items are being explcitly filtered out by module specifier. + isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(location) + + // !!! moduleSpecifierCache := host.getModuleSpecifierCache(); + // !!! packageJsonAutoImportProvider := host.getPackageJsonAutoImportProvider(); + addSymbolToList := func(info []*SymbolExportInfo, symbolName string, isFromAmbientModule bool, exportMapKey ExportInfoMapKey) []*SymbolExportInfo { + // Do a relatively cheap check to bail early if all re-exports are non-importable + // due to file location or package.json dependency filtering. For non-node16+ + // module resolution modes, getting past this point guarantees that we'll be + // able to generate a suitable module specifier, so we can safely show a completion, + // even if we defer computing the module specifier. + info = core.Filter(info, func(i *SymbolExportInfo) bool { + var toFile *ast.SourceFile + if ast.IsSourceFile(i.moduleSymbol.ValueDeclaration) { + toFile = i.moduleSymbol.ValueDeclaration.AsSourceFile() + } + return l.isImportable( + file, + toFile, + i.moduleSymbol, + preferences, + importSpecifierResolver.packageJsonImportFilter(), + ) + }) + if len(info) == 0 { + return nil + } + + // In node16+, module specifier resolution can fail due to modules being blocked + // by package.json `exports`. If that happens, don't show a completion item. + // N.B. We always try to resolve module specifiers here, because we have to know + // now if it's going to fail so we can omit the completion from the list. + result := importSpecifierResolver.getModuleSpecifierForBestExportInfo(typeChecker, info, position, isValidTypeOnlyUseSite) + if result == nil { + return nil + } + + // If we skipped resolving module specifiers, our selection of which ExportInfo + // to use here is arbitrary, since the info shown in the completion list derived from + // it should be identical regardless of which one is used. During the subsequent + // `CompletionEntryDetails` request, we'll get all the ExportInfos again and pick + // the best one based on the module specifier it produces. + moduleSpecifier := result.moduleSpecifier + exportInfo := info[0] + if result.exportInfo != nil { + exportInfo = result.exportInfo + } + + isDefaultExport := exportInfo.exportKind == ExportKindDefault + if exportInfo.symbol == nil { + panic("should have handled `futureExportSymbolInfo` earlier") + } + symbol := exportInfo.symbol + if isDefaultExport { + if defaultSymbol := binder.GetLocalSymbolForExportDefault(symbol); defaultSymbol != nil { + symbol = defaultSymbol + } + } + + // pushAutoImportSymbol + symbolId := ast.GetSymbolId(symbol) + if symbolToSortTextMap[symbolId] == SortTextGlobalsOrKeywords { + // If an auto-importable symbol is available as a global, don't push the auto import + return nil + } + originInfo := &symbolOriginInfo{ + kind: symbolOriginInfoKindExport, + isDefaultExport: isDefaultExport, + isFromPackageJson: exportInfo.isFromPackageJson, + fileName: exportInfo.moduleFileName, + data: &symbolOriginInfoExport{ + symbolName: symbolName, + moduleSymbol: exportInfo.moduleSymbol, + exportName: core.IfElse(exportInfo.exportKind == ExportKindExportEquals, ast.InternalSymbolNameExportEquals, exportInfo.symbol.Name), + exportMapKey: exportMapKey, + moduleSpecifier: moduleSpecifier, + }, + } + symbolToOriginInfoMap[symbolId] = originInfo + symbolToSortTextMap[symbolId] = core.IfElse(importStatementCompletion != nil, SortTextLocationPriority, SortTextAutoImportSuggestions) + symbols = append(symbols, symbol) + return nil + } + l.searchExportInfosForCompletions(ctx, + typeChecker, + file, + preferences, + importStatementCompletion != nil, + isRightOfOpenTag, + isTypeOnlyLocation, + lowerCaseTokenText, + addSymbolToList, + ) + + // !!! completionInfoFlags + // !!! logging + } + tryGetImportCompletionSymbols := func() globalsSearch { if importStatementCompletion == nil { return globalsSearchContinue } isNewIdentifierLocation = true - // !!! auto imports - // collectAutoImports() + collectAutoImports() return globalsSearchSuccess } @@ -1508,9 +1683,7 @@ func getCompletionData( } } - // !!! auto imports - // collectAutoImports() - + collectAutoImports() if isTypeOnlyLocation { if contextToken != nil && ast.IsAssertionExpression(contextToken.Parent) { keywordFilters = KeywordCompletionFiltersTypeAssertionKeywords @@ -1652,8 +1825,8 @@ func getDefaultCommitCharacters(isNewIdentifierLocation bool) []string { func (l *LanguageService) completionInfoFromData( ctx context.Context, + typeChecker *checker.Checker, file *ast.SourceFile, - program *compiler.Program, compilerOptions *core.CompilerOptions, data *completionDataData, preferences *UserPreferences, @@ -1665,8 +1838,6 @@ func (l *LanguageService) completionInfoFromData( isNewIdentifierLocation := data.isNewIdentifierLocation contextToken := data.contextToken literals := data.literals - typeChecker, done := program.GetTypeCheckerForFile(ctx, file) - defer done() // Verify if the file is JSX language variant if file.LanguageVariant == core.LanguageVariantJSX { @@ -1708,7 +1879,6 @@ func (l *LanguageService) completionInfoFromData( nil, /*replacementToken*/ position, file, - program, preferences, compilerOptions, clientOptions, @@ -1716,9 +1886,7 @@ func (l *LanguageService) completionInfoFromData( compareCompletionEntries := getCompareCompletionEntries(ctx) if data.keywordFilters != KeywordCompletionFiltersNone { - keywordCompletions := getKeywordCompletions( - data.keywordFilters, - !data.insideJSDocTagTypeExpression && ast.IsSourceFileJS(file)) + keywordCompletions := getKeywordCompletions(data.keywordFilters, !data.insideJSDocTagTypeExpression && ast.IsSourceFileJS(file)) for _, keywordEntry := range keywordCompletions { if data.isTypeOnlyLocation && isTypeKeyword(scanner.StringToToken(keywordEntry.Label)) || !data.isTypeOnlyLocation && isContextualKeywordInAutoImportableExpressionSpace(keywordEntry.Label) || @@ -1776,14 +1944,13 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( replacementToken *ast.Node, position int, file *ast.SourceFile, - program *compiler.Program, preferences *UserPreferences, compilerOptions *core.CompilerOptions, clientOptions *lsproto.CompletionClientCapabilities, ) (uniqueNames collections.Set[string], sortedEntries []*lsproto.CompletionItem) { closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location) useSemicolons := probablyUsesSemicolons(file) - typeChecker, done := program.GetTypeCheckerForFile(ctx, file) + typeChecker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) defer done() isMemberCompletion := isMemberCompletionKind(data.completionKind) // Tracks unique names. @@ -1826,13 +1993,13 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( } entry := l.createCompletionItem( ctx, + typeChecker, symbol, sortText, replacementToken, data, position, file, - program, name, needsConvertPropertyAccess, origin, @@ -1893,13 +2060,13 @@ func createCompletionItemForLiteral( func (l *LanguageService) createCompletionItem( ctx context.Context, + typeChecker *checker.Checker, symbol *ast.Symbol, sortText sortText, replacementToken *ast.Node, data *completionDataData, position int, file *ast.SourceFile, - program *compiler.Program, name string, needsConvertPropertyAccess bool, origin *symbolOriginInfo, @@ -1917,8 +2084,6 @@ func (l *LanguageService) createCompletionItem( source := getSourceFromOrigin(origin) var labelDetails *lsproto.CompletionItemLabelDetails - typeChecker, done := program.GetTypeCheckerForFile(ctx, file) - defer done() insertQuestionDot := originIsNullableMember(origin) useBraces := originIsSymbolMember(origin) || needsConvertPropertyAccess if originIsThisType(origin) { @@ -2008,12 +2173,43 @@ func (l *LanguageService) createCompletionItem( file) } - if originIsResolvedExport(origin) { + if originIsExport(origin) { + resolvedOrigin := origin.asExport() labelDetails = &lsproto.CompletionItemLabelDetails{ - Description: &origin.asResolvedExport().moduleSpecifier, // !!! vscode @link support + Description: &resolvedOrigin.moduleSpecifier, // !!! vscode @link support } if data.importStatementCompletion != nil { - // !!! auto-imports + quotedModuleSpecifier := escapeSnippetText(quote(file, preferences, resolvedOrigin.moduleSpecifier)) + exportKind := ExportKindNamed + if origin.isDefaultExport { + exportKind = ExportKindDefault + } else if resolvedOrigin.exportName == ast.InternalSymbolNameExportEquals { + exportKind = ExportKindExportEquals + } + + insertText = "import " + typeOnlyText := scanner.TokenToString(ast.KindTypeKeyword) + " " + if data.importStatementCompletion.isTopLevelTypeOnly { + insertText += typeOnlyText + } + tabStop := core.IfElse(ptrIsTrue(clientOptions.CompletionItem.SnippetSupport), "$1", "") + importKind := getImportKind(file, exportKind, l.GetProgram(), true /*forceImportKeyword*/) + escapedSnippet := escapeSnippetText(name) + suffix := core.IfElse(useSemicolons, ";", "") + switch importKind { + case ImportKindCommonJS: + insertText += fmt.Sprintf(`%s%s = require(%s)%s`, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) + case ImportKindDefault: + insertText += fmt.Sprintf(`%s%s from %s%s`, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) + case ImportKindNamespace: + insertText += fmt.Sprintf(`* as %s from %s%s`, escapedSnippet, quotedModuleSpecifier, suffix) + case ImportKindNamed: + importSpecifierTypeOnlyText := core.IfElse(data.importStatementCompletion.couldBeTypeOnlyImportSpecifier, typeOnlyText, "") + insertText += fmt.Sprintf(`{ %s%s%s } from %s%s`, importSpecifierTypeOnlyText, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) + } + + replacementSpan = data.importStatementCompletion.replacementSpan + isSnippet = ptrIsTrue(clientOptions.CompletionItem.SnippetSupport) } } @@ -2100,10 +2296,10 @@ func (l *LanguageService) createCompletionItem( } } - if originIsExport(origin) || originIsResolvedExport(origin) { - // !!! auto-imports - // data = originToCompletionEntryData(origin) - // hasAction = importStatementCompletion == nil + var autoImportData *completionEntryData + if originIsExport(origin) { + autoImportData = origin.toCompletionEntryData() + hasAction = data.importStatementCompletion == nil } parentNamedImportOrExport := ast.FindAncestor(data.location, isNamedImportsOrExports) @@ -2164,6 +2360,7 @@ func (l *LanguageService) createCompletionItem( hasAction, preselect, source, + autoImportData, ) } @@ -2304,6 +2501,13 @@ func getDotAccessor(file *ast.SourceFile, position int) string { return "" } +func strPtrIsEmpty(ptr *string) bool { + if ptr == nil { + return true + } + return *ptr == "" +} + func strPtrTo(v string) *string { if v == "" { return nil @@ -2527,17 +2731,13 @@ func originIsIgnore(origin *symbolOriginInfo) bool { } func originIncludesSymbolName(origin *symbolOriginInfo) bool { - return originIsExport(origin) || originIsResolvedExport(origin) || originIsComputedPropertyName(origin) + return originIsExport(origin) || originIsComputedPropertyName(origin) } func originIsExport(origin *symbolOriginInfo) bool { return origin != nil && origin.kind&symbolOriginInfoKindExport != 0 } -func originIsResolvedExport(origin *symbolOriginInfo) bool { - return origin != nil && origin.kind&symbolOriginInfoKindResolvedExport != 0 -} - func originIsComputedPropertyName(origin *symbolOriginInfo) bool { return origin != nil && origin.kind&symbolOriginInfoKindComputedPropertyName != 0 } @@ -2571,8 +2771,8 @@ func getSourceFromOrigin(origin *symbolOriginInfo) string { return stringutil.StripQuotes(ast.SymbolName(origin.asExport().moduleSymbol)) } - if originIsResolvedExport(origin) { - return origin.asResolvedExport().moduleSpecifier + if originIsExport(origin) { + return origin.asExport().moduleSpecifier } if originIsThisType(origin) { @@ -3023,9 +3223,9 @@ func generateIdentifierForArbitraryString(text string) string { if size > 0 && validChar { if needsUnderscore { identifier += "_" - identifier += string(ch) - needsUnderscore = false } + identifier += string(ch) + needsUnderscore = false } else { needsUnderscore = true } @@ -3131,23 +3331,71 @@ func getCompareCompletionEntries(ctx context.Context) func(entryInSlice *lsproto if result == stringutil.ComparisonEqual { result = compareStrings(entryInSlice.Label, entryToInsert.Label) } - // !!! auto-imports - // if (result === Comparison.EqualTo && entryInArray.data?.moduleSpecifier && entryToInsert.data?.moduleSpecifier) { - // // Sort same-named auto-imports by module specifier - // result = compareNumberOfDirectorySeparators( - // (entryInArray.data as CompletionEntryDataResolved).moduleSpecifier, - // (entryToInsert.data as CompletionEntryDataResolved).moduleSpecifier, - // ); - // } + if result == stringutil.ComparisonEqual && entryInSlice.Data != nil && entryToInsert.Data != nil { + sliceEntryData, ok1 := (*entryInSlice.Data).(*completionEntryData) + insertEntryData, ok2 := (*entryToInsert.Data).(*completionEntryData) + if ok1 && ok2 && sliceEntryData.ModuleSpecifier != "" && insertEntryData.ModuleSpecifier != "" { + // Sort same-named auto-imports by module specifier + result = compareNumberOfDirectorySeparators( + sliceEntryData.ModuleSpecifier, + insertEntryData.ModuleSpecifier, + ) + } + } if result == stringutil.ComparisonEqual { // Fall back to symbol order - if we return `EqualTo`, `insertSorted` will put later symbols first. return stringutil.ComparisonLessThan } - return result } } +// True if the first character of `lowercaseCharacters` is the first character +// of some "word" in `identiferString` (where the string is split into "words" +// by camelCase and snake_case segments), then if the remaining characters of +// `lowercaseCharacters` appear, in order, in the rest of `identifierString`.// +// True: +// 'state' in 'useState' +// 'sae' in 'useState' +// 'viable' in 'ENVIRONMENT_VARIABLE'// +// False: +// 'staet' in 'useState' +// 'tate' in 'useState' +// 'ment' in 'ENVIRONMENT_VARIABLE' +func charactersFuzzyMatchInString(identifierString string, lowercaseCharacters string) bool { + if lowercaseCharacters == "" { + return true + } + + var prevChar rune + matchedFirstCharacter := false + characterIndex := 0 + lowerCaseRunes := []rune(lowercaseCharacters) + testChar := lowerCaseRunes[characterIndex] + + for _, strChar := range []rune(identifierString) { + if strChar == testChar || strChar == unicode.ToUpper(testChar) { + willMatchFirstChar := prevChar == 0 || // Beginning of word + 'a' <= prevChar && prevChar <= 'z' && 'A' <= strChar && strChar <= 'Z' || // camelCase transition + prevChar == '_' && strChar != '_' // snake_case transition + matchedFirstCharacter = matchedFirstCharacter || willMatchFirstChar + if !matchedFirstCharacter { + continue + } + characterIndex++ + if characterIndex == len(lowerCaseRunes) { + return true + } else { + testChar = lowerCaseRunes[characterIndex] + } + } + prevChar = strChar + } + + // Did not find all characters + return false +} + var ( keywordCompletionsCache = collections.SyncMap[KeywordCompletionFilters, []*lsproto.CompletionItem]{} allKeywordCompletions = sync.OnceValue(func() []*lsproto.CompletionItem { @@ -4257,6 +4505,7 @@ func (l *LanguageService) getJsxClosingTagCompletion( false, /*hasAction*/ false, /*preselect*/ "", /*source*/ + nil, /*autoImportEntryData*/ // !!! jsx autoimports ) items := []*lsproto.CompletionItem{item} itemDefaults := l.setItemDefaults( @@ -4293,6 +4542,7 @@ func (l *LanguageService) createLSPCompletionItem( hasAction bool, preselect bool, source string, + autoImportEntryData *completionEntryData, ) *lsproto.CompletionItem { kind := getCompletionsSymbolKind(elementKind) var data any = &itemData{ @@ -4300,7 +4550,7 @@ func (l *LanguageService) createLSPCompletionItem( Position: position, Source: source, Name: name, - AutoImport: nil, // !!! auto-imports + AutoImport: autoImportEntryData, } // Text edit @@ -4441,6 +4691,7 @@ func (l *LanguageService) getLabelStatementCompletions( false, /*hasAction*/ false, /*preselect*/ "", /*source*/ + nil, /*autoImportEntryData*/ )) } } @@ -4678,6 +4929,11 @@ func hasCompletionItem(clientOptions *lsproto.CompletionClientCapabilities) bool return clientOptions != nil && clientOptions.CompletionItem != nil } +// strada TODO: this function is, at best, poorly named. Use sites are pretty suspicious. +func compilerOptionsIndicateEsModules(options *core.CompilerOptions) bool { + return options.Module == core.ModuleKindNone || options.GetEmitScriptTarget() >= core.ScriptTargetES2015 || options.NoEmit.IsTrue() +} + func clientSupportsItemLabelDetails(clientOptions *lsproto.CompletionClientCapabilities) bool { return hasCompletionItem(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.LabelDetailsSupport) } @@ -4727,15 +4983,40 @@ func getArgumentInfoForCompletions(node *ast.Node, position int, file *ast.Sourc } type itemData struct { - FileName string `json:"fileName"` - Position int `json:"position"` - Source string `json:"source,omitzero"` - Name string `json:"name,omitzero"` - AutoImport *autoImportData `json:"autoImport,omitzero"` + FileName string `json:"fileName"` + Position int `json:"position"` + Source string `json:"source,omitempty"` + Name string `json:"name,omitempty"` + AutoImport *completionEntryData `json:"autoImport,omitempty"` } -// !!! CompletionEntryDataAutoImport -type autoImportData struct{} +type completionEntryData struct { + /** + * The name of the property or export in the module's symbol table. Differs from the completion name + * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. + */ + ExportName string `json:"exportName"` + ExportMapKey ExportInfoMapKey `json:"exportMapKey"` + ModuleSpecifier string `json:"moduleSpecifier"` + + /** The file name declaring the export's module symbol, if it was an external module */ + FileName *string `json:"fileName"` + /** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */ + AmbientModuleName *string `json:"ambientModuleName"` + + /** True if the export was found in the package.json AutoImportProvider */ + IsPackageJsonImport core.Tristate `json:"isPackageJsonImport"` +} + +func (d *completionEntryData) toSymbolOriginExport(symbolName string, moduleSymbol *ast.Symbol, isDefaultExport bool) *symbolOriginInfoExport { + return &symbolOriginInfoExport{ + symbolName: symbolName, + moduleSymbol: moduleSymbol, + exportName: d.ExportName, + exportMapKey: d.ExportMapKey, + moduleSpecifier: d.ModuleSpecifier, + } +} // Special values for `CompletionInfo['source']` used to disambiguate // completion items with the same `name`. (Each completion item must @@ -4808,19 +5089,19 @@ func (l *LanguageService) getCompletionItemDetails( if IsInString(file, position, previousToken) { return l.getStringLiteralCompletionDetails( ctx, + checker, item, itemData.Name, file, position, contextToken, - program, preferences, ) } // Compute all the completion symbols again. - symbolCompletion := getSymbolCompletionFromItemData( - program, + symbolCompletion := l.getSymbolCompletionFromItemData( + ctx, checker, file, position, @@ -4850,7 +5131,7 @@ func (l *LanguageService) getCompletionItemDetails( } case symbolCompletion.symbol != nil: symbolDetails := symbolCompletion.symbol - actions := getCompletionItemActions(symbolDetails.symbol) + actions := l.getCompletionItemActions(ctx, checker, file, position, itemData, symbolDetails, preferences) return createCompletionDetailsForSymbol( item, symbolDetails.symbol, @@ -4892,9 +5173,9 @@ type symbolDetails struct { isTypeOnlyLocation bool } -func getSymbolCompletionFromItemData( - program *compiler.Program, - checker *checker.Checker, +func (l *LanguageService) getSymbolCompletionFromItemData( + ctx context.Context, + ch *checker.Checker, file *ast.SourceFile, position int, itemData *itemData, @@ -4907,11 +5188,16 @@ func getSymbolCompletionFromItemData( } } if itemData.AutoImport != nil { - // !!! auto-import - return detailsData{} + if autoImportSymbolData := l.getAutoImportSymbolFromCompletionEntryData(ch, itemData.AutoImport.ExportName, itemData.AutoImport); autoImportSymbolData != nil { + autoImportSymbolData.contextToken, autoImportSymbolData.previousToken = getRelevantTokens(position, file) + autoImportSymbolData.location = astnav.GetTouchingPropertyName(file, position) + autoImportSymbolData.jsxInitializer = jsxInitializer{false, nil} + autoImportSymbolData.isTypeOnlyLocation = false + return detailsData{symbol: autoImportSymbolData} + } } - completionData := getCompletionData(program, checker, file, position, preferences) + completionData := l.getCompletionData(ctx, ch, file, position, &UserPreferences{IncludeCompletionsForModuleExports: ptrTo(true), IncludeCompletionsForImportStatements: ptrTo(true)}) if completionData == nil { return detailsData{} } @@ -4966,6 +5252,49 @@ func getSymbolCompletionFromItemData( return detailsData{} } +func (l *LanguageService) getAutoImportSymbolFromCompletionEntryData(ch *checker.Checker, name string, autoImportData *completionEntryData) *symbolDetails { + containingProgram := l.GetProgram() // !!! isPackageJson ? packageJsonAutoimportProvider : program + var moduleSymbol *ast.Symbol + if autoImportData.AmbientModuleName != nil { + moduleSymbol = ch.TryFindAmbientModule(*autoImportData.AmbientModuleName) + } else if autoImportData.FileName != nil { + moduleSymbolSourceFile := containingProgram.GetSourceFile(*autoImportData.FileName) + if moduleSymbolSourceFile == nil { + panic("module sourceFile not found: " + *autoImportData.FileName) + } + moduleSymbol = ch.GetMergedSymbol(moduleSymbolSourceFile.Symbol) + } + if moduleSymbol == nil { + return nil + } + + var symbol *ast.Symbol + if autoImportData.ExportName == ast.InternalSymbolNameExportEquals { + symbol = ch.ResolveExternalModuleSymbol(moduleSymbol) + } else { + symbol = ch.TryGetMemberInModuleExportsAndProperties(autoImportData.ExportName, moduleSymbol) + } + if symbol == nil { + return nil + } + + isDefaultExport := autoImportData.ExportName == ast.InternalSymbolNameDefault + if isDefaultExport { + if localSymbol := binder.GetLocalSymbolForExportDefault(symbol); localSymbol != nil { + symbol = localSymbol + } + } + origin := &symbolOriginInfo{ + kind: symbolOriginInfoKindExport, + fileName: *autoImportData.FileName, + isFromPackageJson: autoImportData.IsPackageJsonImport.IsTrue(), + isDefaultExport: isDefaultExport, + data: autoImportData.toSymbolOriginExport(name, moduleSymbol, isDefaultExport), + } + + return &symbolDetails{symbol: symbol, origin: origin} +} + func createSimpleDetails( item *lsproto.CompletionItem, name string, @@ -5008,20 +5337,250 @@ func createCompletionDetailsForSymbol( actions []codeAction, ) *lsproto.CompletionItem { details := make([]string, 0, len(actions)+1) + edits := make([]*lsproto.TextEdit, 0, len(actions)) for _, action := range actions { details = append(details, action.description) + edits = append(edits, action.changes...) } quickInfo, documentation := getQuickInfoAndDocumentationForSymbol(checker, symbol, location) details = append(details, quickInfo) + if len(edits) != 0 { + item.AdditionalTextEdits = &edits + } return createCompletionDetails(item, strings.Join(details, "\n\n"), documentation) } -// !!! auto-import // !!! snippets -func getCompletionItemActions(symbol *ast.Symbol) []codeAction { +func (l *LanguageService) getCompletionItemActions(ctx context.Context, ch *checker.Checker, file *ast.SourceFile, position int, itemData *itemData, symbolDetails *symbolDetails, preferences *UserPreferences) []codeAction { + if itemData.AutoImport != nil && itemData.AutoImport.ModuleSpecifier != "" && symbolDetails.previousToken != nil { + // Import statement completion: 'import c|' + if symbolDetails.contextToken != nil && l.getImportStatementCompletionInfo(symbolDetails.contextToken, file).replacementSpan != nil { + return nil + } else if l.getImportStatementCompletionInfo(symbolDetails.previousToken, file).replacementSpan != nil { + return nil // !!! sourceDisplay [textPart(data.moduleSpecifier)] + } + } + // !!! CompletionSource.ClassMemberSnippet + // !!! origin.isTypeOnlyAlias + // entryId.source == CompletionSourceObjectLiteralMemberWithComma && contextToken + + if symbolDetails.origin == nil { + return nil + } + + symbol := symbolDetails.symbol + if symbol.ExportSymbol != nil { + symbol = symbol.ExportSymbol + } + targetSymbol := ch.GetMergedSymbol(ch.SkipAlias(symbol)) + isJsxOpeningTagName := symbolDetails.contextToken.Kind == ast.KindLessThanToken && ast.IsJsxOpeningLikeElement(symbolDetails.contextToken.Parent) + if symbolDetails.previousToken != nil && ast.IsIdentifier(symbolDetails.previousToken) { + // If the previous token is an identifier, we can use its start position. + position = astnav.GetStartOfNode(symbolDetails.previousToken, file, false) + } + + moduleSymbol := symbolDetails.origin.moduleSymbol() + + var exportMapkey ExportInfoMapKey + if itemData.AutoImport != nil { + exportMapkey = itemData.AutoImport.ExportMapKey + } + moduleSpecifier, importCompletionAction := l.getImportCompletionAction( + ctx, + ch, + targetSymbol, + moduleSymbol, + file, + position, + exportMapkey, + itemData.Name, + isJsxOpeningTagName, + // formatContext, + preferences, + ) + + if !(moduleSpecifier == itemData.AutoImport.ModuleSpecifier || itemData.AutoImport.ModuleSpecifier == "") { + panic("") + } + return []codeAction{importCompletionAction} +} + +func (l *LanguageService) getImportStatementCompletionInfo(contextToken *ast.Node, sourceFile *ast.SourceFile) importStatementCompletionInfo { + result := importStatementCompletionInfo{} + var candidate *ast.Node + parent := contextToken.Parent + switch { + case ast.IsImportEqualsDeclaration(parent): + // import Foo | + // import Foo f| + lastToken := lsutil.GetLastToken(parent, sourceFile) + if contextToken.Kind == ast.KindIdentifier && lastToken != contextToken { + result.keywordCompletion = ast.KindFromKeyword + result.isKeywordOnlyCompletion = true + } else { + if contextToken.Kind != ast.KindTypeKeyword { + result.keywordCompletion = ast.KindTypeKeyword + } + if isModuleSpecifierMissingOrEmpty(parent.AsImportEqualsDeclaration().ModuleReference) { + candidate = parent + } + } + + case couldBeTypeOnlyImportSpecifier(parent, contextToken) && canCompleteFromNamedBindings(parent.Parent): + candidate = parent + case ast.IsNamedImports(parent) || ast.IsNamespaceImport(parent): + if !parent.Parent.IsTypeOnly() && (contextToken.Kind == ast.KindOpenBraceToken || + contextToken.Kind == ast.KindImportKeyword || + contextToken.Kind == ast.KindCommaToken) { + result.keywordCompletion = ast.KindTypeKeyword + } + if canCompleteFromNamedBindings(parent) { + // At `import { ... } |` or `import * as Foo |`, the only possible completion is `from` + if contextToken.Kind == ast.KindCloseBraceToken || contextToken.Kind == ast.KindIdentifier { + result.isKeywordOnlyCompletion = true + result.keywordCompletion = ast.KindFromKeyword + } else { + candidate = parent.Parent.Parent + } + } + + case ast.IsExportDeclaration(parent) && contextToken.Kind == ast.KindAsteriskToken, + ast.IsNamedExports(parent) && contextToken.Kind == ast.KindCloseBraceToken: + result.isKeywordOnlyCompletion = true + result.keywordCompletion = ast.KindFromKeyword + + case contextToken.Kind == ast.KindImportKeyword: + if ast.IsSourceFile(parent) { + // A lone import keyword with nothing following it does not parse as a statement at all + result.keywordCompletion = ast.KindTypeKeyword + candidate = contextToken + } else if ast.IsImportDeclaration(parent) { + // `import s| from` + result.keywordCompletion = ast.KindTypeKeyword + if isModuleSpecifierMissingOrEmpty(parent.ModuleSpecifier()) { + candidate = parent + } + } + } + + if candidate != nil { + result.isNewIdentifierLocation = true + result.replacementSpan = l.getSingleLineReplacementSpanForImportCompletionNode(candidate) + result.couldBeTypeOnlyImportSpecifier = couldBeTypeOnlyImportSpecifier(candidate, contextToken) + if ast.IsImportDeclaration(candidate) { + result.isTopLevelTypeOnly = candidate.AsImportDeclaration().ImportClause.IsTypeOnly() + } else if candidate.Kind == ast.KindImportEqualsDeclaration { + result.isTopLevelTypeOnly = candidate.IsTypeOnly() + } + } else { + result.isNewIdentifierLocation = result.keywordCompletion == ast.KindTypeKeyword + } + return result +} + +func (l *LanguageService) getSingleLineReplacementSpanForImportCompletionNode(node *ast.Node) *lsproto.Range { + // node is ImportDeclaration | ImportEqualsDeclaration | ImportSpecifier | JSDocImportTag | Token + if ancestor := ast.FindAncestor(node, core.Or(ast.IsImportDeclaration, ast.IsImportEqualsDeclaration, ast.IsJSDocImportTag)); ancestor != nil { + node = ancestor + } + sourceFile := ast.GetSourceFileOfNode(node) + if printer.GetLinesBetweenPositions(sourceFile, node.Pos(), node.End()) == 0 { + return l.createLspRangeFromNode(node, sourceFile) + } + + if node.Kind == ast.KindImportKeyword || node.Kind == ast.KindImportSpecifier { + panic("ImportKeyword was necessarily on one line; ImportSpecifier was necessarily parented in an ImportDeclaration") + } + + // Guess which point in the import might actually be a later statement parsed as part of the import + // during parser recovery - either in the middle of named imports, or the module specifier. + var potentialSplitPoint *ast.Node + if node.Kind == ast.KindImportDeclaration || node.Kind == ast.KindJSDocImportTag { + var specifier *ast.Node + if importClause := node.ImportClause(); importClause != nil { + specifier = getPotentiallyInvalidImportSpecifier(importClause.AsImportClause().NamedBindings) + } + if specifier != nil { + potentialSplitPoint = specifier + } else { + potentialSplitPoint = node.ModuleSpecifier() + } + } else { + potentialSplitPoint = node.AsImportEqualsDeclaration().ModuleReference + } + + withoutModuleSpecifier := core.NewTextRange(scanner.GetTokenPosOfNode(lsutil.GetFirstToken(node, sourceFile), sourceFile, false), potentialSplitPoint.Pos()) + // The module specifier/reference was previously found to be missing, empty, or + // not a string literal - in this last case, it's likely that statement on a following + // line was parsed as the module specifier of a partially-typed import, e.g. + // import Foo| + // interface Blah {} + // This appears to be a multiline-import, and editors can't replace multiple lines. + // But if everything but the "module specifier" is on one line, by this point we can + // assume that the "module specifier" is actually just another statement, and return + // the single-line range of the import excluding that probable statement. + if printer.GetLinesBetweenPositions(sourceFile, withoutModuleSpecifier.Pos(), withoutModuleSpecifier.End()) == 0 { + return l.createLspRangeFromBounds(withoutModuleSpecifier.Pos(), withoutModuleSpecifier.End(), sourceFile) + } return nil } +func couldBeTypeOnlyImportSpecifier(importSpecifier *ast.Node, contextToken *ast.Node) bool { + return ast.IsImportSpecifier(importSpecifier) && (importSpecifier.IsTypeOnly() || contextToken == importSpecifier.Name() && isTypeKeywordTokenOrIdentifier(contextToken)) +} + +func canCompleteFromNamedBindings(namedBindings *ast.NamedImportBindings) bool { + if !isModuleSpecifierMissingOrEmpty(namedBindings.Parent.Parent.ModuleSpecifier()) || namedBindings.Parent.Name() != nil { + return false + } + if ast.IsNamedImports(namedBindings) { + // We can only complete on named imports if there are no other named imports already, + // but parser recovery sometimes puts later statements in the named imports list, so + // we try to only consider the probably-valid ones. + invalidNamedImport := getPotentiallyInvalidImportSpecifier(namedBindings) + elements := namedBindings.Elements() + validImports := len(elements) + if invalidNamedImport != nil { + validImports = slices.Index(elements, invalidNamedImport) + } + + return validImports < 2 && validImports > -1 + } + return true +} + +// Tries to identify the first named import that is not really a named import, but rather +// just parser recovery for a situation like: +// +// import { Foo| +// interface Bar {} +// +// in which `Foo`, `interface`, and `Bar` are all parsed as import specifiers. The caller +// will also check if this token is on a separate line from the rest of the import. +func getPotentiallyInvalidImportSpecifier(namedBindings *ast.NamedImportBindings) *ast.Node { + if namedBindings.Kind != ast.KindNamedImports { + return nil + } + return core.Find(namedBindings.Elements(), func(e *ast.Node) bool { + return e.PropertyName() == nil && isNonContextualKeyword(scanner.StringToToken(e.Name().Text())) && + astnav.FindPrecedingToken(ast.GetSourceFileOfNode(namedBindings), e.Name().Pos()).Kind != ast.KindCommaToken + }) +} + +func isModuleSpecifierMissingOrEmpty(specifier *ast.Expression) bool { + if ast.NodeIsMissing(specifier) { + return true + } + node := specifier + if ast.IsExternalModuleReference(node) { + node = node.Expression() + } + if !ast.IsStringLiteralLike(node) { + return true + } + return node.Text() == "" +} + func hasDocComment(file *ast.SourceFile, position int) bool { token := astnav.GetTokenAtPosition(file, position) return ast.FindAncestor(token, (*ast.Node).IsJSDoc) != nil diff --git a/internal/ls/constants.go b/internal/ls/constants.go new file mode 100644 index 0000000000..60f240b203 --- /dev/null +++ b/internal/ls/constants.go @@ -0,0 +1,6 @@ +package ls + +const ( + moduleSpecifierResolutionLimit = 100 + moduleSpecifierResolutionCacheAttemptLimit = 1000 +) diff --git a/internal/ls/converters.go b/internal/ls/converters.go index f41fa5ab4c..af16188501 100644 --- a/internal/ls/converters.go +++ b/internal/ls/converters.go @@ -58,13 +58,6 @@ func (c *Converters) ToLSPLocation(script Script, rng core.TextRange) lsproto.Lo } } -func (c *Converters) FromLSPLocation(script Script, rng lsproto.Range) Location { - return Location{ - FileName: script.FileName(), - Range: c.FromLSPRange(script, rng), - } -} - func LanguageKindToScriptKind(languageID lsproto.LanguageKind) core.ScriptKind { switch languageID { case "typescript": diff --git a/internal/ls/findallreferences.go b/internal/ls/findallreferences.go index 5e494c7d75..875be0442d 100644 --- a/internal/ls/findallreferences.go +++ b/internal/ls/findallreferences.go @@ -795,15 +795,13 @@ func getReferencesForThisKeyword(thisOrSuperKeyword *ast.Node, sourceFiles []*as } staticFlag &= searchSpaceNode.ModifierFlags() searchSpaceNode = searchSpaceNode.Parent // re-assign to be the owning class - break case ast.KindSourceFile: if ast.IsExternalModule(searchSpaceNode.AsSourceFile()) || isParameterName(thisOrSuperKeyword) { return nil } case ast.KindFunctionDeclaration, ast.KindFunctionExpression: - break - // Computed properties in classes are not handled here because references to this are illegal, - // so there is no point finding references to them. + // Computed properties in classes are not handled here because references to this are illegal, + // so there is no point finding references to them. default: return nil } diff --git a/internal/ls/format.go b/internal/ls/format.go index 978cab815a..548c766741 100644 --- a/internal/ls/format.go +++ b/internal/ls/format.go @@ -12,8 +12,8 @@ import ( "github.com/microsoft/typescript-go/internal/scanner" ) -func toFormatCodeSettings(opt *lsproto.FormattingOptions) *format.FormatCodeSettings { - initial := format.GetDefaultFormatCodeSettings("\n") +func toFormatCodeSettings(opt *lsproto.FormattingOptions, newLine string) *format.FormatCodeSettings { + initial := format.GetDefaultFormatCodeSettings(newLine) initial.TabSize = int(opt.TabSize) initial.IndentSize = int(opt.TabSize) initial.ConvertTabsToSpaces = opt.InsertSpaces @@ -46,7 +46,7 @@ func (l *LanguageService) ProvideFormatDocument( edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForDocument( ctx, file, - toFormatCodeSettings(options), + toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()), )) return lsproto.TextEditsOrNull{TextEdits: &edits}, nil } @@ -61,7 +61,7 @@ func (l *LanguageService) ProvideFormatDocumentRange( edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForRange( ctx, file, - toFormatCodeSettings(options), + toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()), l.converters.FromLSPRange(file, r), )) return lsproto.TextEditsOrNull{TextEdits: &edits}, nil @@ -78,7 +78,7 @@ func (l *LanguageService) ProvideFormatDocumentOnType( edits := l.toLSProtoTextEdits(file, l.getFormattingEditsAfterKeystroke( ctx, file, - toFormatCodeSettings(options), + toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()), int(l.converters.LineAndCharacterToPosition(file, position)), character, )) diff --git a/internal/ls/importTracker.go b/internal/ls/importTracker.go index 947fcab400..ac434e638d 100644 --- a/internal/ls/importTracker.go +++ b/internal/ls/importTracker.go @@ -18,14 +18,6 @@ const ( ImpExpKindExport ) -type ExportKind int32 - -const ( - ExportKindDefault ExportKind = iota - ExportKindNamed - ExportKindExportEquals -) - type ImportExportSymbol struct { kind ImpExpKind symbol *ast.Symbol diff --git a/internal/ls/organizeimports.go b/internal/ls/organizeimports.go new file mode 100644 index 0000000000..8cdeddab5d --- /dev/null +++ b/internal/ls/organizeimports.go @@ -0,0 +1,128 @@ +package ls + +import ( + "cmp" + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/compiler" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/modulespecifiers" + "github.com/microsoft/typescript-go/internal/tspath" +) + +// statement = anyImportOrRequireStatement +func getImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int { + // !!! + return len(sortedImports) +} + +// returns `-1` if `a` is better than `b` +// +// note: this sorts in descending order of preference; different than convention in other cmp-like functions +func compareModuleSpecifiers( + a *ImportFix, // !!! ImportFixWithModuleSpecifier + b *ImportFix, // !!! ImportFixWithModuleSpecifier + importingFile *ast.SourceFile, // | FutureSourceFile, + program *compiler.Program, + preferences UserPreferences, + allowsImportingSpecifier func(specifier string) bool, + toPath func(fileName string) tspath.Path, +) int { + if a.kind == ImportFixKindUseNamespace || b.kind == ImportFixKindUseNamespace { + return 0 + } + if comparison := compareBooleans( + b.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(b.moduleSpecifier), + a.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(a.moduleSpecifier), + ); comparison != 0 { + return comparison + } + if comparison := compareModuleSpecifierRelativity(a, b, preferences); comparison != 0 { + return comparison + } + if comparison := compareNodeCoreModuleSpecifiers(a.moduleSpecifier, b.moduleSpecifier, importingFile, program); comparison != 0 { + return comparison + } + if comparison := compareBooleans(isFixPossiblyReExportingImportingFile(a, importingFile.Path(), toPath), isFixPossiblyReExportingImportingFile(b, importingFile.Path(), toPath)); comparison != 0 { + return comparison + } + if comparison := compareNumberOfDirectorySeparators(a.moduleSpecifier, b.moduleSpecifier); comparison != 0 { + return comparison + } + return 0 +} + +// True > False +func compareBooleans(a, b bool) int { + if a && !b { + return -1 + } else if !a && b { + return 1 + } + return 0 +} + +// returns `-1` if `a` is better than `b` +func compareModuleSpecifierRelativity(a *ImportFix, b *ImportFix, preferences UserPreferences) int { + switch preferences.ImportModuleSpecifierPreference { + case modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative: + return compareBooleans(a.moduleSpecifierKind == modulespecifiers.ResultKindRelative, b.moduleSpecifierKind == modulespecifiers.ResultKindRelative) + } + return 0 +} + +func compareNodeCoreModuleSpecifiers(a, b string, importingFile *ast.SourceFile, program *compiler.Program) int { + if strings.HasPrefix(a, "node:") && !strings.HasPrefix(b, "node:") { + if shouldUseUriStyleNodeCoreModules(importingFile, program) { + return -1 + } + return 1 + } + if strings.HasPrefix(b, "node:") && !strings.HasPrefix(a, "node:") { + if shouldUseUriStyleNodeCoreModules(importingFile, program) { + return 1 + } + return -1 + } + return 0 +} + +func shouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) bool { + for _, node := range file.Imports() { + if core.NodeCoreModules()[node.Text()] && !core.ExclusivelyPrefixedNodeCoreModules[node.Text()] { + if strings.HasPrefix(node.Text(), "node:") { + return true + } else { + return false + } + } + } + + return program.UsesUriStyleNodeCoreModules() +} + +// This is a simple heuristic to try to avoid creating an import cycle with a barrel re-export. +// E.g., do not `import { Foo } from ".."` when you could `import { Foo } from "../Foo"`. +// This can produce false positives or negatives if re-exports cross into sibling directories +// (e.g. `export * from "../whatever"`) or are not named "index". +func isFixPossiblyReExportingImportingFile(fix *ImportFix, importingFilePath tspath.Path, toPath func(fileName string) tspath.Path) bool { + if fix.isReExport != nil && *(fix.isReExport) && + fix.exportInfo != nil && fix.exportInfo.moduleFileName != "" && isIndexFileName(fix.exportInfo.moduleFileName) { + reExportDir := toPath(tspath.GetDirectoryPath(fix.exportInfo.moduleFileName)) + return strings.HasPrefix(string(importingFilePath), string(reExportDir)) + } + return false +} + +func compareNumberOfDirectorySeparators(path1, path2 string) int { + return cmp.Compare(strings.Count(path1, "/"), strings.Count(path2, "/")) +} + +func isIndexFileName(fileName string) bool { + fileName = tspath.GetBaseFileName(fileName) + if tspath.FileExtensionIsOneOf(fileName, []string{".js", ".jsx", ".d.ts", ".ts", ".tsx"}) { + fileName = tspath.RemoveFileExtension(fileName) + } + return fileName == "index" +} diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go index c81b3a79f2..0d5573d7dc 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -46,7 +46,6 @@ func (l *LanguageService) getStringLiteralCompletions( position int, contextToken *ast.Node, compilerOptions *core.CompilerOptions, - program *compiler.Program, preferences *UserPreferences, clientOptions *lsproto.CompletionClientCapabilities, ) *lsproto.CompletionList { @@ -60,7 +59,6 @@ func (l *LanguageService) getStringLiteralCompletions( file, contextToken, position, - program, preferences) return l.convertStringLiteralCompletions( ctx, @@ -68,7 +66,6 @@ func (l *LanguageService) getStringLiteralCompletions( contextToken, file, position, - program, compilerOptions, preferences, clientOptions, @@ -83,7 +80,6 @@ func (l *LanguageService) convertStringLiteralCompletions( contextToken *ast.StringLiteralLike, file *ast.SourceFile, position int, - program *compiler.Program, options *core.CompilerOptions, preferences *UserPreferences, clientOptions *lsproto.CompletionClientCapabilities, @@ -112,7 +108,6 @@ func (l *LanguageService) convertStringLiteralCompletions( contextToken, /*replacementToken*/ position, file, - program, preferences, options, clientOptions, @@ -161,6 +156,7 @@ func (l *LanguageService) convertStringLiteralCompletions( false, /*hasAction*/ false, /*preselect*/ "", /*source*/ + nil, /*autoImportEntryData*/ ) }) defaultCommitCharacters := getDefaultCommitCharacters(completion.isNewIdentifier) @@ -210,6 +206,7 @@ func (l *LanguageService) convertPathCompletions( false, /*hasAction*/ false, /*preselect*/ "", /*source*/ + nil, /*autoImportEntryData*/ ) }) itemDefaults := l.setItemDefaults( @@ -232,10 +229,9 @@ func (l *LanguageService) getStringLiteralCompletionEntries( file *ast.SourceFile, node *ast.StringLiteralLike, position int, - program *compiler.Program, preferences *UserPreferences, ) *stringLiteralCompletions { - typeChecker, done := program.GetTypeCheckerForFile(ctx, file) + typeChecker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) defer done() parent := walkUpParentheses(node.Parent) switch parent.Kind { @@ -245,7 +241,7 @@ func (l *LanguageService) getStringLiteralCompletionEntries( return getStringLiteralCompletionsFromModuleNames( file, node, - program, + l.GetProgram(), preferences, ) } @@ -327,7 +323,7 @@ func (l *LanguageService) getStringLiteralCompletionEntries( // import x = require("/*completion position*/"); // var y = require("/*completion position*/"); // export * from "/*completion position*/"; - return getStringLiteralCompletionsFromModuleNames(file, node, program, preferences) + return getStringLiteralCompletionsFromModuleNames(file, node, l.GetProgram(), preferences) case ast.KindCaseClause: tracker := newCaseClauseTracker(typeChecker, parent.Parent.AsCaseBlock().Clauses.Nodes) contextualTypes := fromContextualType(checker.ContextFlagsCompletions, node, typeChecker) @@ -660,12 +656,12 @@ func getStringLiteralCompletionsFromSignature( func (l *LanguageService) getStringLiteralCompletionDetails( ctx context.Context, + checker *checker.Checker, item *lsproto.CompletionItem, name string, file *ast.SourceFile, position int, contextToken *ast.Node, - program *compiler.Program, preferences *UserPreferences, ) *lsproto.CompletionItem { if contextToken == nil || !ast.IsStringLiteralLike(contextToken) { @@ -676,14 +672,11 @@ func (l *LanguageService) getStringLiteralCompletionDetails( file, contextToken, position, - program, preferences, ) if completions == nil { return item } - checker, done := program.GetTypeCheckerForFile(ctx, file) - defer done() return stringLiteralCompletionDetails(item, name, contextToken, completions, file, checker) } diff --git a/internal/ls/types.go b/internal/ls/types.go index 226202d504..6aafc4bbbb 100644 --- a/internal/ls/types.go +++ b/internal/ls/types.go @@ -1,14 +1,9 @@ package ls import ( - "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/modulespecifiers" ) -type Location struct { - FileName string - Range core.TextRange -} - type JsxAttributeCompletionStyle string const ( @@ -18,6 +13,10 @@ const ( ) type UserPreferences struct { + // If enabled, TypeScript will search through all external modules' exports and add them to the completions list. + // This affects lone identifier completions but not completions on the right hand side of `obj.`. + IncludeCompletionsForModuleExports *bool + // Enables auto-import-style completions on partially-typed import statements. E.g., allows // `import write|` to be completed to `import { writeFile } from "fs"`. IncludeCompletionsForImportStatements *bool @@ -40,4 +39,18 @@ type UserPreferences struct { IncludeCompletionsWithObjectLiteralMethodSnippets *bool JsxAttributeCompletionStyle *JsxAttributeCompletionStyle + + ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference + ImportModuleSpecifierEndingPreference modulespecifiers.ImportModuleSpecifierEndingPreference + PreferTypeOnlyAutoImports *bool + AutoImportSpecifierExcludeRegexes []string + AutoImportFileExcludePatterns []string +} + +func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences { + return modulespecifiers.UserPreferences{ + ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference, + ImportModuleSpecifierEndingPreference: p.ImportModuleSpecifierEndingPreference, + AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes, + } } diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index f35b3364d8..b4f888fd12 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -6,6 +6,7 @@ import ( "iter" "slices" "strings" + "unicode" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" @@ -18,6 +19,7 @@ import ( "github.com/microsoft/typescript-go/internal/lsutil" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/tspath" ) // Implements a cmp.Compare like function for two lsproto.Position @@ -118,6 +120,45 @@ func getNonModuleSymbolOfMergedModuleSymbol(symbol *ast.Symbol) *ast.Symbol { return nil } +func moduleSymbolToValidIdentifier(moduleSymbol *ast.Symbol, target core.ScriptTarget, forceCapitalize bool) string { + return moduleSpecifierToValidIdentifier(stringutil.StripQuotes(moduleSymbol.Name), target, forceCapitalize) +} + +func moduleSpecifierToValidIdentifier(moduleSpecifier string, target core.ScriptTarget, forceCapitalize bool) string { + baseName := tspath.GetBaseFileName(strings.TrimSuffix(tspath.RemoveFileExtension(moduleSpecifier), "/index")) + res := []rune{} + lastCharWasValid := true + baseNameRunes := []rune(baseName) + if len(baseNameRunes) > 0 && scanner.IsIdentifierStart(baseNameRunes[0]) { + if forceCapitalize { + res = append(res, unicode.ToUpper(baseNameRunes[0])) + } else { + res = append(res, baseNameRunes[0]) + } + } else { + lastCharWasValid = false + } + + for i := 1; i < len(baseNameRunes); i++ { + isValid := scanner.IsIdentifierPart(baseNameRunes[i]) + if isValid { + if !lastCharWasValid { + res = append(res, unicode.ToUpper(baseNameRunes[i])) + } else { + res = append(res, baseNameRunes[i]) + } + } + lastCharWasValid = isValid + } + + // Need `"_"` to ensure result isn't empty. + resString := string(res) + if resString != "" && !isNonContextualKeyword(scanner.StringToToken(resString)) { + return resString + } + return "_" + resString +} + func getLocalSymbolForExportSpecifier(referenceLocation *ast.Identifier, referenceSymbol *ast.Symbol, exportSpecifier *ast.ExportSpecifier, ch *checker.Checker) *ast.Symbol { if isExportSpecifierAlias(referenceLocation, exportSpecifier) { if symbol := ch.GetExportSpecifierLocalTargetSymbol(exportSpecifier.AsNode()); symbol != nil { @@ -522,6 +563,10 @@ func isTypeKeyword(kind ast.Kind) bool { return typeKeywords.Has(kind) } +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)) +} + // Returns a map of all names in the file to their positions. // !!! cache this func getNameTable(file *ast.SourceFile) map[string]int { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 2e319cc544..165b0972db 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -757,7 +757,10 @@ func (s *Server) handleCompletion(ctx context.Context, languageService *ls.Langu params.Position, params.Context, getCompletionClientCapabilities(s.initializeParams), - &ls.UserPreferences{}) + &ls.UserPreferences{ + IncludeCompletionsForModuleExports: ptrTo(true), + IncludeCompletionsForImportStatements: ptrTo(true), + }) } func (s *Server) handleCompletionItemResolve(ctx context.Context, params *lsproto.CompletionItem, reqMsg *lsproto.RequestMessage) (lsproto.CompletionResolveResponse, error) { @@ -775,7 +778,10 @@ func (s *Server) handleCompletionItemResolve(ctx context.Context, params *lsprot params, data, getCompletionClientCapabilities(s.initializeParams), - &ls.UserPreferences{}, + &ls.UserPreferences{ + IncludeCompletionsForModuleExports: ptrTo(true), + IncludeCompletionsForImportStatements: ptrTo(true), + }, ) } diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 8cdcfc6b7d..1f8e26b040 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -24,16 +24,42 @@ func GetModuleSpecifiers( host ModuleSpecifierGenerationHost, userPreferences UserPreferences, options ModuleSpecifierOptions, + forAutoImports bool, ) []string { + result, _ := GetModuleSpecifiersWithInfo( + moduleSymbol, + checker, + compilerOptions, + importingSourceFile, + host, + userPreferences, + options, + forAutoImports, + ) + return result +} + +func GetModuleSpecifiersWithInfo( + moduleSymbol *ast.Symbol, + checker CheckerShape, + compilerOptions *core.CompilerOptions, + importingSourceFile SourceFileForSpecifierGeneration, + host ModuleSpecifierGenerationHost, + userPreferences UserPreferences, + options ModuleSpecifierOptions, + forAutoImports bool, +) ([]string, ResultKind) { ambient := tryGetModuleNameFromAmbientModule(moduleSymbol, checker) if len(ambient) > 0 { - return []string{ambient} + // !!! todo forAutoImport + return []string{ambient}, ResultKindAmbient } moduleSourceFile := ast.GetSourceFileOfModule(moduleSymbol) if moduleSourceFile == nil { - return nil + return nil, ResultKindNone } + modulePaths := getAllModulePathsWorker( getInfo(host.GetSourceOfProjectReferenceIfOutputIncluded(importingSourceFile), host), moduleSourceFile.FileName(), @@ -42,17 +68,15 @@ func GetModuleSpecifiers( // options, ) - result := computeModuleSpecifiers( + return computeModuleSpecifiers( modulePaths, compilerOptions, importingSourceFile, host, userPreferences, options, - /*forAutoImport*/ false, + forAutoImports, ) - - return result } func tryGetModuleNameFromAmbientModule(moduleSymbol *ast.Symbol, checker CheckerShape) string { @@ -123,6 +147,29 @@ func getInfo( } } +func getAllModulePaths( + info Info, + importedFileName string, + host ModuleSpecifierGenerationHost, + compilerOptions *core.CompilerOptions, + preferences UserPreferences, + options ModuleSpecifierOptions, +) []ModulePath { + // !!! use new cache model + // importingFilePath := tspath.ToPath(info.ImportingSourceFileName, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()); + // importedFilePath := tspath.ToPath(importedFileName, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()); + // cache := host.getModuleSpecifierCache(); + // if (cache != nil) { + // cached := cache.get(importingFilePath, importedFilePath, preferences, options); + // if (cached.modulePaths) {return cached.modulePaths;} + // } + modulePaths := getAllModulePathsWorker(info, importedFileName, host) // , compilerOptions, options); + // if (cache != nil) { + // cache.setModulePaths(importingFilePath, importedFilePath, preferences, options, modulePaths); + // } + return modulePaths +} + func getAllModulePathsWorker( info Info, importedFileName string, @@ -150,7 +197,7 @@ func getAllModulePathsWorker( // } allFileNames := make(map[string]ModulePath) - paths := getEachFileNameOfModule(info.ImportingSourceFileName, importedFileName, host, true) + paths := GetEachFileNameOfModule(info.ImportingSourceFileName, importedFileName, host, true) for _, p := range paths { allFileNames[p.FileName] = p } @@ -190,11 +237,11 @@ func containsIgnoredPath(s string) bool { strings.Contains(s, "/.#") } -func containsNodeModules(s string) bool { +func ContainsNodeModules(s string) bool { return strings.Contains(s, "/node_modules/") } -func getEachFileNameOfModule( +func GetEachFileNameOfModule( importingFileName string, importedFileName string, host ModuleSpecifierGenerationHost, @@ -226,7 +273,7 @@ func getEachFileNameOfModule( if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { results = append(results, ModulePath{ FileName: p, - IsInNodeModules: containsNodeModules(p), + IsInNodeModules: ContainsNodeModules(p), IsRedirect: referenceRedirect == p, }) } @@ -269,7 +316,7 @@ func getEachFileNameOfModule( if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { results = append(results, ModulePath{ FileName: p, - IsInNodeModules: containsNodeModules(p), + IsInNodeModules: ContainsNodeModules(p), IsRedirect: referenceRedirect == p, }) } @@ -287,7 +334,7 @@ func computeModuleSpecifiers( userPreferences UserPreferences, options ModuleSpecifierOptions, forAutoImport bool, -) []string { +) ([]string, ResultKind) { info := getInfo(importingSourceFile.FileName(), host) preferences := getModuleSpecifierPreferences(userPreferences, host, compilerOptions, importingSourceFile, "") @@ -322,7 +369,7 @@ func computeModuleSpecifiers( } if existingSpecifier != "" { - return []string{existingSpecifier} + return []string{existingSpecifier}, ResultKindNone } importedFileIsInNodeModules := core.Some(modulePaths, func(p ModulePath) bool { return p.IsInNodeModules }) @@ -347,7 +394,7 @@ func computeModuleSpecifiers( if modulePath.IsRedirect { // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar", // not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking. - return nodeModulesSpecifiers + return nodeModulesSpecifiers, ResultKindNodeModules } } @@ -369,8 +416,8 @@ func computeModuleSpecifiers( } if modulePath.IsRedirect { redirectPathsSpecifiers = append(redirectPathsSpecifiers, local) - } else if pathIsBareSpecifier(local) { - if containsNodeModules(local) { + } else if PathIsBareSpecifier(local) { + if ContainsNodeModules(local) { // We could be in this branch due to inappropriate use of `baseUrl`, not intentional `paths` // usage. It's impossible to reason about where to prioritize baseUrl-generated module // specifiers, but if they contain `/node_modules/`, they're going to trigger a portability @@ -394,15 +441,15 @@ func computeModuleSpecifiers( } if len(pathsSpecifiers) > 0 { - return pathsSpecifiers + return pathsSpecifiers, ResultKindPaths } if len(redirectPathsSpecifiers) > 0 { - return redirectPathsSpecifiers + return redirectPathsSpecifiers, ResultKindRedirect } if len(nodeModulesSpecifiers) > 0 { - return nodeModulesSpecifiers + return nodeModulesSpecifiers, ResultKindNodeModules } - return relativeSpecifiers + return relativeSpecifiers, ResultKindRelative } func getLocalModuleSpecifier( @@ -644,7 +691,7 @@ func tryGetModuleNameAsNodeModule( packageNameOnly bool, overrideMode core.ResolutionMode, ) string { - parts := getNodeModulePathParts(pathObj.FileName) + parts := GetNodeModulePathParts(pathObj.FileName) if parts == nil { return "" } @@ -713,7 +760,7 @@ func tryGetModuleNameAsNodeModule( // If the module was found in @types, get the actual Node package name nodeModulesDirectoryName := moduleSpecifier[parts.TopLevelPackageNameIndex+1:] - return getPackageNameFromTypesPackageName(nodeModulesDirectoryName) + return GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) } type pkgJsonDirAttemptResult struct { @@ -764,7 +811,7 @@ func tryDirectoryWithPackageJson( // name in the package.json content via url/filepath dependency specifiers. We need to // use the actual directory name, so don't look at `packageJsonContent.name` here. nodeModulesDirectoryName := packageRootPath[parts.TopLevelPackageNameIndex+1:] - packageName := getPackageNameFromTypesPackageName(nodeModulesDirectoryName) + packageName := GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) conditions := module.GetConditions(options, importMode) var fromExports string @@ -1202,3 +1249,38 @@ func tryGetModuleNameFromExportsOrImports( } return "" } + +// `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? +// Because when this is called by the declaration emitter, `importingSourceFile` is the implementation +// file, but `importingSourceFileName` and `toFileName` refer to declaration files (the former to the +// one currently being produced; the latter to the one being imported). We need an implementation file +// just to get its `impliedNodeFormat` and to detect certain preferences from existing import module +// specifiers. +func GetModuleSpecifier( + compilerOptions *core.CompilerOptions, + host ModuleSpecifierGenerationHost, + importingSourceFile *ast.SourceFile, // !!! | FutureSourceFile + importingSourceFileName string, + oldImportSpecifier string, // used only in updatingModuleSpecifier + toFileName string, + options ModuleSpecifierOptions, +) string { + userPreferences := UserPreferences{} + info := getInfo(importingSourceFileName, host) + modulePaths := getAllModulePaths(info, toFileName, host, compilerOptions, userPreferences, options) + preferences := getModuleSpecifierPreferences(userPreferences, host, compilerOptions, importingSourceFile, oldImportSpecifier) + + resolutionMode := options.OverrideImportMode + if resolutionMode == core.ResolutionModeNone { + resolutionMode = host.GetDefaultResolutionModeForFile(importingSourceFile) + } + + for _, modulePath := range modulePaths { + if firstDefined := tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences, false /*packageNameOnly*/, options.OverrideImportMode); len(firstDefined) > 0 { + return firstDefined + } else if firstDefined := getLocalModuleSpecifier(toFileName, info, compilerOptions, host, resolutionMode, preferences, false); len(firstDefined) > 0 { + return firstDefined + } + } + return "" +} diff --git a/internal/modulespecifiers/util.go b/internal/modulespecifiers/util.go index 6abb7d3605..2203d23113 100644 --- a/internal/modulespecifiers/util.go +++ b/internal/modulespecifiers/util.go @@ -30,7 +30,7 @@ func comparePathsByRedirectAndNumberOfDirectorySeparators(a ModulePath, b Module return -1 } -func pathIsBareSpecifier(path string) bool { +func PathIsBareSpecifier(path string) bool { return !tspath.PathIsAbsolute(path) && !tspath.PathIsRelative(path) } @@ -61,7 +61,7 @@ func isExcludedByRegex(moduleSpecifier string, excludes []string) bool { * */ func ensurePathIsNonModuleName(path string) string { - if pathIsBareSpecifier(path) { + if PathIsBareSpecifier(path) { return "./" + path } return path @@ -234,7 +234,7 @@ const ( nodeModulesPathParseStatePackageContent ) -func getNodeModulePathParts(fullPath string) *NodeModulePathParts { +func GetNodeModulePathParts(fullPath string) *NodeModulePathParts { // If fullPath can't be valid module file within node_modules, returns undefined. // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js // Returns indices: ^ ^ ^ ^ @@ -287,7 +287,25 @@ func getNodeModulePathParts(fullPath string) *NodeModulePathParts { return nil } -func getPackageNameFromTypesPackageName(mangledName string) string { +func GetNodeModulesPackageName( + compilerOptions *core.CompilerOptions, + importingSourceFile *ast.SourceFile, // !!! | FutureSourceFile + nodeModulesFileName string, + host ModuleSpecifierGenerationHost, + preferences UserPreferences, + options ModuleSpecifierOptions, +) string { + info := getInfo(importingSourceFile.FileName(), host) + modulePaths := getAllModulePaths(info, nodeModulesFileName, host, compilerOptions, preferences, options) + for _, modulePath := range modulePaths { + if result := tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, preferences, true /*packageNameOnly*/, options.OverrideImportMode); len(result) > 0 { + return result + } + } + return "" +} + +func GetPackageNameFromTypesPackageName(mangledName string) string { withoutAtTypePrefix := strings.TrimPrefix(mangledName, "@types/") if withoutAtTypePrefix != mangledName { return module.UnmangleScopedPackageName(withoutAtTypePrefix) diff --git a/internal/parser/jsdoc.go b/internal/parser/jsdoc.go index a2a630ac96..875ab54f61 100644 --- a/internal/parser/jsdoc.go +++ b/internal/parser/jsdoc.go @@ -39,7 +39,7 @@ func (p *Parser) withJSDoc(node *ast.Node, hasJSDoc bool) []*ast.Node { } // Should only be called once per node p.hasDeprecatedTag = false - ranges := getJSDocCommentRanges(&p.factory, p.jsdocCommentRangesSpace, node, p.sourceText) + ranges := GetJSDocCommentRanges(&p.factory, p.jsdocCommentRangesSpace, node, p.sourceText) p.jsdocCommentRangesSpace = ranges[:0] jsdoc := p.nodeSlicePool.NewSlice(len(ranges))[:0] pos := node.Pos() diff --git a/internal/parser/utilities.go b/internal/parser/utilities.go index ca9715f247..ee31ae4704 100644 --- a/internal/parser/utilities.go +++ b/internal/parser/utilities.go @@ -25,7 +25,7 @@ func tokenIsIdentifierOrKeywordOrGreaterThan(token ast.Kind) bool { return token == ast.KindGreaterThanToken || tokenIsIdentifierOrKeyword(token) } -func getJSDocCommentRanges(f *ast.NodeFactory, commentRanges []ast.CommentRange, node *ast.Node, text string) []ast.CommentRange { +func GetJSDocCommentRanges(f *ast.NodeFactory, commentRanges []ast.CommentRange, node *ast.Node, text string) []ast.CommentRange { switch node.Kind { case ast.KindParameter, ast.KindTypeParameter, ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindParenthesizedExpression, ast.KindVariableDeclaration, ast.KindExportSpecifier: for commentRange := range scanner.GetTrailingCommentRanges(f, text, node.Pos()) { diff --git a/internal/printer/changetrackerwriter.go b/internal/printer/changetrackerwriter.go new file mode 100644 index 0000000000..11b3f681ae --- /dev/null +++ b/internal/printer/changetrackerwriter.go @@ -0,0 +1,231 @@ +package printer + +import ( + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/stringutil" +) + +type ChangeTrackerWriter struct { + textWriter + lastNonTriviaPosition int + pos map[triviaPositionKey]int + end map[triviaPositionKey]int +} + +type triviaPositionKey interface { // *astNode | *ast.NodeList + Pos() int + End() int +} + +func NewChangeTrackerWriter(newline string) *ChangeTrackerWriter { + ctw := &ChangeTrackerWriter{ + textWriter: textWriter{newLine: newline}, + lastNonTriviaPosition: 0, + pos: map[triviaPositionKey]int{}, + end: map[triviaPositionKey]int{}, + } + ctw.textWriter.Clear() + return ctw +} + +func (ct *ChangeTrackerWriter) GetPrintHandlers() PrintHandlers { + return PrintHandlers{ + OnBeforeEmitNode: func(nodeOpt *ast.Node) { + if nodeOpt != nil { + ct.setPos(nodeOpt) + } + }, + OnAfterEmitNode: func(nodeOpt *ast.Node) { + if nodeOpt != nil { + ct.setEnd(nodeOpt) + } + }, + OnBeforeEmitNodeList: func(nodesOpt *ast.NodeList) { + if nodesOpt != nil { + ct.setPos(nodesOpt) + } + }, + OnAfterEmitNodeList: func(nodesOpt *ast.NodeList) { + if nodesOpt != nil { + ct.setEnd(nodesOpt) + } + }, + OnBeforeEmitToken: func(nodeOpt *ast.TokenNode) { + if nodeOpt != nil { + ct.setPos(nodeOpt) + } + }, + OnAfterEmitToken: func(nodeOpt *ast.TokenNode) { + if nodeOpt != nil { + ct.setEnd(nodeOpt) + } + }, + } +} + +func (ct *ChangeTrackerWriter) setPos(node triviaPositionKey) { + ct.pos[node] = ct.lastNonTriviaPosition +} + +func (ct *ChangeTrackerWriter) setEnd(node triviaPositionKey) { + ct.end[node] = ct.lastNonTriviaPosition +} + +func (ct *ChangeTrackerWriter) getPos(node triviaPositionKey) int { + return ct.pos[node] +} + +func (ct *ChangeTrackerWriter) getEnd(node triviaPositionKey) int { + return ct.end[node] +} + +func (ct *ChangeTrackerWriter) setLastNonTriviaPosition(s string, force bool) { + if force || scanner.SkipTrivia(s, 0) != len(s) { + ct.lastNonTriviaPosition = ct.textWriter.GetTextPos() + i := 0 + for stringutil.IsWhiteSpaceLike(rune(s[len(s)-i-1])) { + i++ + } + // trim trailing whitespaces + ct.lastNonTriviaPosition -= i + } +} + +func (ct *ChangeTrackerWriter) AssignPositionsToNode(node *ast.Node, factory *ast.NodeFactory) *ast.Node { + var visitor *ast.NodeVisitor + visitor = &ast.NodeVisitor{ + Visit: func(n *ast.Node) *ast.Node { return ct.assignPositionsToNodeWorker(n, visitor) }, + Factory: factory, + Hooks: ast.NodeVisitorHooks{ + VisitNode: ct.assignPositionsToNodeWorker, + VisitNodes: ct.assignPositionsToNodeArray, + VisitToken: ct.assignPositionsToNodeWorker, + VisitModifiers: func(modifiers *ast.ModifierList, v *ast.NodeVisitor) *ast.ModifierList { + if modifiers != nil { + ct.assignPositionsToNodeArray(&modifiers.NodeList, v) + } + return modifiers + }, + }, + } + return ct.assignPositionsToNodeWorker(node, visitor) +} + +func (ct *ChangeTrackerWriter) assignPositionsToNodeWorker( + node *ast.Node, + v *ast.NodeVisitor, +) *ast.Node { + if node == nil { + return node + } + visited := node.VisitEachChild(v) + // create proxy node for non synthesized nodes + newNode := visited + if !ast.NodeIsSynthesized(visited) { + newNode = visited.Clone(v.Factory) + } + newNode.ForEachChild(func(child *ast.Node) bool { + child.Parent = newNode + return true + }) + newNode.Loc = core.NewTextRange(ct.getPos(node), ct.getEnd(node)) + return newNode +} + +func (ct *ChangeTrackerWriter) assignPositionsToNodeArray( + nodes *ast.NodeList, + v *ast.NodeVisitor, +) *ast.NodeList { + visited := v.VisitNodes(nodes) + if visited == nil { + return visited + } + if nodes == nil { + // Debug.assert(nodes); + panic("if nodes is nil, visited should not be nil") + } + // clone nodearray if necessary + nodeArray := visited + if visited == nodes { + nodeArray = visited.Clone(v.Factory) + } + + nodeArray.Loc = core.NewTextRange(ct.getPos(nodes), ct.getEnd(nodes)) + return nodeArray +} + +func (ct *ChangeTrackerWriter) Write(text string) { + ct.textWriter.Write(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteTrailingSemicolon(text string) { + ct.textWriter.WriteTrailingSemicolon(text) + ct.setLastNonTriviaPosition(text, false) +} +func (ct *ChangeTrackerWriter) WriteComment(text string) { ct.textWriter.WriteComment(text) } +func (ct *ChangeTrackerWriter) WriteKeyword(text string) { + ct.textWriter.WriteKeyword(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteOperator(text string) { + ct.textWriter.WriteOperator(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WritePunctuation(text string) { + ct.textWriter.WritePunctuation(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteSpace(text string) { + ct.textWriter.WriteSpace(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteStringLiteral(text string) { + ct.textWriter.WriteStringLiteral(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteParameter(text string) { + ct.textWriter.WriteParameter(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteProperty(text string) { + ct.textWriter.WriteProperty(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteSymbol(text string, symbol *ast.Symbol) { + ct.textWriter.WriteSymbol(text, symbol) + ct.setLastNonTriviaPosition(text, false) +} +func (ct *ChangeTrackerWriter) WriteLine() { ct.textWriter.WriteLine() } +func (ct *ChangeTrackerWriter) WriteLineForce(force bool) { ct.textWriter.WriteLineForce(force) } +func (ct *ChangeTrackerWriter) IncreaseIndent() { ct.textWriter.IncreaseIndent() } +func (ct *ChangeTrackerWriter) DecreaseIndent() { ct.textWriter.DecreaseIndent() } +func (ct *ChangeTrackerWriter) Clear() { ct.textWriter.Clear(); ct.lastNonTriviaPosition = 0 } +func (ct *ChangeTrackerWriter) String() string { return ct.textWriter.String() } +func (ct *ChangeTrackerWriter) RawWrite(s string) { + ct.textWriter.RawWrite(s) + ct.setLastNonTriviaPosition(s, false) +} + +func (ct *ChangeTrackerWriter) WriteLiteral(s string) { + ct.textWriter.WriteLiteral(s) + ct.setLastNonTriviaPosition(s, true) +} +func (ct *ChangeTrackerWriter) GetTextPos() int { return ct.textWriter.GetTextPos() } +func (ct *ChangeTrackerWriter) GetLine() int { return ct.textWriter.GetLine() } +func (ct *ChangeTrackerWriter) GetColumn() int { return ct.textWriter.GetColumn() } +func (ct *ChangeTrackerWriter) GetIndent() int { return ct.textWriter.GetIndent() } +func (ct *ChangeTrackerWriter) IsAtStartOfLine() bool { return ct.textWriter.IsAtStartOfLine() } +func (ct *ChangeTrackerWriter) HasTrailingComment() bool { return ct.textWriter.HasTrailingComment() } +func (ct *ChangeTrackerWriter) HasTrailingWhitespace() bool { + return ct.textWriter.HasTrailingWhitespace() +} diff --git a/internal/printer/printer.go b/internal/printer/printer.go index fe6aaf365a..c98583b6cc 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -46,8 +46,8 @@ type PrinterOptions struct { OnlyPrintJSDocStyle bool NeverAsciiEscape bool // StripInternal bool - PreserveSourceNewlines bool - // TerminateUnterminatedLiterals bool + PreserveSourceNewlines bool + TerminateUnterminatedLiterals bool // !!! } type PrintHandlers struct { @@ -725,7 +725,7 @@ func (p *Printer) shouldEmitComments(node *ast.Node) bool { func (p *Printer) shouldWriteComment(comment ast.CommentRange) bool { return !p.Options.OnlyPrintJSDocStyle || p.currentSourceFile != nil && isJSDocLikeText(p.currentSourceFile.Text(), comment) || - p.currentSourceFile != nil && isPinnedComment(p.currentSourceFile.Text(), comment) + p.currentSourceFile != nil && IsPinnedComment(p.currentSourceFile.Text(), comment) } func (p *Printer) shouldEmitIndented(node *ast.Node) bool { @@ -4107,7 +4107,7 @@ func (p *Printer) emitJsxAttributeLike(node *ast.JsxAttributeLike) { func (p *Printer) emitJsxExpression(node *ast.JsxExpression) { state := p.enterNode(node.AsNode()) if node.Expression != nil || !p.commentsDisabled && !ast.NodeIsSynthesized(node.AsNode()) && p.hasCommentsAtPosition(node.Pos()) { // preserve empty expressions if they contain comments! - indented := p.currentSourceFile != nil && !ast.NodeIsSynthesized(node.AsNode()) && getLinesBetweenPositions(p.currentSourceFile, node.Pos(), node.End()) != 0 + indented := p.currentSourceFile != nil && !ast.NodeIsSynthesized(node.AsNode()) && GetLinesBetweenPositions(p.currentSourceFile, node.Pos(), node.End()) != 0 p.increaseIndentIf(indented) end := p.emitToken(ast.KindOpenBraceToken, node.Pos(), WriteKindPunctuation, node.AsNode()) p.emitTokenNode(node.DotDotDotToken) @@ -5340,7 +5340,7 @@ func (p *Printer) emitDetachedComments(textRange core.TextRange) (result detache // var x = 10; if textRange.Pos() == 0 { for comment := range scanner.GetLeadingCommentRanges(p.emitContext.Factory.AsNodeFactory(), text, textRange.Pos()) { - if isPinnedComment(text, comment) { + if IsPinnedComment(text, comment) { leadingComments = append(leadingComments, comment) } } @@ -5443,7 +5443,7 @@ func (p *Printer) emitComment(comment ast.CommentRange) { func (p *Printer) isTripleSlashComment(comment ast.CommentRange) bool { return p.currentSourceFile != nil && - isRecognizedTripleSlashComment(p.currentSourceFile.Text(), comment) + IsRecognizedTripleSlashComment(p.currentSourceFile.Text(), comment) } // diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index 9eac7f1b89..0b28e865ab 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -362,10 +362,10 @@ func getStartPositionOfRange(r core.TextRange, sourceFile *ast.SourceFile, inclu } func positionsAreOnSameLine(pos1 int, pos2 int, sourceFile *ast.SourceFile) bool { - return getLinesBetweenPositions(sourceFile, pos1, pos2) == 0 + return GetLinesBetweenPositions(sourceFile, pos1, pos2) == 0 } -func getLinesBetweenPositions(sourceFile *ast.SourceFile, pos1 int, pos2 int) int { +func GetLinesBetweenPositions(sourceFile *ast.SourceFile, pos1 int, pos2 int) int { if pos1 == pos2 { return 0 } @@ -384,18 +384,18 @@ func getLinesBetweenPositions(sourceFile *ast.SourceFile, pos1 int, pos2 int) in func getLinesBetweenRangeEndAndRangeStart(range1 core.TextRange, range2 core.TextRange, sourceFile *ast.SourceFile, includeSecondRangeComments bool) int { range2Start := getStartPositionOfRange(range2, sourceFile, includeSecondRangeComments) - return getLinesBetweenPositions(sourceFile, range1.End(), range2Start) + return GetLinesBetweenPositions(sourceFile, range1.End(), range2Start) } func getLinesBetweenPositionAndPrecedingNonWhitespaceCharacter(pos int, stopPos int, sourceFile *ast.SourceFile, includeComments bool) int { startPos := scanner.SkipTriviaEx(sourceFile.Text(), pos, &scanner.SkipTriviaOptions{StopAtComments: includeComments}) prevPos := getPreviousNonWhitespacePosition(startPos, stopPos, sourceFile) - return getLinesBetweenPositions(sourceFile, core.IfElse(prevPos >= 0, prevPos, stopPos), startPos) + return GetLinesBetweenPositions(sourceFile, core.IfElse(prevPos >= 0, prevPos, stopPos), startPos) } func getLinesBetweenPositionAndNextNonWhitespaceCharacter(pos int, stopPos int, sourceFile *ast.SourceFile, includeComments bool) int { nextPos := scanner.SkipTriviaEx(sourceFile.Text(), pos, &scanner.SkipTriviaOptions{StopAtComments: includeComments}) - return getLinesBetweenPositions(sourceFile, pos, core.IfElse(stopPos < nextPos, stopPos, nextPos)) + return GetLinesBetweenPositions(sourceFile, pos, core.IfElse(stopPos < nextPos, stopPos, nextPos)) } func getPreviousNonWhitespacePosition(pos int, stopPos int, sourceFile *ast.SourceFile) int { @@ -817,7 +817,7 @@ func matchQuotedString(text string, pos *int) bool { // /// // /// // /// -func isRecognizedTripleSlashComment(text string, commentRange ast.CommentRange) bool { +func IsRecognizedTripleSlashComment(text string, commentRange ast.CommentRange) bool { if commentRange.Kind == ast.KindSingleLineCommentTrivia && commentRange.Len() > 2 && text[commentRange.Pos()+1] == '/' && @@ -881,7 +881,7 @@ func isJSDocLikeText(text string, comment ast.CommentRange) bool { text[comment.Pos()+3] != '/' } -func isPinnedComment(text string, comment ast.CommentRange) bool { +func IsPinnedComment(text string, comment ast.CommentRange) bool { return comment.Kind == ast.KindMultiLineCommentTrivia && comment.Len() > 5 && text[comment.Pos()+2] == '!' diff --git a/internal/printer/utilities_test.go b/internal/printer/utilities_test.go index 02281a0f8b..1d19afe4f1 100644 --- a/internal/printer/utilities_test.go +++ b/internal/printer/utilities_test.go @@ -146,7 +146,7 @@ func TestIsRecognizedTripleSlashComment(t *testing.T) { commentRange.Kind = ast.KindSingleLineCommentTrivia commentRange.TextRange = core.NewTextRange(0, len(rec.s)) } - actual := isRecognizedTripleSlashComment(rec.s, commentRange) + actual := IsRecognizedTripleSlashComment(rec.s, commentRange) assert.Equal(t, actual, rec.expected) }) } diff --git a/internal/tspath/path.go b/internal/tspath/path.go index 64a12c5da5..7e726dfa00 100644 --- a/internal/tspath/path.go +++ b/internal/tspath/path.go @@ -973,6 +973,23 @@ func FileExtensionIs(path string, extension string) bool { return len(path) > len(extension) && strings.HasSuffix(path, extension) } +// Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. +// Stops at global cache location +func ForEachAncestorDirectoryStoppingAtGlobalCache[T any]( + globalCacheLocation string, + directory string, + callback func(directory string) (result T, stop bool), +) T { + result, _ := ForEachAncestorDirectory(directory, func(ancestorDirectory string) (T, bool) { + result, stop := callback(ancestorDirectory) + if stop || ancestorDirectory == globalCacheLocation { + return result, true + } + return result, false + }) + return result +} + func ForEachAncestorDirectory[T any](directory string, callback func(directory string) (result T, stop bool)) (result T, ok bool) { for { result, stop := callback(directory) diff --git a/testdata/baselines/reference/fourslash/autoImport/AutoImportCompletion1.baseline.md b/testdata/baselines/reference/fourslash/autoImport/AutoImportCompletion1.baseline.md new file mode 100644 index 0000000000..75c9179fb0 --- /dev/null +++ b/testdata/baselines/reference/fourslash/autoImport/AutoImportCompletion1.baseline.md @@ -0,0 +1,15 @@ +// === Auto Imports === +```ts +// @FileName: /c.ts +import {someVar} from "./a.ts"; +someVar; +a/**/ + +``````ts +import {someVar} from "./a.ts"; +import { anotherVar } from "./b.js"; +someVar; +a + +``` + diff --git a/testdata/baselines/reference/fourslash/autoImport/AutoImportCompletion2.baseline.md b/testdata/baselines/reference/fourslash/autoImport/AutoImportCompletion2.baseline.md new file mode 100644 index 0000000000..1fff3fc8ff --- /dev/null +++ b/testdata/baselines/reference/fourslash/autoImport/AutoImportCompletion2.baseline.md @@ -0,0 +1,14 @@ +// === Auto Imports === +```ts +// @FileName: /c.ts +import {someVar} from "./a.ts"; +someVar; +a/**/ + +``````ts +import {someVar,anotherVar} from "./a.ts"; +someVar; +a + +``` + diff --git a/testdata/baselines/reference/fourslash/autoImport/AutoImportCompletion3.baseline.md b/testdata/baselines/reference/fourslash/autoImport/AutoImportCompletion3.baseline.md new file mode 100644 index 0000000000..e565df0c68 --- /dev/null +++ b/testdata/baselines/reference/fourslash/autoImport/AutoImportCompletion3.baseline.md @@ -0,0 +1,14 @@ +// === Auto Imports === +```ts +// @FileName: /c.ts +import { aa, someVar } from "./a.ts"; +someVar; +b/**/ + +``````ts +import { aa, someVar,bb } from "./a.ts"; +someVar; +b + +``` + diff --git a/testdata/baselines/reference/fourslash/autoImport/NodeModulesImportCompletions1Baseline.baseline.md b/testdata/baselines/reference/fourslash/autoImport/NodeModulesImportCompletions1Baseline.baseline.md new file mode 100644 index 0000000000..5a82677957 --- /dev/null +++ b/testdata/baselines/reference/fourslash/autoImport/NodeModulesImportCompletions1Baseline.baseline.md @@ -0,0 +1,72 @@ +// === Auto Imports === +```ts +// @FileName: /main.mts +import {} from "./src//*1*/"; //note, this test should not work until packagejsonautoimportprovider is implemented +import mod = require("./src/"); +const m = import("./src/"); +```no autoimport completions found + +// === Auto Imports === +```ts +// @FileName: /main.mts +import {} from "./src/"; //note, this test should not work until packagejsonautoimportprovider is implemented +import mod = require("./src/"); +const m = import("./src//*3*/"); +```no autoimport completions found + +// === Auto Imports === +```ts +// @FileName: /main.cts +import {} from "./src/"; +import mod = require("./src/"); +const m = import("./src//*6*/"); +```no autoimport completions found + +// === Auto Imports === +```ts +// @FileName: /main.ts +import {} from "./src/"; +import mod = require("./src/"); +const m = import("./src//*9*/"); +```no autoimport completions found + +// === Auto Imports === +```ts +// @FileName: /main.mts +import {} from "./src/"; //note, this test should not work until packagejsonautoimportprovider is implemented +import mod = require("./src//*2*/"); +const m = import("./src/"); +```no autoimport completions found + +// === Auto Imports === +```ts +// @FileName: /main.cts +import {} from "./src//*4*/"; +import mod = require("./src/"); +const m = import("./src/"); +```no autoimport completions found + +// === Auto Imports === +```ts +// @FileName: /main.cts +import {} from "./src/"; +import mod = require("./src//*5*/"); +const m = import("./src/"); +```no autoimport completions found + +// === Auto Imports === +```ts +// @FileName: /main.ts +import {} from "./src//*7*/"; +import mod = require("./src/"); +const m = import("./src/"); +```no autoimport completions found + +// === Auto Imports === +```ts +// @FileName: /main.ts +import {} from "./src/"; +import mod = require("./src//*8*/"); +const m = import("./src/"); +```no autoimport completions found +