diff --git a/internal/ast/symbol.go b/internal/ast/symbol.go index 56cfb2ba1b..7d0875adc0 100644 --- a/internal/ast/symbol.go +++ b/internal/ast/symbol.go @@ -23,6 +23,18 @@ type Symbol struct { GlobalExports SymbolTable // Conditional global UMD exports } +func (s *Symbol) IsExternalModule() bool { + return s.Flags&SymbolFlagsModule != 0 && len(s.Name) > 0 && s.Name[0] == '"' +} + +func (s *Symbol) IsStatic() bool { + if s.ValueDeclaration == nil { + return false + } + modifierFlags := s.ValueDeclaration.ModifierFlags() + return modifierFlags&ModifierFlagsStatic != 0 +} + // SymbolTable type SymbolTable map[string]*Symbol diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 7c9471b73a..c254799c92 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -3526,6 +3526,10 @@ func IsTypeDeclarationName(name *Node) bool { GetNameOfDeclaration(name.Parent) == name } +func IsRightSideOfPropertyAccess(node *Node) bool { + return node.Parent.Kind == KindPropertyAccessExpression && node.Parent.Name() == node +} + func IsRightSideOfQualifiedNameOrPropertyAccess(node *Node) bool { parent := node.Parent switch parent.Kind { diff --git a/internal/core/core.go b/internal/core/core.go index 7b7efc6a89..e1a997199f 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -687,3 +687,13 @@ func DeduplicateSorted[T any](slice []T, isEqual func(a, b T) bool) []T { return deduplicated } + +// CompareBooleans treats true as greater than false. +func CompareBooleans(a, b bool) int { + if a && !b { + return -1 + } else if !a && b { + return 1 + } + return 0 +} diff --git a/internal/format/rulecontext.go b/internal/format/rulecontext.go index 7b91c7bfff..a71ae27cef 100644 --- a/internal/format/rulecontext.go +++ b/internal/format/rulecontext.go @@ -6,7 +6,7 @@ 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/lsutil" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/scanner" ) diff --git a/internal/fourslash/_scripts/convertFourslash.mts b/internal/fourslash/_scripts/convertFourslash.mts index 5d47f7a836..a825d461f4 100644 --- a/internal/fourslash/_scripts/convertFourslash.mts +++ b/internal/fourslash/_scripts/convertFourslash.mts @@ -1205,7 +1205,7 @@ function parseUserPreferences(arg: ts.ObjectLiteralExpression): string | undefin preferences.push(`UseAliasesForRename: ${stringToTristate(prop.initializer.getText())}`); break; case "quotePreference": - preferences.push(`QuotePreference: ls.QuotePreference(${prop.initializer.getText()})`); + preferences.push(`QuotePreference: lsutil.QuotePreference(${prop.initializer.getText()})`); break; } } @@ -1216,7 +1216,7 @@ function parseUserPreferences(arg: ts.ObjectLiteralExpression): string | undefin if (preferences.length === 0) { return "nil /*preferences*/"; } - return `&ls.UserPreferences{${preferences.join(",")}}`; + return `&lsutil.UserPreferences{${preferences.join(",")}}`; } function parseBaselineMarkerOrRangeArg(arg: ts.Expression): string | undefined { @@ -1813,6 +1813,9 @@ function generateGoTest(failingTests: Set, test: GoTest): string { if (commands.includes("ls.")) { imports.push(`"github.com/microsoft/typescript-go/internal/ls"`); } + if (commands.includes("lsutil.")) { + imports.push(`"github.com/microsoft/typescript-go/internal/ls/lsutil"`); + } if (commands.includes("lsproto.")) { imports.push(`"github.com/microsoft/typescript-go/internal/lsp/lsproto"`); } diff --git a/internal/fourslash/baselineutil.go b/internal/fourslash/baselineutil.go index 1a659331d9..6512ec5321 100644 --- a/internal/fourslash/baselineutil.go +++ b/internal/fourslash/baselineutil.go @@ -13,7 +13,7 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/debug" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/testutil/baseline" @@ -163,7 +163,7 @@ func (f *FourslashTest) getBaselineForGroupedLocationsWithFileContents(groupedRa return nil } - fileName := ls.FileNameToDocumentURI(path) + fileName := lsconv.FileNameToDocumentURI(path) ranges := groupedRanges.Get(fileName) if len(ranges) == 0 { return nil @@ -219,7 +219,7 @@ func (f *FourslashTest) getBaselineContentForFile( detailPrefixes := map[*baselineDetail]string{} detailSuffixes := map[*baselineDetail]string{} canDetermineContextIdInline := true - uri := ls.FileNameToDocumentURI(fileName) + uri := lsconv.FileNameToDocumentURI(fileName) if options.marker != nil && options.marker.FileName() == fileName { details = append(details, &baselineDetail{pos: options.marker.LSPos(), positionMarker: options.markerName}) @@ -258,7 +258,7 @@ func (f *FourslashTest) getBaselineContentForFile( } slices.SortStableFunc(details, func(d1, d2 *baselineDetail) int { - return ls.ComparePositions(d1.pos, d2.pos) + return lsproto.ComparePositions(d1.pos, d2.pos) }) // !!! if canDetermineContextIdInline @@ -362,20 +362,20 @@ type textWithContext struct { isLibFile bool fileName string content string // content of the original file - lineStarts *ls.LSPLineMap - converters *ls.Converters + lineStarts *lsconv.LSPLineMap + converters *lsconv.Converters // posLineInfo posInfo *lsproto.Position lineInfo int } -// implements ls.Script +// implements lsconv.Script func (t *textWithContext) FileName() string { return t.fileName } -// implements ls.Script +// implements lsconv.Script func (t *textWithContext) Text() string { return t.content } @@ -391,10 +391,10 @@ func newTextWithContext(fileName string, content string) *textWithContext { pos: lsproto.Position{Line: 0, Character: 0}, fileName: fileName, content: content, - lineStarts: ls.ComputeLSPLineStarts(content), + lineStarts: lsconv.ComputeLSPLineStarts(content), } - t.converters = ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *ls.LSPLineMap { + t.converters = lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *lsconv.LSPLineMap { return t.lineStarts }) t.readableContents.WriteString("// === " + fileName + " ===") diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 93f61817ab..7dcb546e2f 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -16,6 +16,8 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsconv" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project" @@ -41,9 +43,9 @@ type FourslashTest struct { rangesByText *collections.MultiMap[string, *RangeMarker] scriptInfos map[string]*scriptInfo - converters *ls.Converters + converters *lsconv.Converters - userPreferences *ls.UserPreferences + userPreferences *lsutil.UserPreferences currentCaretPosition lsproto.Position lastKnownMarkerName *string activeFilename string @@ -53,7 +55,7 @@ type FourslashTest struct { type scriptInfo struct { fileName string content string - lineMap *ls.LSPLineMap + lineMap *lsconv.LSPLineMap version int32 } @@ -61,14 +63,14 @@ func newScriptInfo(fileName string, content string) *scriptInfo { return &scriptInfo{ fileName: fileName, content: content, - lineMap: ls.ComputeLSPLineStarts(content), + lineMap: lsconv.ComputeLSPLineStarts(content), version: 1, } } func (s *scriptInfo) editContent(start int, end int, newText string) { s.content = s.content[:start] + newText + s.content[end:] - s.lineMap = ls.ComputeLSPLineStarts(s.content) + s.lineMap = lsconv.ComputeLSPLineStarts(s.content) s.version++ } @@ -172,7 +174,7 @@ func NewFourslash(t *testing.T, capabilities *lsproto.ClientCapabilities, conten } }() - converters := ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(fileName string) *ls.LSPLineMap { + converters := lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(fileName string) *lsconv.LSPLineMap { scriptInfo, ok := scriptInfos[fileName] if !ok { return nil @@ -185,7 +187,7 @@ func NewFourslash(t *testing.T, capabilities *lsproto.ClientCapabilities, conten in: inputWriter, out: outputReader, testData: &testData, - userPreferences: ls.NewDefaultUserPreferences(), // !!! parse default preferences for fourslash case? + userPreferences: lsutil.NewDefaultUserPreferences(), // !!! parse default preferences for fourslash case? vfs: fs, scriptInfos: scriptInfos, converters: converters, @@ -332,14 +334,14 @@ func (f *FourslashTest) readMsg(t *testing.T) *lsproto.Message { return msg } -func (f *FourslashTest) Configure(t *testing.T, config *ls.UserPreferences) { +func (f *FourslashTest) Configure(t *testing.T, config *lsutil.UserPreferences) { f.userPreferences = config sendNotification(t, f, lsproto.WorkspaceDidChangeConfigurationInfo, &lsproto.DidChangeConfigurationParams{ Settings: config, }) } -func (f *FourslashTest) ConfigureWithReset(t *testing.T, config *ls.UserPreferences) (reset func()) { +func (f *FourslashTest) ConfigureWithReset(t *testing.T, config *lsutil.UserPreferences) (reset func()) { originalConfig := f.userPreferences.Copy() f.Configure(t, config) return func() { @@ -486,7 +488,7 @@ func (f *FourslashTest) openFile(t *testing.T, filename string) { f.activeFilename = filename sendNotification(t, f, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{ TextDocument: &lsproto.TextDocumentItem{ - Uri: ls.FileNameToDocumentURI(filename), + Uri: lsconv.FileNameToDocumentURI(filename), LanguageId: getLanguageKind(filename), Text: script.content, }, @@ -521,7 +523,7 @@ type CompletionsExpectedList struct { IsIncomplete bool ItemDefaults *CompletionsExpectedItemDefaults Items *CompletionsExpectedItems - UserPreferences *ls.UserPreferences // !!! allow user preferences in fourslash + UserPreferences *lsutil.UserPreferences // !!! allow user preferences in fourslash } type Ignored = struct{} @@ -614,7 +616,7 @@ func (f *FourslashTest) VerifyCompletions(t *testing.T, markerInput MarkerInput, func (f *FourslashTest) verifyCompletionsWorker(t *testing.T, expected *CompletionsExpectedList) *lsproto.CompletionList { prefix := f.getCurrentPositionPrefix() - var userPreferences *ls.UserPreferences + var userPreferences *lsutil.UserPreferences if expected != nil { userPreferences = expected.UserPreferences } @@ -623,11 +625,11 @@ func (f *FourslashTest) verifyCompletionsWorker(t *testing.T, expected *Completi return list } -func (f *FourslashTest) getCompletions(t *testing.T, userPreferences *ls.UserPreferences) *lsproto.CompletionList { +func (f *FourslashTest) getCompletions(t *testing.T, userPreferences *lsutil.UserPreferences) *lsproto.CompletionList { prefix := f.getCurrentPositionPrefix() params := &lsproto.CompletionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Context: &lsproto.CompletionContext{}, @@ -921,17 +923,17 @@ type ApplyCodeActionFromCompletionOptions struct { Description string NewFileContent *string NewRangeContent *string - UserPreferences *ls.UserPreferences + UserPreferences *lsutil.UserPreferences } func (f *FourslashTest) VerifyApplyCodeActionFromCompletion(t *testing.T, markerName *string, options *ApplyCodeActionFromCompletionOptions) { f.GoToMarker(t, *markerName) - var userPreferences *ls.UserPreferences + var userPreferences *lsutil.UserPreferences if options != nil && options.UserPreferences != nil { userPreferences = options.UserPreferences } else { // Default preferences: enables auto-imports - userPreferences = ls.NewDefaultUserPreferences() + userPreferences = lsutil.NewDefaultUserPreferences() } reset := f.ConfigureWithReset(t, userPreferences) @@ -988,7 +990,7 @@ func (f *FourslashTest) VerifyBaselineFindAllReferences( params := &lsproto.ReferenceParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Context: &lsproto.ReferenceContext{}, @@ -1029,7 +1031,7 @@ func (f *FourslashTest) VerifyBaselineGoToDefinition( params := &lsproto.DefinitionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -1078,7 +1080,7 @@ func (f *FourslashTest) VerifyBaselineGoToTypeDefinition( params := &lsproto.TypeDefinitionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -1123,7 +1125,7 @@ func (f *FourslashTest) VerifyBaselineHover(t *testing.T) { params := &lsproto.HoverParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: marker.LSPosition, } @@ -1194,7 +1196,7 @@ func (f *FourslashTest) VerifyBaselineSignatureHelp(t *testing.T) { params := &lsproto.SignatureHelpParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: marker.LSPosition, } @@ -1315,7 +1317,7 @@ func (f *FourslashTest) VerifyBaselineSelectionRanges(t *testing.T) { // Get selection ranges at this marker params := &lsproto.SelectionRangeParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(marker.FileName()), + Uri: lsconv.FileNameToDocumentURI(marker.FileName()), }, Positions: []lsproto.Position{marker.LSPosition}, } @@ -1439,7 +1441,7 @@ func (f *FourslashTest) VerifyBaselineSelectionRanges(t *testing.T) { func (f *FourslashTest) VerifyBaselineDocumentHighlights( t *testing.T, - preferences *ls.UserPreferences, + preferences *lsutil.UserPreferences, markerOrRangeOrNames ...MarkerOrRangeOrName, ) { var markerOrRanges []MarkerOrRange @@ -1465,7 +1467,7 @@ func (f *FourslashTest) VerifyBaselineDocumentHighlights( func (f *FourslashTest) verifyBaselineDocumentHighlights( t *testing.T, - preferences *ls.UserPreferences, + preferences *lsutil.UserPreferences, markerOrRanges []MarkerOrRange, ) { for _, markerOrRange := range markerOrRanges { @@ -1473,7 +1475,7 @@ func (f *FourslashTest) verifyBaselineDocumentHighlights( params := &lsproto.DocumentHighlightParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -1501,7 +1503,7 @@ func (f *FourslashTest) verifyBaselineDocumentHighlights( var spans []lsproto.Location for _, h := range *highlights { spans = append(spans, lsproto.Location{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), Range: h.Range, }) } @@ -1700,7 +1702,7 @@ func (f *FourslashTest) editScript(t *testing.T, fileName string, start int, end } sendNotification(t, f, lsproto.TextDocumentDidChangeInfo, &lsproto.DidChangeTextDocumentParams{ TextDocument: lsproto.VersionedTextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(fileName), + Uri: lsconv.FileNameToDocumentURI(fileName), Version: script.version, }, ContentChanges: []lsproto.TextDocumentContentChangePartialOrWholeDocument{ @@ -1729,7 +1731,7 @@ func (f *FourslashTest) VerifyQuickInfoAt(t *testing.T, marker string, expectedT func (f *FourslashTest) getQuickInfoAtCurrentPosition(t *testing.T) *lsproto.Hover { params := &lsproto.HoverParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, } @@ -1839,7 +1841,7 @@ func (f *FourslashTest) verifySignatureHelp( prefix := f.getCurrentPositionPrefix() params := &lsproto.SignatureHelpParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Context: context, @@ -1871,7 +1873,7 @@ func (f *FourslashTest) getCurrentPositionPrefix() string { } func (f *FourslashTest) BaselineAutoImportsCompletions(t *testing.T, markerNames []string) { - reset := f.ConfigureWithReset(t, &ls.UserPreferences{ + reset := f.ConfigureWithReset(t, &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, }) @@ -1881,7 +1883,7 @@ func (f *FourslashTest) BaselineAutoImportsCompletions(t *testing.T, markerNames f.GoToMarker(t, markerName) params := &lsproto.CompletionParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, Context: &lsproto.CompletionContext{}, @@ -1912,7 +1914,7 @@ func (f *FourslashTest) BaselineAutoImportsCompletions(t *testing.T, markerNames ))) currentFile := newScriptInfo(f.activeFilename, fileContent) - converters := ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *ls.LSPLineMap { + converters := lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *lsconv.LSPLineMap { return currentFile.lineMap }) var list []*lsproto.CompletionItem @@ -1959,7 +1961,7 @@ func (f *FourslashTest) BaselineAutoImportsCompletions(t *testing.T, markerNames // } // 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) }) + slices.SortFunc(allChanges, func(a, b *lsproto.TextEdit) int { return lsproto.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):] @@ -1974,7 +1976,7 @@ type MarkerOrRangeOrName = any func (f *FourslashTest) VerifyBaselineRename( t *testing.T, - preferences *ls.UserPreferences, + preferences *lsutil.UserPreferences, markerOrNameOrRanges ...MarkerOrRangeOrName, ) { var markerOrRanges []MarkerOrRange @@ -2000,7 +2002,7 @@ func (f *FourslashTest) VerifyBaselineRename( func (f *FourslashTest) verifyBaselineRename( t *testing.T, - preferences *ls.UserPreferences, + preferences *lsutil.UserPreferences, markerOrRanges []MarkerOrRange, ) { for _, markerOrRange := range markerOrRanges { @@ -2009,7 +2011,7 @@ func (f *FourslashTest) verifyBaselineRename( // !!! set preferences params := &lsproto.RenameParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, NewName: "?", @@ -2042,7 +2044,7 @@ func (f *FourslashTest) verifyBaselineRename( if preferences.UseAliasesForRename != core.TSUnknown { fmt.Fprintf(&renameOptions, "// @useAliasesForRename: %v\n", preferences.UseAliasesForRename.IsTrue()) } - if preferences.QuotePreference != ls.QuotePreferenceUnknown { + if preferences.QuotePreference != lsutil.QuotePreferenceUnknown { fmt.Fprintf(&renameOptions, "// @quotePreference: %v\n", preferences.QuotePreference) } } @@ -2086,11 +2088,11 @@ func (f *FourslashTest) verifyBaselineRename( } } -func (f *FourslashTest) VerifyRenameSucceeded(t *testing.T, preferences *ls.UserPreferences) { +func (f *FourslashTest) VerifyRenameSucceeded(t *testing.T, preferences *lsutil.UserPreferences) { // !!! set preferences params := &lsproto.RenameParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, NewName: "?", @@ -2110,11 +2112,11 @@ func (f *FourslashTest) VerifyRenameSucceeded(t *testing.T, preferences *ls.User } } -func (f *FourslashTest) VerifyRenameFailed(t *testing.T, preferences *ls.UserPreferences) { +func (f *FourslashTest) VerifyRenameFailed(t *testing.T, preferences *lsutil.UserPreferences) { // !!! set preferences params := &lsproto.RenameParams{ TextDocument: lsproto.TextDocumentIdentifier{ - Uri: ls.FileNameToDocumentURI(f.activeFilename), + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), }, Position: f.currentCaretPosition, NewName: "?", @@ -2136,7 +2138,7 @@ func (f *FourslashTest) VerifyRenameFailed(t *testing.T, preferences *ls.UserPre func (f *FourslashTest) VerifyBaselineRenameAtRangesWithText( t *testing.T, - preferences *ls.UserPreferences, + preferences *lsutil.UserPreferences, texts ...string, ) { var markerOrRanges []MarkerOrRange diff --git a/internal/fourslash/test_parser.go b/internal/fourslash/test_parser.go index da8de6fc79..fb9e2c924a 100644 --- a/internal/fourslash/test_parser.go +++ b/internal/fourslash/test_parser.go @@ -9,7 +9,7 @@ import ( "github.com/go-json-experiment/json" "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/testrunner" @@ -163,17 +163,17 @@ type TestFileInfo struct { emit bool } -// FileName implements ls.Script. +// FileName implements lsconv.Script. func (t *TestFileInfo) FileName() string { return t.fileName } -// Text implements ls.Script. +// Text implements lsconv.Script. func (t *TestFileInfo) Text() string { return t.Content } -var _ ls.Script = (*TestFileInfo)(nil) +var _ lsconv.Script = (*TestFileInfo)(nil) const emitThisFileOption = "emitthisfile" @@ -368,8 +368,8 @@ func parseFileContent(fileName string, content string, fileOptions map[string]st outputString := output.String() // Set LS positions for markers - lineMap := ls.ComputeLSPLineStarts(outputString) - converters := ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *ls.LSPLineMap { + lineMap := lsconv.ComputeLSPLineStarts(outputString) + converters := lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *lsconv.LSPLineMap { return lineMap }) diff --git a/internal/fourslash/tests/autoImportCompletion_test.go b/internal/fourslash/tests/autoImportCompletion_test.go index 078eeac2c8..fb107e2d6a 100644 --- a/internal/fourslash/tests/autoImportCompletion_test.go +++ b/internal/fourslash/tests/autoImportCompletion_test.go @@ -6,7 +6,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "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/ls/lsutil" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -26,7 +26,7 @@ a/**/ ` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ - UserPreferences: &ls.UserPreferences{ + UserPreferences: &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, }, @@ -41,7 +41,7 @@ a/**/ }) f.BaselineAutoImportsCompletions(t, []string{""}) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ - UserPreferences: &ls.UserPreferences{ + UserPreferences: &lsutil.UserPreferences{ // completion autoimport preferences off; this tests if fourslash server communication correctly registers changes in user preferences IncludeCompletionsForModuleExports: core.TSUnknown, IncludeCompletionsForImportStatements: core.TSUnknown, @@ -71,7 +71,7 @@ a/**/ ` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ - UserPreferences: &ls.UserPreferences{ + UserPreferences: &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, }, @@ -102,7 +102,7 @@ b/**/ ` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ - UserPreferences: &ls.UserPreferences{ + UserPreferences: &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, }, diff --git a/internal/fourslash/tests/gen/renameExportSpecifier2_test.go b/internal/fourslash/tests/gen/renameExportSpecifier2_test.go index 404830405a..2568eab221 100644 --- a/internal/fourslash/tests/gen/renameExportSpecifier2_test.go +++ b/internal/fourslash/tests/gen/renameExportSpecifier2_test.go @@ -5,7 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -20,5 +20,5 @@ export { name/**/ }; import { name } from './a'; const x = name.toString();` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) - f.VerifyBaselineRename(t, &ls.UserPreferences{UseAliasesForRename: core.TSFalse}, "") + f.VerifyBaselineRename(t, &lsutil.UserPreferences{UseAliasesForRename: core.TSFalse}, "") } diff --git a/internal/fourslash/tests/gen/renameExportSpecifier_test.go b/internal/fourslash/tests/gen/renameExportSpecifier_test.go index 0b4207e579..7f8c4191c4 100644 --- a/internal/fourslash/tests/gen/renameExportSpecifier_test.go +++ b/internal/fourslash/tests/gen/renameExportSpecifier_test.go @@ -5,7 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -20,5 +20,5 @@ export { name as name/**/ }; import { name } from './a'; const x = name.toString();` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) - f.VerifyBaselineRename(t, &ls.UserPreferences{UseAliasesForRename: core.TSFalse}, "") + f.VerifyBaselineRename(t, &lsutil.UserPreferences{UseAliasesForRename: core.TSFalse}, "") } diff --git a/internal/fourslash/tests/gen/renameModuleExportsProperties1_test.go b/internal/fourslash/tests/gen/renameModuleExportsProperties1_test.go index 012d2fc57c..2bfa3a1cfe 100644 --- a/internal/fourslash/tests/gen/renameModuleExportsProperties1_test.go +++ b/internal/fourslash/tests/gen/renameModuleExportsProperties1_test.go @@ -5,7 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -16,5 +16,5 @@ func TestRenameModuleExportsProperties1(t *testing.T) { const content = `[|class [|{| "contextRangeIndex": 0 |}A|] {}|] module.exports = { [|A|] }` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) - f.VerifyBaselineRename(t, &ls.UserPreferences{UseAliasesForRename: core.TSTrue}, f.Ranges()[1], f.Ranges()[2]) + f.VerifyBaselineRename(t, &lsutil.UserPreferences{UseAliasesForRename: core.TSTrue}, f.Ranges()[1], f.Ranges()[2]) } diff --git a/internal/fourslash/tests/gen/renameModuleExportsProperties3_test.go b/internal/fourslash/tests/gen/renameModuleExportsProperties3_test.go index 35eca115e1..6219a95c04 100644 --- a/internal/fourslash/tests/gen/renameModuleExportsProperties3_test.go +++ b/internal/fourslash/tests/gen/renameModuleExportsProperties3_test.go @@ -5,7 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -18,5 +18,5 @@ func TestRenameModuleExportsProperties3(t *testing.T) { [|class [|{| "contextRangeIndex": 0 |}A|] {}|] module.exports = { [|A|] }` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) - f.VerifyBaselineRename(t, &ls.UserPreferences{UseAliasesForRename: core.TSTrue}, f.Ranges()[1], f.Ranges()[2]) + f.VerifyBaselineRename(t, &lsutil.UserPreferences{UseAliasesForRename: core.TSTrue}, f.Ranges()[1], f.Ranges()[2]) } diff --git a/internal/fourslash/tests/gen/renameNamedImport_test.go b/internal/fourslash/tests/gen/renameNamedImport_test.go index 7a9df3f016..174d6987df 100644 --- a/internal/fourslash/tests/gen/renameNamedImport_test.go +++ b/internal/fourslash/tests/gen/renameNamedImport_test.go @@ -5,7 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -28,5 +28,5 @@ someExportedVariable; f := fourslash.NewFourslash(t, nil /*capabilities*/, content) f.GoToFile(t, "/home/src/workspaces/project/lib/index.ts") f.GoToFile(t, "/home/src/workspaces/project/src/index.ts") - f.VerifyBaselineRename(t, &ls.UserPreferences{UseAliasesForRename: core.TSTrue}, "i") + f.VerifyBaselineRename(t, &lsutil.UserPreferences{UseAliasesForRename: core.TSTrue}, "i") } diff --git a/internal/fourslash/tests/gen/renameNumericalIndexSingleQuoted_test.go b/internal/fourslash/tests/gen/renameNumericalIndexSingleQuoted_test.go index f02f765d71..fba05fbfba 100644 --- a/internal/fourslash/tests/gen/renameNumericalIndexSingleQuoted_test.go +++ b/internal/fourslash/tests/gen/renameNumericalIndexSingleQuoted_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/microsoft/typescript-go/internal/fourslash" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -15,5 +15,5 @@ func TestRenameNumericalIndexSingleQuoted(t *testing.T) { const content = `const foo = { [|0|]: true }; foo[[|0|]];` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) - f.VerifyBaselineRenameAtRangesWithText(t, &ls.UserPreferences{QuotePreference: ls.QuotePreference("single")}, "0") + f.VerifyBaselineRenameAtRangesWithText(t, &lsutil.UserPreferences{QuotePreference: lsutil.QuotePreference("single")}, "0") } diff --git a/internal/fourslash/tests/gen/renameRestBindingElement_test.go b/internal/fourslash/tests/gen/renameRestBindingElement_test.go index c4c4200e56..2b3067e157 100644 --- a/internal/fourslash/tests/gen/renameRestBindingElement_test.go +++ b/internal/fourslash/tests/gen/renameRestBindingElement_test.go @@ -5,7 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -22,5 +22,5 @@ function foo([|{ a, ...[|{| "contextRangeIndex": 0 |}rest|] }: I|]) { [|rest|]; }` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) - f.VerifyBaselineRename(t, &ls.UserPreferences{UseAliasesForRename: core.TSTrue}, f.Ranges()[1]) + f.VerifyBaselineRename(t, &lsutil.UserPreferences{UseAliasesForRename: core.TSTrue}, f.Ranges()[1]) } diff --git a/internal/ls/autoimportfixes.go b/internal/ls/autoimportfixes.go index 8e554880b4..82a7ddaced 100644 --- a/internal/ls/autoimportfixes.go +++ b/internal/ls/autoimportfixes.go @@ -7,6 +7,9 @@ import ( "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/debug" + "github.com/microsoft/typescript-go/internal/ls/change" + "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/ls/organizeimports" "github.com/microsoft/typescript-go/internal/stringutil" ) @@ -17,11 +20,12 @@ type Import struct { 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 addNamespaceQualifier(ct *change.Tracker, sourceFile *ast.SourceFile, qualification *Qualification) { + ct.InsertText(sourceFile, qualification.usagePosition, qualification.namespacePrefix+".") } -func (ct *changeTracker) doAddExistingFix( +func (ls *LanguageService) doAddExistingFix( + ct *change.Tracker, sourceFile *ast.SourceFile, clause *ast.Node, // ImportClause | ObjectBindingPattern, defaultImport *Import, @@ -53,10 +57,10 @@ func (ct *changeTracker) doAddExistingFix( // return // } if defaultImport != nil { - ct.addElementToBindingPattern(sourceFile, clause, defaultImport.name, ptrTo("default")) + addElementToBindingPattern(ct, sourceFile, clause, defaultImport.name, ptrTo("default")) } for _, specifier := range namedImports { - ct.addElementToBindingPattern(sourceFile, clause, specifier.name, &specifier.propertyName) + addElementToBindingPattern(ct, sourceFile, clause, specifier.name, &specifier.propertyName) } return } @@ -79,18 +83,18 @@ func (ct *changeTracker) doAddExistingFix( 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: ", "}) + ct.InsertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(clause, sourceFile, false)), ct.NodeFactory.NewIdentifier(defaultImport.name), change.NodeOptions{Suffix: ", "}) } if len(namedImports) > 0 { - specifierComparer, isSorted := ct.ls.getNamedImportSpecifierComparerWithDetection(importClause.Parent, sourceFile) + specifierComparer, isSorted := organizeimports.GetNamedImportSpecifierComparerWithDetection(importClause.Parent, sourceFile, ls.UserPreferences()) 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, ct.ls.UserPreferences()), + (!importClause.IsTypeOnly() || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, ls.UserPreferences()), identifier, ct.NodeFactory.NewIdentifier(namedImport.name), ) @@ -128,36 +132,36 @@ func (ct *changeTracker) doAddExistingFix( // ).elements // } for _, spec := range newSpecifiers { - insertionIndex := getImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer) - ct.insertImportSpecifierAtIndex(sourceFile, spec, importClause.NamedBindings, insertionIndex) + insertionIndex := organizeimports.GetImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer) + ct.InsertImportSpecifierAtIndex(sourceFile, spec, importClause.NamedBindings, insertionIndex) } } else if len(existingSpecifiers) > 0 && isSorted.IsTrue() { // Existing specifiers are sorted, so insert each new specifier at the correct position for _, spec := range newSpecifiers { - insertionIndex := getImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer) + insertionIndex := organizeimports.GetImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer) if insertionIndex >= len(existingSpecifiers) { // Insert at the end - ct.insertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers) + ct.InsertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers) } else { // Insert before the element at insertionIndex - ct.insertNodeInListAfter(sourceFile, existingSpecifiers[insertionIndex], spec.AsNode(), existingSpecifiers) + ct.InsertNodeInListAfter(sourceFile, existingSpecifiers[insertionIndex], spec.AsNode(), existingSpecifiers) } } } else if len(existingSpecifiers) > 0 { // Existing specifiers may not be sorted, append to the end for _, spec := range newSpecifiers { - ct.insertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers) + 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) + 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) + ct.InsertNodeAfter(sourceFile, clause.Name(), namedImports) } } } @@ -182,19 +186,19 @@ func (ct *changeTracker) doAddExistingFix( } } -func (ct *changeTracker) addElementToBindingPattern(sourceFile *ast.SourceFile, bindingPattern *ast.Node, name string, propertyName *string) { - element := ct.newBindingElementFromNameAndPropertyName(name, propertyName) +func addElementToBindingPattern(ct *change.Tracker, sourceFile *ast.SourceFile, bindingPattern *ast.Node, name string, propertyName *string) { + element := newBindingElementFromNameAndPropertyName(ct, name, propertyName) if len(bindingPattern.Elements()) > 0 { - ct.insertNodeInListAfter(sourceFile, bindingPattern.Elements()[len(bindingPattern.Elements())-1], element, nil) + ct.InsertNodeInListAfter(sourceFile, bindingPattern.Elements()[len(bindingPattern.Elements())-1], element, nil) } else { - ct.replaceNode(sourceFile, bindingPattern, ct.NodeFactory.NewBindingPattern( + 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 { +func newBindingElementFromNameAndPropertyName(ct *change.Tracker, name string, propertyName *string) *ast.Node { var newPropertyNameIdentifier *ast.Node if propertyName != nil { newPropertyNameIdentifier = ct.NodeFactory.NewIdentifier(*propertyName) @@ -207,7 +211,7 @@ func (ct *changeTracker) newBindingElementFromNameAndPropertyName(name string, p ) } -func (ct *changeTracker) insertImports(sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool) { +func (ls *LanguageService) insertImports(ct *change.Tracker, sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool) { var existingImportStatements []*ast.Statement if imports[0].Kind == ast.KindVariableStatement { @@ -215,10 +219,10 @@ func (ct *changeTracker) insertImports(sourceFile *ast.SourceFile, imports []*as } else { existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsAnyImportSyntax) } - comparer, isSorted := ct.ls.getOrganizeImportsStringComparerWithDetection(existingImportStatements) + comparer, isSorted := organizeimports.GetOrganizeImportsStringComparerWithDetection(existingImportStatements, ls.UserPreferences()) sortedNewImports := slices.Clone(imports) slices.SortFunc(sortedNewImports, func(a, b *ast.Statement) int { - return compareImportsOrRequireStatements(a, b, comparer) + return organizeimports.CompareImportsOrRequireStatements(a, b, comparer) }) // !!! FutureSourceFile // if !isFullSourceFile(sourceFile) { @@ -233,25 +237,25 @@ func (ct *changeTracker) insertImports(sourceFile *ast.SourceFile, imports []*as if len(existingImportStatements) > 0 && isSorted { // Existing imports are sorted, insert each new import at the correct position for _, newImport := range sortedNewImports { - insertionIndex := getImportDeclarationInsertIndex(existingImportStatements, newImport, func(a, b *ast.Statement) stringutil.Comparison { - return compareImportsOrRequireStatements(a, b, comparer) + insertionIndex := organizeimports.GetImportDeclarationInsertIndex(existingImportStatements, newImport, func(a, b *ast.Statement) stringutil.Comparison { + return organizeimports.CompareImportsOrRequireStatements(a, b, comparer) }) if insertionIndex == 0 { // If the first import is top-of-file, insert after the leading comment which is likely the header - ct.insertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(existingImportStatements[0], sourceFile, false)), newImport.AsNode(), changeNodeOptions{}) + ct.InsertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(existingImportStatements[0], sourceFile, false)), newImport.AsNode(), change.NodeOptions{}) } else { prevImport := existingImportStatements[insertionIndex-1] - ct.insertNodeAfter(sourceFile, prevImport.AsNode(), newImport.AsNode()) + ct.InsertNodeAfter(sourceFile, prevImport.AsNode(), newImport.AsNode()) } } } else if len(existingImportStatements) > 0 { - ct.insertNodesAfter(sourceFile, existingImportStatements[len(existingImportStatements)-1], sortedNewImports) + ct.InsertNodesAfter(sourceFile, existingImportStatements[len(existingImportStatements)-1], sortedNewImports) } else { - ct.insertAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween) + ct.InsertAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween) } } -func (ct *changeTracker) makeImport(defaultImport *ast.IdentifierNode, namedImports []*ast.Node, moduleSpecifier *ast.Expression, isTypeOnly bool) *ast.Statement { +func makeImport(ct *change.Tracker, 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)) @@ -263,7 +267,8 @@ func (ct *changeTracker) makeImport(defaultImport *ast.IdentifierNode, namedImpo return ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, importClause, moduleSpecifier, nil /*attributes*/) } -func (ct *changeTracker) getNewImports( +func (ls *LanguageService) getNewImports( + ct *change.Tracker, moduleSpecifier string, quotePreference quotePreference, defaultImport *Import, @@ -281,7 +286,7 @@ func (ct *changeTracker) getNewImports( // 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() || ct.ls.UserPreferences().PreferTypeOnlyAutoImports) && + (compilerOptions.VerbatimModuleSyntax.IsTrue() || ls.UserPreferences().PreferTypeOnlyAutoImports) && defaultImport != nil && defaultImport.addAsTypeOnly != AddAsTypeOnlyNotAllowed && !core.Some(namedImports, func(i *Import) bool { return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed }) var defaultImportNode *ast.Node @@ -289,13 +294,13 @@ func (ct *changeTracker) getNewImports( defaultImportNode = ct.NodeFactory.NewIdentifier(defaultImport.name) } - statements = append(statements, ct.makeImport(defaultImportNode, core.Map(namedImports, func(namedImport *Import) *ast.Node { + statements = append(statements, makeImport(ct, 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, ct.ls.UserPreferences()), + !topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, ls.UserPreferences()), namedImportPropertyName, ct.NodeFactory.NewIdentifier(namedImport.name), ) @@ -307,7 +312,7 @@ func (ct *changeTracker) getNewImports( if namespaceLikeImport.kind == ImportKindCommonJS { declaration = ct.NodeFactory.NewImportEqualsDeclaration( /*modifiers*/ nil, - shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, ct.ls.UserPreferences()), + shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, ls.UserPreferences()), ct.NodeFactory.NewIdentifier(namespaceLikeImport.name), ct.NodeFactory.NewExternalModuleReference(moduleSpecifierStringLiteral), ) @@ -315,7 +320,7 @@ func (ct *changeTracker) getNewImports( declaration = ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, ct.NodeFactory.NewImportClause( - /*phaseModifier*/ core.IfElse(shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, ct.ls.UserPreferences()), ast.KindTypeKeyword, ast.KindUnknown), + /*phaseModifier*/ core.IfElse(shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, ls.UserPreferences()), ast.KindTypeKeyword, ast.KindUnknown), /*name*/ nil, ct.NodeFactory.NewNamespaceImport(ct.NodeFactory.NewIdentifier(namespaceLikeImport.name)), ), @@ -335,6 +340,6 @@ func needsTypeOnly(addAsTypeOnly AddAsTypeOnly) bool { return addAsTypeOnly == AddAsTypeOnlyRequired } -func shouldUseTypeOnly(addAsTypeOnly AddAsTypeOnly, preferences *UserPreferences) bool { +func shouldUseTypeOnly(addAsTypeOnly AddAsTypeOnly, preferences *lsutil.UserPreferences) bool { return needsTypeOnly(addAsTypeOnly) || addAsTypeOnly != AddAsTypeOnlyNotAllowed && preferences.PreferTypeOnlyAutoImports } diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go index 1e586cc070..ec9ef60d6b 100644 --- a/internal/ls/autoimports.go +++ b/internal/ls/autoimports.go @@ -14,6 +14,8 @@ import ( "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/ls/change" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/modulespecifiers" @@ -398,7 +400,7 @@ func (l *LanguageService) isImportable( if toFile == nil { moduleName := stringutil.StripQuotes(toModule.Name) if _, ok := core.NodeCoreModules()[moduleName]; ok { - if useNodePrefix := shouldUseUriStyleNodeCoreModules(fromFile, l.GetProgram()); useNodePrefix { + if useNodePrefix := lsutil.ShouldUseUriStyleNodeCoreModules(fromFile, l.GetProgram()); useNodePrefix { return useNodePrefix == strings.HasPrefix(moduleName, "node:") } } @@ -499,7 +501,7 @@ func getDefaultLikeExportInfo(moduleSymbol *ast.Symbol, ch *checker.Checker) *Ex type importSpecifierResolverForCompletions struct { *ast.SourceFile // importingFile - *UserPreferences + *lsutil.UserPreferences l *LanguageService filter *packageJsonImportFilter } @@ -569,6 +571,86 @@ func (l *LanguageService) getBestFix(fixes []*ImportFix, sourceFile *ast.SourceF return best } +// 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 (l *LanguageService) compareModuleSpecifiers( + a *ImportFix, // !!! ImportFixWithModuleSpecifier + b *ImportFix, // !!! ImportFixWithModuleSpecifier + importingFile *ast.SourceFile, // | FutureSourceFile, + allowsImportingSpecifier func(specifier string) bool, + toPath func(fileName string) tspath.Path, +) int { + if a.kind == ImportFixKindUseNamespace || b.kind == ImportFixKindUseNamespace { + return 0 + } + if comparison := core.CompareBooleans( + b.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(b.moduleSpecifier), + a.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(a.moduleSpecifier), + ); comparison != 0 { + return comparison + } + if comparison := compareModuleSpecifierRelativity(a, b, l.UserPreferences()); comparison != 0 { + return comparison + } + if comparison := compareNodeCoreModuleSpecifiers(a.moduleSpecifier, b.moduleSpecifier, importingFile, l.GetProgram()); comparison != 0 { + return comparison + } + if comparison := core.CompareBooleans(isFixPossiblyReExportingImportingFile(a, importingFile.Path(), toPath), isFixPossiblyReExportingImportingFile(b, importingFile.Path(), toPath)); comparison != 0 { + return comparison + } + if comparison := tspath.CompareNumberOfDirectorySeparators(a.moduleSpecifier, b.moduleSpecifier); comparison != 0 { + return comparison + } + return 0 +} + +func compareNodeCoreModuleSpecifiers(a, b string, importingFile *ast.SourceFile, program *compiler.Program) int { + if strings.HasPrefix(a, "node:") && !strings.HasPrefix(b, "node:") { + if lsutil.ShouldUseUriStyleNodeCoreModules(importingFile, program) { + return -1 + } + return 1 + } + if strings.HasPrefix(b, "node:") && !strings.HasPrefix(a, "node:") { + if lsutil.ShouldUseUriStyleNodeCoreModules(importingFile, program) { + return 1 + } + return -1 + } + return 0 +} + +// 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 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" +} + +// returns `-1` if `a` is better than `b` +func compareModuleSpecifierRelativity(a *ImportFix, b *ImportFix, preferences *lsutil.UserPreferences) int { + switch preferences.ImportModuleSpecifierPreference { + case modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative: + return core.CompareBooleans(a.moduleSpecifierKind == modulespecifiers.ResultKindRelative, b.moduleSpecifierKind == modulespecifiers.ResultKindRelative) + } + return 0 +} + func (l *LanguageService) getImportFixes( ch *checker.Checker, exportInfos []*SymbolExportInfo, // | FutureSymbolExportInfo[], @@ -1362,14 +1444,14 @@ func (l *LanguageService) codeActionForFix( fix *ImportFix, includeSymbolNameInDescription bool, ) codeAction { - tracker := l.newChangeTracker(ctx) // !!! changetracker.with + tracker := change.NewTracker(ctx, l.GetProgram().Options(), l.FormatOptions(), l.converters) // !!! changetracker.with diag := l.codeActionForFixWorker(tracker, sourceFile, symbolName, fix, includeSymbolNameInDescription) - changes := tracker.getChanges()[sourceFile.FileName()] + changes := tracker.GetChanges()[sourceFile.FileName()] return codeAction{description: diag.Message(), changes: changes} } func (l *LanguageService) codeActionForFixWorker( - changeTracker *changeTracker, + changeTracker *change.Tracker, sourceFile *ast.SourceFile, symbolName string, fix *ImportFix, @@ -1377,14 +1459,15 @@ func (l *LanguageService) codeActionForFixWorker( ) *diagnostics.Message { switch fix.kind { case ImportFixKindUseNamespace: - changeTracker.addNamespaceQualifier(sourceFile, fix.qualification()) + addNamespaceQualifier(changeTracker, 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( + l.doAddExistingFix( + changeTracker, sourceFile, fix.importClauseOrBindingPattern, core.IfElse(fix.importKind == ImportKindDefault, &Import{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly}, nil), @@ -1410,18 +1493,19 @@ func (l *LanguageService) codeActionForFixWorker( } if fix.useRequire { - declarations = changeTracker.getNewRequires(fix.moduleSpecifier, defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options()) + declarations = getNewRequires(changeTracker, fix.moduleSpecifier, defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options()) } else { - declarations = changeTracker.getNewImports(fix.moduleSpecifier, getQuotePreference(sourceFile, l.UserPreferences()), defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options()) + declarations = l.getNewImports(changeTracker, fix.moduleSpecifier, getQuotePreference(sourceFile, l.UserPreferences()), defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options()) } - changeTracker.insertImports( + l.insertImports( + changeTracker, sourceFile, declarations, /*blankLineBetween*/ true, ) if qualification != nil { - changeTracker.addNamespaceQualifier(sourceFile, qualification) + addNamespaceQualifier(changeTracker, sourceFile, qualification) } if includeSymbolNameInDescription { return diagnostics.FormatMessage(diagnostics.Import_0_from_1, symbolName, fix.moduleSpecifier) @@ -1440,14 +1524,15 @@ func (l *LanguageService) codeActionForFixWorker( return nil } -func (c *changeTracker) getNewRequires( +func getNewRequires( + changeTracker *change.Tracker, moduleSpecifier string, defaultImport *Import, namedImports []*Import, namespaceLikeImport *Import, compilerOptions *core.CompilerOptions, ) []*ast.Statement { - quotedModuleSpecifier := c.NodeFactory.NewStringLiteral(moduleSpecifier) + quotedModuleSpecifier := changeTracker.NodeFactory.NewStringLiteral(moduleSpecifier) var statements []*ast.Statement // const { default: foo, bar, etc } = require('./mod'); @@ -1456,29 +1541,30 @@ func (c *changeTracker) getNewRequires( for _, namedImport := range namedImports { var propertyName *ast.Node if namedImport.propertyName != "" { - propertyName = c.NodeFactory.NewIdentifier(namedImport.propertyName) + propertyName = changeTracker.NodeFactory.NewIdentifier(namedImport.propertyName) } - bindingElements = append(bindingElements, c.NodeFactory.NewBindingElement( + bindingElements = append(bindingElements, changeTracker.NodeFactory.NewBindingElement( /*dotDotDotToken*/ nil, propertyName, - c.NodeFactory.NewIdentifier(namedImport.name), + changeTracker.NodeFactory.NewIdentifier(namedImport.name), /*initializer*/ nil, )) } if defaultImport != nil { bindingElements = append([]*ast.Node{ - c.NodeFactory.NewBindingElement( + changeTracker.NodeFactory.NewBindingElement( /*dotDotDotToken*/ nil, - c.NodeFactory.NewIdentifier("default"), - c.NodeFactory.NewIdentifier(defaultImport.name), + changeTracker.NodeFactory.NewIdentifier("default"), + changeTracker.NodeFactory.NewIdentifier(defaultImport.name), /*initializer*/ nil, ), }, bindingElements...) } - declaration := c.createConstEqualsRequireDeclaration( - c.NodeFactory.NewBindingPattern( + declaration := createConstEqualsRequireDeclaration( + changeTracker, + changeTracker.NodeFactory.NewBindingPattern( ast.KindObjectBindingPattern, - c.NodeFactory.NewNodeList(bindingElements), + changeTracker.NodeFactory.NewNodeList(bindingElements), ), quotedModuleSpecifier, ) @@ -1487,8 +1573,9 @@ func (c *changeTracker) getNewRequires( // const foo = require('./mod'); if namespaceLikeImport != nil { - declaration := c.createConstEqualsRequireDeclaration( - c.NodeFactory.NewIdentifier(namespaceLikeImport.name), + declaration := createConstEqualsRequireDeclaration( + changeTracker, + changeTracker.NodeFactory.NewIdentifier(namespaceLikeImport.name), quotedModuleSpecifier, ) statements = append(statements, declaration) @@ -1498,21 +1585,21 @@ func (c *changeTracker) getNewRequires( return statements } -func (c *changeTracker) createConstEqualsRequireDeclaration(name *ast.Node, quotedModuleSpecifier *ast.Node) *ast.Statement { - return c.NodeFactory.NewVariableStatement( +func createConstEqualsRequireDeclaration(changeTracker *change.Tracker, name *ast.Node, quotedModuleSpecifier *ast.Node) *ast.Statement { + return changeTracker.NodeFactory.NewVariableStatement( /*modifiers*/ nil, - c.NodeFactory.NewVariableDeclarationList( + changeTracker.NodeFactory.NewVariableDeclarationList( ast.NodeFlagsConst, - c.NodeFactory.NewNodeList([]*ast.Node{ - c.NodeFactory.NewVariableDeclaration( + changeTracker.NodeFactory.NewNodeList([]*ast.Node{ + changeTracker.NodeFactory.NewVariableDeclaration( name, /*exclamationToken*/ nil, /*type*/ nil, - c.NodeFactory.NewCallExpression( - c.NodeFactory.NewIdentifier("require"), + changeTracker.NodeFactory.NewCallExpression( + changeTracker.NodeFactory.NewIdentifier("require"), /*questionDotToken*/ nil, /*typeArguments*/ nil, - c.NodeFactory.NewNodeList([]*ast.Node{quotedModuleSpecifier}), + changeTracker.NodeFactory.NewNodeList([]*ast.Node{quotedModuleSpecifier}), ast.NodeFlagsNone, ), ), @@ -1535,3 +1622,7 @@ func getModuleSpecifierText(promotedDeclaration *ast.ImportDeclaration) string { } return promotedDeclaration.Parent.ModuleSpecifier().Text() } + +func ptrTo[T any](v T) *T { + return &v +} diff --git a/internal/ls/changetracker.go b/internal/ls/change/tracker.go similarity index 59% rename from internal/ls/changetracker.go rename to internal/ls/change/tracker.go index 971ab03c7a..5d88f40a7b 100644 --- a/internal/ls/changetracker.go +++ b/internal/ls/change/tracker.go @@ -1,4 +1,4 @@ -package ls +package change import ( "context" @@ -9,18 +9,19 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/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 { +type NodeOptions struct { // Text to be inserted before the new node - prefix string + Prefix string // Text to be inserted after the new node - suffix string + Suffix string // Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node indentation *int @@ -69,14 +70,14 @@ type trackerEdit struct { *ast.Node // single nodes []*ast.Node // multiple - options changeNodeOptions + options NodeOptions } -type changeTracker struct { +type Tracker struct { // initialized with formatSettings *format.FormatCodeSettings newLine string - ls *LanguageService + converters *lsconv.Converters ctx context.Context *printer.EmitContext @@ -88,99 +89,98 @@ type changeTracker struct { // printer } -func (ls *LanguageService) newChangeTracker(ctx context.Context) *changeTracker { +func NewTracker(ctx context.Context, compilerOptions *core.CompilerOptions, formatOptions *format.FormatCodeSettings, converters *lsconv.Converters) *Tracker { emitContext := printer.NewEmitContext() - newLine := ls.GetProgram().Options().NewLine.GetNewLineCharacter() - formatCodeSettings := ls.FormatOptions() - ctx = format.WithFormatCodeSettings(ctx, formatCodeSettings, newLine) // !!! formatSettings in context? - return &changeTracker{ - ls: ls, + newLine := compilerOptions.NewLine.GetNewLineCharacter() + ctx = format.WithFormatCodeSettings(ctx, formatOptions, newLine) // !!! formatSettings in context? + return &Tracker{ EmitContext: emitContext, NodeFactory: &emitContext.Factory.NodeFactory, changes: &collections.MultiMap[*ast.SourceFile, *trackerEdit]{}, ctx: ctx, - formatSettings: formatCodeSettings, + converters: converters, + formatSettings: formatOptions, newLine: newLine, } } // !!! address strada note // - Note: after calling this, the TextChanges object must be discarded! -func (ct *changeTracker) getChanges() map[string][]*lsproto.TextEdit { +func (t *Tracker) GetChanges() map[string][]*lsproto.TextEdit { // !!! finishDeleteDeclarations // !!! finishClassesWithNodesInsertedAtStart - changes := ct.getTextChangesFromChanges() + changes := t.getTextChangesFromChanges() // !!! changes for new files return changes } -func (ct *changeTracker) replaceNode(sourceFile *ast.SourceFile, oldNode *ast.Node, newNode *ast.Node, options *changeNodeOptions) { +func (t *Tracker) ReplaceNode(sourceFile *ast.SourceFile, oldNode *ast.Node, newNode *ast.Node, options *NodeOptions) { if options == nil { // defaults to `useNonAdjustedPositions` - options = &changeNodeOptions{ + options = &NodeOptions{ leadingTriviaOption: leadingTriviaOptionExclude, trailingTriviaOption: trailingTriviaOptionExclude, } } - ct.replaceRange(sourceFile, ct.getAdjustedRange(sourceFile, oldNode, oldNode, options.leadingTriviaOption, options.trailingTriviaOption), newNode, *options) + t.ReplaceRange(sourceFile, t.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 (t *Tracker) ReplaceRange(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNode *ast.Node, options NodeOptions) { + t.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithSingleNode, Range: lsprotoRange, options: options, Node: newNode}) } -func (ct *changeTracker) replaceRangeWithText(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, text string) { - ct.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindText, Range: lsprotoRange, NewText: text}) +func (t *Tracker) ReplaceRangeWithText(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, text string) { + t.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) { +func (t *Tracker) ReplaceRangeWithNodes(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNodes []*ast.Node, options NodeOptions) { if len(newNodes) == 1 { - ct.replaceRange(sourceFile, lsprotoRange, newNodes[0], options) + t.ReplaceRange(sourceFile, lsprotoRange, newNodes[0], options) return } - ct.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithMultipleNodes, Range: lsprotoRange, nodes: newNodes, options: options}) + t.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 (t *Tracker) InsertText(sourceFile *ast.SourceFile, pos lsproto.Position, text string) { + t.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 (t *Tracker) InsertNodeAt(sourceFile *ast.SourceFile, pos core.TextPos, newNode *ast.Node, options NodeOptions) { + lsPos := t.converters.PositionToLineAndCharacter(sourceFile, pos) + t.ReplaceRange(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNode, options) } -func (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 (t *Tracker) InsertNodesAt(sourceFile *ast.SourceFile, pos core.TextPos, newNodes []*ast.Node, options NodeOptions) { + lsPos := t.converters.PositionToLineAndCharacter(sourceFile, pos) + t.ReplaceRangeWithNodes(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNodes, options) } -func (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 (t *Tracker) InsertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) { + endPosition := t.endPosForInsertNodeAfter(sourceFile, after, newNode) + t.InsertNodeAt(sourceFile, endPosition, newNode, t.getInsertNodeAfterOptions(sourceFile, after)) } -func (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 (t *Tracker) InsertNodesAfter(sourceFile *ast.SourceFile, after *ast.Node, newNodes []*ast.Node) { + endPosition := t.endPosForInsertNodeAfter(sourceFile, after, newNodes[0]) + t.InsertNodesAt(sourceFile, endPosition, newNodes, t.getInsertNodeAfterOptions(sourceFile, after)) } -func (ct *changeTracker) insertNodeBefore(sourceFile *ast.SourceFile, before *ast.Node, newNode *ast.Node, blankLineBetween bool) { - ct.insertNodeAt(sourceFile, core.TextPos(ct.getAdjustedStartPosition(sourceFile, before, leadingTriviaOptionNone, false)), newNode, ct.getOptionsForInsertNodeBefore(before, newNode, blankLineBetween)) +func (t *Tracker) InsertNodeBefore(sourceFile *ast.SourceFile, before *ast.Node, newNode *ast.Node, blankLineBetween bool) { + t.InsertNodeAt(sourceFile, core.TextPos(t.getAdjustedStartPosition(sourceFile, before, leadingTriviaOptionNone, false)), newNode, t.getOptionsForInsertNodeBefore(before, newNode, blankLineBetween)) } -func (ct *changeTracker) endPosForInsertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) core.TextPos { +func (t *Tracker) endPosForInsertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) core.TextPos { if (needSemicolonBetween(after, newNode)) && (rune(sourceFile.Text()[after.End()-1]) != ';') { // check if previous statement ends with semicolon // if not - insert semicolon to preserve the code from changing the meaning due to ASI - endPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End())) - ct.replaceRange(sourceFile, + endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End())) + t.ReplaceRange(sourceFile, lsproto.Range{Start: endPos, End: endPos}, sourceFile.GetOrCreateToken(ast.KindSemicolonToken, after.End(), after.End(), after.Parent), - changeNodeOptions{}, + NodeOptions{}, ) } - return core.TextPos(ct.getAdjustedEndPosition(sourceFile, after, trailingTriviaOptionNone)) + return core.TextPos(t.getAdjustedEndPosition(sourceFile, after, trailingTriviaOptionNone)) } /** @@ -188,7 +188,7 @@ func (ct *changeTracker) endPosForInsertNodeAfter(sourceFile *ast.SourceFile, af * 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) { +func (t *Tracker) InsertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node, containingList []*ast.Node) { if len(containingList) == 0 { containingList = format.GetContainingList(after, sourceFile).Nodes } @@ -221,7 +221,7 @@ func (ct *changeTracker) insertNodeInListAfter(sourceFile *ast.SourceFile, after // write separator and leading trivia of the next element as suffix suffix := scanner.TokenToString(nextToken.Kind) + sourceFile.Text()[nextToken.End():startPos] - ct.insertNodeAt(sourceFile, core.TextPos(startPos), newNode, changeNodeOptions{suffix: suffix}) + t.InsertNodeAt(sourceFile, core.TextPos(startPos), newNode, NodeOptions{Suffix: suffix}) } return } @@ -253,132 +253,140 @@ func (ct *changeTracker) insertNodeInListAfter(sourceFile *ast.SourceFile, after } separatorString := scanner.TokenToString(separator) - end := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End())) + end := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End())) if !multilineList { - ct.replaceRange(sourceFile, lsproto.Range{Start: end, End: end}, newNode, changeNodeOptions{prefix: separatorString}) + t.ReplaceRange(sourceFile, lsproto.Range{Start: end, End: end}, newNode, NodeOptions{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{}) + t.ReplaceRange(sourceFile, lsproto.Range{Start: end, End: end}, sourceFile.GetOrCreateToken(separator, after.End(), after.End()+len(separatorString), after.Parent), NodeOptions{}) // use the same indentation as 'after' item - indentation := format.FindFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, ct.formatSettings) + indentation := format.FindFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, t.formatSettings) // insert element before the line break on the line that contains 'after' element insertPos := scanner.SkipTriviaEx(sourceFile.Text(), 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( + insertLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(insertPos)) + t.ReplaceRange( sourceFile, lsproto.Range{Start: insertLSPos, End: insertLSPos}, newNode, - changeNodeOptions{ + NodeOptions{ indentation: ptrTo(indentation), - prefix: ct.newLine, + Prefix: t.newLine, }, ) } -// insertImportSpecifierAtIndex inserts a new import specifier at the specified index in a NamedImports list -func (ct *changeTracker) insertImportSpecifierAtIndex(sourceFile *ast.SourceFile, newSpecifier *ast.Node, namedImports *ast.Node, index int) { +// InsertImportSpecifierAtIndex inserts a new import specifier at the specified index in a NamedImports list +func (t *Tracker) InsertImportSpecifierAtIndex(sourceFile *ast.SourceFile, newSpecifier *ast.Node, namedImports *ast.Node, index int) { namedImportsNode := namedImports.AsNamedImports() elements := namedImportsNode.Elements.Nodes if index > 0 && len(elements) > index { - ct.insertNodeInListAfter(sourceFile, elements[index-1], newSpecifier, elements) + t.InsertNodeInListAfter(sourceFile, elements[index-1], newSpecifier, elements) } else { // Insert before the first element firstElement := elements[0] multiline := printer.GetLinesBetweenPositions(sourceFile, firstElement.Pos(), namedImports.Parent.Parent.Pos()) != 0 - ct.insertNodeBefore(sourceFile, firstElement, newSpecifier, multiline) + t.InsertNodeBefore(sourceFile, firstElement, newSpecifier, multiline) } } -func (ct *changeTracker) insertAtTopOfFile(sourceFile *ast.SourceFile, insert []*ast.Statement, blankLineBetween bool) { +func (t *Tracker) InsertAtTopOfFile(sourceFile *ast.SourceFile, insert []*ast.Statement, blankLineBetween bool) { if len(insert) == 0 { return } - pos := ct.getInsertionPositionAtSourceFileTop(sourceFile) - options := changeNodeOptions{} + pos := t.getInsertionPositionAtSourceFileTop(sourceFile) + options := NodeOptions{} if pos != 0 { - options.prefix = ct.newLine + options.Prefix = t.newLine } if len(sourceFile.Text()) == 0 || !stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) { - options.suffix = ct.newLine + options.Suffix = t.newLine } if blankLineBetween { - options.suffix += ct.newLine + options.Suffix += t.newLine } if len(insert) == 1 { - ct.insertNodeAt(sourceFile, core.TextPos(pos), insert[0], options) + t.InsertNodeAt(sourceFile, core.TextPos(pos), insert[0], options) } else { - ct.insertNodesAt(sourceFile, core.TextPos(pos), insert, options) + t.InsertNodesAt(sourceFile, core.TextPos(pos), insert, options) } } -func (ct *changeTracker) getInsertNodeAfterOptions(sourceFile *ast.SourceFile, node *ast.Node) changeNodeOptions { - newLineChar := ct.newLine - var options changeNodeOptions +func (t *Tracker) getInsertNodeAfterOptions(sourceFile *ast.SourceFile, node *ast.Node) NodeOptions { + newLineChar := t.newLine + var options NodeOptions switch node.Kind { case ast.KindParameter: // default opts - options = changeNodeOptions{} + options = NodeOptions{} case ast.KindClassDeclaration, ast.KindModuleDeclaration: - options = changeNodeOptions{prefix: newLineChar, suffix: newLineChar} + options = NodeOptions{Prefix: newLineChar, Suffix: newLineChar} case ast.KindVariableDeclaration, ast.KindStringLiteral, ast.KindIdentifier: - options = changeNodeOptions{prefix: ", "} + options = NodeOptions{Prefix: ", "} case ast.KindPropertyAssignment: - options = changeNodeOptions{suffix: "," + newLineChar} + options = NodeOptions{Suffix: "," + newLineChar} case ast.KindExportKeyword: - options = changeNodeOptions{prefix: " "} + options = NodeOptions{Prefix: " "} default: if !(ast.IsStatement(node) || ast.IsClassOrTypeElement(node)) { // Else we haven't handled this kind of node yet -- add it panic("unimplemented node type " + node.Kind.String() + " in changeTracker.getInsertNodeAfterOptions") } - options = changeNodeOptions{suffix: newLineChar} + options = NodeOptions{Suffix: newLineChar} } if node.End() == sourceFile.End() && ast.IsStatement(node) { - options.prefix = "\n" + options.prefix + options.Prefix = "\n" + options.Prefix } return options } -func (ct *changeTracker) getOptionsForInsertNodeBefore(before *ast.Node, inserted *ast.Node, blankLineBetween bool) changeNodeOptions { +func (t *Tracker) getOptionsForInsertNodeBefore(before *ast.Node, inserted *ast.Node, blankLineBetween bool) NodeOptions { if ast.IsStatement(before) || ast.IsClassOrTypeElement(before) { if blankLineBetween { - return changeNodeOptions{suffix: ct.newLine + ct.newLine} + return NodeOptions{Suffix: t.newLine + t.newLine} } - return changeNodeOptions{suffix: ct.newLine} + return NodeOptions{Suffix: t.newLine} } else if before.Kind == ast.KindVariableDeclaration { // insert `x = 1, ` into `const x = 1, y = 2; - return changeNodeOptions{suffix: ", "} + return NodeOptions{Suffix: ", "} } else if before.Kind == ast.KindParameter { if inserted.Kind == ast.KindParameter { - return changeNodeOptions{suffix: ", "} + return NodeOptions{Suffix: ", "} } - return changeNodeOptions{} + return NodeOptions{} } else if (before.Kind == ast.KindStringLiteral && before.Parent != nil && before.Parent.Kind == ast.KindImportDeclaration) || before.Kind == ast.KindNamedImports { - return changeNodeOptions{suffix: ", "} + return NodeOptions{Suffix: ", "} } else if before.Kind == ast.KindImportSpecifier { suffix := "," if blankLineBetween { - suffix += ct.newLine + suffix += t.newLine } else { suffix += " " } - return changeNodeOptions{suffix: suffix} + return NodeOptions{Suffix: suffix} } // We haven't handled this kind of node yet -- add it panic("unimplemented node type " + before.Kind.String() + " in changeTracker.getOptionsForInsertNodeBefore") } + +func ptrTo[T any](v T) *T { + return &v +} + +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)) +} diff --git a/internal/ls/changetrackerimpl.go b/internal/ls/change/trackerimpl.go similarity index 74% rename from internal/ls/changetrackerimpl.go rename to internal/ls/change/trackerimpl.go index b36c1ea982..92b1babd2e 100644 --- a/internal/ls/changetrackerimpl.go +++ b/internal/ls/change/trackerimpl.go @@ -1,4 +1,4 @@ -package ls +package change import ( "fmt" @@ -10,6 +10,7 @@ import ( "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/parser" "github.com/microsoft/typescript-go/internal/printer" @@ -17,15 +18,15 @@ import ( "github.com/microsoft/typescript-go/internal/stringutil" ) -func (ct *changeTracker) getTextChangesFromChanges() map[string][]*lsproto.TextEdit { +func (t *Tracker) getTextChangesFromChanges() map[string][]*lsproto.TextEdit { changes := map[string][]*lsproto.TextEdit{} - for sourceFile, changesInFile := range ct.changes.M { + for sourceFile, changesInFile := range t.changes.M { // order changes by start position // If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa. - slices.SortStableFunc(changesInFile, func(a, b *trackerEdit) int { return CompareRanges(ptrTo(a.Range), ptrTo(b.Range)) }) + slices.SortStableFunc(changesInFile, func(a, b *trackerEdit) int { return lsproto.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 { + if lsproto.ComparePositions(changesInFile[i].Range.End, changesInFile[i+1].Range.Start) > 0 { // assert change[i].End <= change[i + 1].Start panic(fmt.Sprintf("changes overlap: %v and %v", changesInFile[i].Range, changesInFile[i+1].Range)) } @@ -34,7 +35,7 @@ func (ct *changeTracker) getTextChangesFromChanges() map[string][]*lsproto.TextE textChanges := core.MapNonNil(changesInFile, func(change *trackerEdit) *lsproto.TextEdit { // !!! targetSourceFile - newText := ct.computeNewText(change, sourceFile, sourceFile) + newText := t.computeNewText(change, sourceFile, sourceFile) // span := createTextSpanFromRange(c.Range) // !!! // Filter out redundant changes. @@ -53,7 +54,7 @@ func (ct *changeTracker) getTextChangesFromChanges() map[string][]*lsproto.TextE return changes } -func (ct *changeTracker) computeNewText(change *trackerEdit, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile) string { +func (t *Tracker) computeNewText(change *trackerEdit, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile) string { switch change.kind { case trackerEditKindRemove: return "" @@ -61,9 +62,9 @@ func (ct *changeTracker) computeNewText(change *trackerEdit, targetSourceFile *a return change.NewText } - pos := int(ct.ls.converters.LineAndCharacterToPosition(sourceFile, change.Range.Start)) + pos := int(t.converters.LineAndCharacterToPosition(sourceFile, change.Range.Start)) formatNode := func(n *ast.Node) string { - return ct.getFormattedTextOfNode(n, targetSourceFile, sourceFile, pos, change.options) + return t.getFormattedTextOfNode(n, targetSourceFile, sourceFile, pos, change.options) } var text string @@ -71,9 +72,9 @@ func (ct *changeTracker) computeNewText(change *trackerEdit, targetSourceFile *a case trackerEditKindReplaceWithMultipleNodes: if change.options.joiner == "" { - change.options.joiner = ct.newLine + change.options.joiner = t.newLine } - text = strings.Join(core.Map(change.nodes, func(n *ast.Node) string { return strings.TrimSuffix(formatNode(n), ct.newLine) }), change.options.joiner) + text = strings.Join(core.Map(change.nodes, func(n *ast.Node) string { return strings.TrimSuffix(formatNode(n), t.newLine) }), change.options.joiner) case trackerEditKindReplaceWithSingleNode: text = formatNode(change.Node) default: @@ -84,14 +85,14 @@ func (ct *changeTracker) computeNewText(change *trackerEdit, targetSourceFile *a 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 + core.IfElse(strings.HasSuffix(noIndent, change.options.suffix), "", change.options.suffix) + return change.options.Prefix + noIndent + core.IfElse(strings.HasSuffix(noIndent, change.options.Suffix), "", change.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) +func (t *Tracker) getFormattedTextOfNode(nodeIn *ast.Node, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile, pos int, options NodeOptions) string { + text, sourceFileLike := t.getNonformattedText(nodeIn, targetSourceFile) // !!! if (validate) validate(node, text); - formatOptions := getFormatCodeSettingsForWriting(ct.formatSettings, targetSourceFile) + formatOptions := getFormatCodeSettingsForWriting(t.formatSettings, targetSourceFile) var initialIndentation, delta int if options.indentation == nil { @@ -107,13 +108,13 @@ func (ct *changeTracker) getFormattedTextOfNode(nodeIn *ast.Node, targetSourceFi delta = formatOptions.IndentSize } - changes := format.FormatNodeGivenIndentation(ct.ctx, sourceFileLike, sourceFileLike.AsSourceFile(), targetSourceFile.LanguageVariant, initialIndentation, delta) + changes := format.FormatNodeGivenIndentation(t.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) + shouldRemoveSemicolons := options.Semicolons == format.SemicolonPreferenceRemove || shouldAutoDetectSemicolonPreference && !lsutil.ProbablyUsesSemicolons(sourceFile) if shouldRemoveSemicolons { options.Semicolons = format.SemicolonPreferenceRemove } @@ -121,39 +122,39 @@ func getFormatCodeSettingsForWriting(options *format.FormatCodeSettings, sourceF return options } -func (ct *changeTracker) getNonformattedText(node *ast.Node, sourceFile *ast.SourceFile) (string, *ast.Node) { +func (t *Tracker) getNonformattedText(node *ast.Node, sourceFile *ast.SourceFile) (string, *ast.Node) { nodeIn := node - eofToken := ct.Factory.NewToken(ast.KindEndOfFile) + eofToken := t.Factory.NewToken(ast.KindEndOfFile) if ast.IsStatement(node) { - nodeIn = ct.Factory.NewSourceFile( + nodeIn = t.Factory.NewSourceFile( ast.SourceFileParseOptions{FileName: sourceFile.FileName(), Path: sourceFile.Path()}, "", - ct.Factory.NewNodeList([]*ast.Node{node}), - ct.Factory.NewToken(ast.KindEndOfFile), + t.Factory.NewNodeList([]*ast.Node{node}), + t.Factory.NewToken(ast.KindEndOfFile), ) } - writer := printer.NewChangeTrackerWriter(ct.newLine) + writer := printer.NewChangeTrackerWriter(t.newLine) printer.NewPrinter( printer.PrinterOptions{ - NewLine: core.GetNewLineKind(ct.newLine), + NewLine: core.GetNewLineKind(t.newLine), NeverAsciiEscape: true, PreserveSourceNewlines: true, TerminateUnterminatedLiterals: true, }, writer.GetPrintHandlers(), - ct.EmitContext, + t.EmitContext, ).Write(nodeIn, sourceFile, writer, nil) text := writer.String() - text = strings.TrimSuffix(text, ct.newLine) // Newline artifact from printing a SourceFile instead of a node + text = strings.TrimSuffix(text, t.newLine) // Newline artifact from printing a SourceFile instead of a node - nodeOut := writer.AssignPositionsToNode(nodeIn, ct.NodeFactory) + nodeOut := writer.AssignPositionsToNode(nodeIn, t.NodeFactory) var sourceFileLike *ast.Node if !ast.IsStatement(node) { - nodeList := ct.Factory.NewNodeList([]*ast.Node{nodeOut}) + nodeList := t.Factory.NewNodeList([]*ast.Node{nodeOut}) nodeList.Loc = nodeOut.Loc eofToken.Loc = core.NewTextRange(nodeOut.End(), nodeOut.End()) - sourceFileLike = ct.Factory.NewSourceFile( + sourceFileLike = t.Factory.NewSourceFile( ast.SourceFileParseOptions{FileName: sourceFile.FileName(), Path: sourceFile.Path()}, text, nodeList, @@ -171,18 +172,20 @@ func (ct *changeTracker) getNonformattedText(node *ast.Node, sourceFile *ast.Sou } // 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), +func (t *Tracker) getAdjustedRange(sourceFile *ast.SourceFile, startNode *ast.Node, endNode *ast.Node, leadingOption leadingTriviaOption, trailingOption trailingTriviaOption) lsproto.Range { + return t.converters.ToLSPRange( sourceFile, + core.NewTextRange( + t.getAdjustedStartPosition(sourceFile, startNode, leadingOption, false), + t.getAdjustedEndPosition(sourceFile, endNode, trailingOption), + ), ) } // method on the changeTracker because use of converters -func (ct *changeTracker) getAdjustedStartPosition(sourceFile *ast.SourceFile, node *ast.Node, leadingOption leadingTriviaOption, hasTrailingComment bool) int { +func (t *Tracker) 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 { + if JSDocComments := parser.GetJSDocCommentRanges(t.NodeFactory, nil, node, sourceFile.Text()); len(JSDocComments) > 0 { return format.GetLineStartPositionForPosition(JSDocComments[0].Pos(), sourceFile) } } @@ -225,9 +228,9 @@ func (ct *changeTracker) getAdjustedStartPosition(sourceFile *ast.SourceFile, no 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)) + comments := slices.Collect(scanner.GetLeadingCommentRanges(t.NodeFactory, sourceFile.Text(), fullStart)) if len(comments) == 0 { - comments = slices.Collect(scanner.GetTrailingCommentRanges(ct.NodeFactory, sourceFile.Text(), fullStart)) + comments = slices.Collect(scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), fullStart)) } if len(comments) > 0 { return scanner.SkipTriviaEx(sourceFile.Text(), comments[0].End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true}) @@ -245,13 +248,13 @@ func (ct *changeTracker) getAdjustedStartPosition(sourceFile *ast.SourceFile, no // 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 { +func (t *Tracker) getEndPositionOfMultilineTrailingComment(sourceFile *ast.SourceFile, node *ast.Node, trailingOpt trailingTriviaOption) int { if trailingOpt == trailingTriviaOptionInclude { // If the trailing comment is a multiline comment that extends to the next lines, // return the end of the comment and track it for the next nodes to adjust. lineStarts := sourceFile.ECMALineMap() nodeEndLine := scanner.ComputeLineOfPosition(lineStarts, node.End()) - for comment := range scanner.GetTrailingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End()) { + for comment := range scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End()) { // Single line can break the loop as trivia will only be this line. // Comments on subsequest lines are also ignored. if comment.Kind == ast.KindSingleLineCommentTrivia || scanner.ComputeLineOfPosition(lineStarts, comment.Pos()) > nodeEndLine { @@ -271,14 +274,14 @@ func (ct *changeTracker) getEndPositionOfMultilineTrailingComment(sourceFile *as } // method on the changeTracker because of converters -func (ct *changeTracker) getAdjustedEndPosition(sourceFile *ast.SourceFile, node *ast.Node, trailingTriviaOption trailingTriviaOption) int { +func (t *Tracker) getAdjustedEndPosition(sourceFile *ast.SourceFile, node *ast.Node, trailingTriviaOption trailingTriviaOption) int { if trailingTriviaOption == trailingTriviaOptionExclude { return node.End() } if trailingTriviaOption == trailingTriviaOptionExcludeWhitespace { if comments := slices.AppendSeq( - slices.Collect(scanner.GetTrailingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End())), - scanner.GetLeadingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End()), + slices.Collect(scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End())), + scanner.GetLeadingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End()), ); len(comments) > 0 { if realEnd := comments[len(comments)-1].End(); realEnd != 0 { return realEnd @@ -287,7 +290,7 @@ func (ct *changeTracker) getAdjustedEndPosition(sourceFile *ast.SourceFile, node return node.End() } - if multilineEndPosition := ct.getEndPositionOfMultilineTrailingComment(sourceFile, node, trailingTriviaOption); multilineEndPosition != 0 { + if multilineEndPosition := t.getEndPositionOfMultilineTrailingComment(sourceFile, node, trailingTriviaOption); multilineEndPosition != 0 { return multilineEndPosition } @@ -318,7 +321,7 @@ func needSemicolonBetween(a, b *ast.Node) bool { ast.IsStatementButNotDeclaration(b) // TODO: only if b would start with a `(` or `[` } -func (ct *changeTracker) getInsertionPositionAtSourceFileTop(sourceFile *ast.SourceFile) int { +func (t *Tracker) getInsertionPositionAtSourceFileTop(sourceFile *ast.SourceFile) int { var lastPrologue *ast.Node for _, node := range sourceFile.Statements.Nodes { if ast.IsPrologueDirective(node) { @@ -353,7 +356,7 @@ func (ct *changeTracker) getInsertionPositionAtSourceFileTop(sourceFile *ast.Sou advancePastLineBreak() } - ranges := slices.Collect(scanner.GetLeadingCommentRanges(ct.NodeFactory, text, position)) + ranges := slices.Collect(scanner.GetLeadingCommentRanges(t.NodeFactory, text, position)) if len(ranges) == 0 { return position } diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 5a63be9df5..b09040182d 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -22,8 +22,8 @@ import ( "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/format" "github.com/microsoft/typescript-go/internal/jsnum" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" - "github.com/microsoft/typescript-go/internal/lsutil" "github.com/microsoft/typescript-go/internal/nodebuilder" "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" @@ -459,7 +459,7 @@ func (l *LanguageService) getCompletionData( typeChecker *checker.Checker, file *ast.SourceFile, position int, - preferences *UserPreferences, + preferences *lsutil.UserPreferences, ) completionData { inCheckedFile := isCheckedFile(file, l.GetProgram().Options()) @@ -1940,7 +1940,7 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( clientOptions *lsproto.CompletionClientCapabilities, ) (uniqueNames collections.Set[string], sortedEntries []*lsproto.CompletionItem) { closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location) - useSemicolons := probablyUsesSemicolons(file) + useSemicolons := lsutil.ProbablyUsesSemicolons(file) typeChecker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) defer done() isMemberCompletion := isMemberCompletionKind(data.completionKind) @@ -2018,7 +2018,7 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( func completionNameForLiteral( file *ast.SourceFile, - preferences *UserPreferences, + preferences *lsutil.UserPreferences, literal literalValue, ) string { switch literal := literal.(type) { @@ -2035,7 +2035,7 @@ func completionNameForLiteral( func createCompletionItemForLiteral( file *ast.SourceFile, - preferences *UserPreferences, + preferences *lsutil.UserPreferences, literal literalValue, ) *lsproto.CompletionItem { return &lsproto.CompletionItem{ @@ -2251,13 +2251,13 @@ func (l *LanguageService) createCompletionItem( if data.isJsxIdentifierExpected && !data.isRightOfOpenTag && clientSupportsItemSnippet(clientOptions) && - preferences.JsxAttributeCompletionStyle != JsxAttributeCompletionStyleNone && + preferences.JsxAttributeCompletionStyle != lsutil.JsxAttributeCompletionStyleNone && !(ast.IsJsxAttribute(data.location.Parent) && data.location.Parent.Initializer() != nil) { - useBraces := preferences.JsxAttributeCompletionStyle == JsxAttributeCompletionStyleBraces + useBraces := preferences.JsxAttributeCompletionStyle == lsutil.JsxAttributeCompletionStyleBraces t := typeChecker.GetTypeOfSymbolAtLocation(symbol, data.location) // If is boolean like or undefined, don't return a snippet, we want to return just the completion. - if preferences.JsxAttributeCompletionStyle == JsxAttributeCompletionStyleAuto && + if preferences.JsxAttributeCompletionStyle == lsutil.JsxAttributeCompletionStyleAuto && !t.IsBooleanLike() && !(t.IsUnion() && core.Some(t.Types(), (*checker.Type).IsBooleanLike)) { if t.IsStringLike() || @@ -3152,7 +3152,7 @@ func (l *LanguageService) createRangeFromStringLiteralLikeContent(file *ast.Sour return l.createLspRangeFromBounds(nodeStart+1, replacementEnd, file) } -func quotePropertyName(file *ast.SourceFile, preferences *UserPreferences, name string) string { +func quotePropertyName(file *ast.SourceFile, preferences *lsutil.UserPreferences, name string) string { r, _ := utf8.DecodeRuneInString(name) if unicode.IsDigit(r) { return name @@ -3292,7 +3292,7 @@ func compareCompletionEntries(entryInSlice *lsproto.CompletionItem, entryToInser sliceEntryData.AutoImport != nil && sliceEntryData.AutoImport.ModuleSpecifier != "" && insertEntryData.AutoImport != nil && insertEntryData.AutoImport.ModuleSpecifier != "" { // Sort same-named auto-imports by module specifier - result = compareNumberOfDirectorySeparators( + result = tspath.CompareNumberOfDirectorySeparators( sliceEntryData.AutoImport.ModuleSpecifier, insertEntryData.AutoImport.ModuleSpecifier, ) @@ -5155,7 +5155,7 @@ func (l *LanguageService) getSymbolCompletionFromItemData( } } - completionData := l.getCompletionData(ctx, ch, file, position, &UserPreferences{IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue}) + completionData := l.getCompletionData(ctx, ch, file, position, &lsutil.UserPreferences{IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue}) if completionData == nil { return detailsData{} } @@ -5735,7 +5735,7 @@ func getJSDocParameterCompletions( position int, typeChecker *checker.Checker, options *core.CompilerOptions, - preferences *UserPreferences, + preferences *lsutil.UserPreferences, tagNameOnly bool, ) []*lsproto.CompletionItem { currentToken := astnav.GetTokenAtPosition(file, position) @@ -5877,7 +5877,7 @@ func getJSDocParamAnnotation( isSnippet bool, typeChecker *checker.Checker, options *core.CompilerOptions, - preferences *UserPreferences, + preferences *lsutil.UserPreferences, tabstopCounter *int, ) string { if isSnippet { @@ -5963,7 +5963,7 @@ func generateJSDocParamTagsForDestructuring( isSnippet bool, typeChecker *checker.Checker, options *core.CompilerOptions, - preferences *UserPreferences, + preferences *lsutil.UserPreferences, ) []string { tabstopCounter := 1 if !isJS { @@ -6003,7 +6003,7 @@ func jsDocParamPatternWorker( isSnippet bool, typeChecker *checker.Checker, options *core.CompilerOptions, - preferences *UserPreferences, + preferences *lsutil.UserPreferences, counter *int, ) []string { if ast.IsObjectBindingPattern(pattern) && dotDotDotToken == nil { @@ -6072,7 +6072,7 @@ func jsDocParamElementWorker( isSnippet bool, typeChecker *checker.Checker, options *core.CompilerOptions, - preferences *UserPreferences, + preferences *lsutil.UserPreferences, counter *int, ) []string { if ast.IsIdentifier(element.Name()) { // `{ b }` or `{ b: newB }` diff --git a/internal/ls/definition.go b/internal/ls/definition.go index abefaf932e..ac9f0a722d 100644 --- a/internal/ls/definition.go +++ b/internal/ls/definition.go @@ -152,7 +152,7 @@ func getDeclarationsFromLocation(c *checker.Checker, node *ast.Node) []*ast.Node // Returns a CallLikeExpression where `node` is the target being invoked. func getAncestorCallLikeExpression(node *ast.Node) *ast.Node { target := ast.FindAncestor(node, func(n *ast.Node) bool { - return !isRightSideOfPropertyAccess(n) + return !ast.IsRightSideOfPropertyAccess(n) }) callLike := target.Parent if callLike != nil && ast.IsCallLikeExpression(callLike) && ast.GetInvokedExpression(callLike) == target { diff --git a/internal/ls/diagnostics.go b/internal/ls/diagnostics.go index 5eeadfa06a..684e3bcc0b 100644 --- a/internal/ls/diagnostics.go +++ b/internal/ls/diagnostics.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/diagnosticwriter" + "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" ) @@ -30,7 +31,7 @@ func (l *LanguageService) ProvideDiagnostics(ctx context.Context, uri lsproto.Do }, nil } -func toLSPDiagnostics(converters *Converters, diagnostics ...[]*ast.Diagnostic) []*lsproto.Diagnostic { +func toLSPDiagnostics(converters *lsconv.Converters, diagnostics ...[]*ast.Diagnostic) []*lsproto.Diagnostic { size := 0 for _, diagSlice := range diagnostics { size += len(diagSlice) @@ -44,7 +45,7 @@ func toLSPDiagnostics(converters *Converters, diagnostics ...[]*ast.Diagnostic) return lspDiagnostics } -func toLSPDiagnostic(converters *Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic { +func toLSPDiagnostic(converters *lsconv.Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic { var severity lsproto.DiagnosticSeverity switch diagnostic.Category() { case diagnostics.CategorySuggestion: @@ -61,7 +62,7 @@ func toLSPDiagnostic(converters *Converters, diagnostic *ast.Diagnostic) *lsprot for _, related := range diagnostic.RelatedInformation() { relatedInformation = append(relatedInformation, &lsproto.DiagnosticRelatedInformation{ Location: lsproto.Location{ - Uri: FileNameToDocumentURI(related.File().FileName()), + Uri: lsconv.FileNameToDocumentURI(related.File().FileName()), Range: converters.ToLSPRange(related.File(), related.Loc()), }, Message: related.Message(), diff --git a/internal/ls/documenthighlights.go b/internal/ls/documenthighlights.go index 65d7039666..526adb5f9e 100644 --- a/internal/ls/documenthighlights.go +++ b/internal/ls/documenthighlights.go @@ -7,7 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" - "github.com/microsoft/typescript-go/internal/lsutil" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" diff --git a/internal/ls/findallreferences.go b/internal/ls/findallreferences.go index e5df1d9d9d..11e51ec670 100644 --- a/internal/ls/findallreferences.go +++ b/internal/ls/findallreferences.go @@ -15,6 +15,7 @@ import ( "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/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" @@ -466,7 +467,7 @@ func (l *LanguageService) ProvideRename(ctx context.Context, params *lsproto.Ren checker, done := program.GetTypeChecker(ctx) defer done() for _, entry := range entries { - uri := FileNameToDocumentURI(l.getFileNameOfEntry(entry)) + uri := lsconv.FileNameToDocumentURI(l.getFileNameOfEntry(entry)) textEdit := &lsproto.TextEdit{ Range: *l.getRangeOfEntry(entry), NewText: l.getTextForRename(node, entry, params.NewName, checker), @@ -537,7 +538,7 @@ func (l *LanguageService) convertEntriesToLocations(entries []*referenceEntry) [ locations := make([]lsproto.Location, len(entries)) for i, entry := range entries { locations[i] = lsproto.Location{ - Uri: FileNameToDocumentURI(l.getFileNameOfEntry(entry)), + Uri: lsconv.FileNameToDocumentURI(l.getFileNameOfEntry(entry)), Range: *l.getRangeOfEntry(entry), } } @@ -589,7 +590,7 @@ func (l *LanguageService) mergeReferences(program *compiler.Program, referencesT return cmp.Compare(entry1File, entry2File) } - return CompareRanges(l.getRangeOfEntry(entry1), l.getRangeOfEntry(entry2)) + return lsproto.CompareRanges(l.getRangeOfEntry(entry1), l.getRangeOfEntry(entry2)) }) result[refIndex] = &SymbolAndEntries{ definition: reference.definition, @@ -1358,7 +1359,7 @@ func getReferenceEntriesForShorthandPropertyAssignment(node *ast.Node, checker * shorthandSymbol := checker.GetShorthandAssignmentValueSymbol(refSymbol.ValueDeclaration) if shorthandSymbol != nil && len(shorthandSymbol.Declarations) > 0 { for _, declaration := range shorthandSymbol.Declarations { - if getMeaningFromDeclaration(declaration)&ast.SemanticMeaningValue != 0 { + if ast.GetMeaningFromDeclaration(declaration)&ast.SemanticMeaningValue != 0 { addReference(declaration) } } @@ -1366,7 +1367,7 @@ func getReferenceEntriesForShorthandPropertyAssignment(node *ast.Node, checker * } func climbPastPropertyAccess(node *ast.Node) *ast.Node { - if isRightSideOfPropertyAccess(node) { + if ast.IsRightSideOfPropertyAccess(node) { return node.Parent } return node diff --git a/internal/ls/host.go b/internal/ls/host.go index 817fde173c..8f517b787c 100644 --- a/internal/ls/host.go +++ b/internal/ls/host.go @@ -2,14 +2,16 @@ package ls import ( "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/ls/lsconv" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/sourcemap" ) type Host interface { UseCaseSensitiveFileNames() bool ReadFile(path string) (contents string, ok bool) - Converters() *Converters - UserPreferences() *UserPreferences + Converters() *lsconv.Converters + UserPreferences() *lsutil.UserPreferences FormatOptions() *format.FormatCodeSettings GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo } diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index 20038a3e07..e2fc95f24c 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -4,6 +4,8 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/ls/lsconv" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/sourcemap" ) @@ -11,7 +13,7 @@ import ( type LanguageService struct { host Host program *compiler.Program - converters *Converters + converters *lsconv.Converters documentPositionMappers map[string]*sourcemap.DocumentPositionMapper } @@ -31,7 +33,7 @@ func (l *LanguageService) GetProgram() *compiler.Program { return l.program } -func (l *LanguageService) UserPreferences() *UserPreferences { +func (l *LanguageService) UserPreferences() *lsutil.UserPreferences { return l.host.UserPreferences() } diff --git a/internal/ls/converters.go b/internal/ls/lsconv/converters.go similarity index 99% rename from internal/ls/converters.go rename to internal/ls/lsconv/converters.go index 2fb9d262a3..3b290b446c 100644 --- a/internal/ls/converters.go +++ b/internal/ls/lsconv/converters.go @@ -1,4 +1,4 @@ -package ls +package lsconv import ( "fmt" diff --git a/internal/ls/converters_test.go b/internal/ls/lsconv/converters_test.go similarity index 96% rename from internal/ls/converters_test.go rename to internal/ls/lsconv/converters_test.go index 25fc94bce2..30babd1df5 100644 --- a/internal/ls/converters_test.go +++ b/internal/ls/lsconv/converters_test.go @@ -1,9 +1,9 @@ -package ls_test +package lsconv_test import ( "testing" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "gotest.tools/v3/assert" ) @@ -77,7 +77,7 @@ func TestFileNameToDocumentURI(t *testing.T) { for _, test := range tests { t.Run(test.fileName, func(t *testing.T) { t.Parallel() - assert.Equal(t, ls.FileNameToDocumentURI(test.fileName), test.uri) + assert.Equal(t, lsconv.FileNameToDocumentURI(test.fileName), test.uri) }) } } diff --git a/internal/ls/linemap.go b/internal/ls/lsconv/linemap.go similarity index 99% rename from internal/ls/linemap.go rename to internal/ls/lsconv/linemap.go index 345f7cd997..8fc8cc0241 100644 --- a/internal/ls/linemap.go +++ b/internal/ls/lsconv/linemap.go @@ -1,4 +1,4 @@ -package ls +package lsconv import ( "cmp" diff --git a/internal/lsutil/asi.go b/internal/ls/lsutil/asi.go similarity index 100% rename from internal/lsutil/asi.go rename to internal/ls/lsutil/asi.go diff --git a/internal/lsutil/children.go b/internal/ls/lsutil/children.go similarity index 100% rename from internal/lsutil/children.go rename to internal/ls/lsutil/children.go diff --git a/internal/ls/userpreferences.go b/internal/ls/lsutil/userpreferences.go similarity index 99% rename from internal/ls/userpreferences.go rename to internal/ls/lsutil/userpreferences.go index aece6a49a7..dd900658ce 100644 --- a/internal/ls/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -1,4 +1,4 @@ -package ls +package lsutil import ( "slices" diff --git a/internal/ls/lsutil/utilities.go b/internal/ls/lsutil/utilities.go new file mode 100644 index 0000000000..0dd5e7579f --- /dev/null +++ b/internal/ls/lsutil/utilities.go @@ -0,0 +1,83 @@ +package lsutil + +import ( + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/compiler" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/scanner" +) + +func ProbablyUsesSemicolons(file *ast.SourceFile) bool { + withSemicolon := 0 + withoutSemicolon := 0 + nStatementsToObserve := 5 + + var visit func(node *ast.Node) bool + visit = func(node *ast.Node) bool { + if node.Flags&ast.NodeFlagsReparsed != 0 { + return false + } + if SyntaxRequiresTrailingSemicolonOrASI(node.Kind) { + lastToken := GetLastToken(node, file) + if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken { + withSemicolon++ + } else { + withoutSemicolon++ + } + } else if SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) { + lastToken := GetLastToken(node, file) + if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken { + withSemicolon++ + } else if lastToken != nil && lastToken.Kind != ast.KindCommaToken { + lastTokenLine, _ := scanner.GetECMALineAndCharacterOfPosition( + file, + astnav.GetStartOfNode(lastToken, file, false /*includeJSDoc*/)) + nextTokenLine, _ := scanner.GetECMALineAndCharacterOfPosition( + file, + scanner.GetRangeOfTokenAtPosition(file, lastToken.End()).Pos()) + // Avoid counting missing semicolon in single-line objects: + // `function f(p: { x: string /*no semicolon here is insignificant*/ }) {` + if lastTokenLine != nextTokenLine { + withoutSemicolon++ + } + } + } + + if withSemicolon+withoutSemicolon >= nStatementsToObserve { + return true + } + + return node.ForEachChild(visit) + } + + file.ForEachChild(visit) + + // One statement missing a semicolon isn't sufficient evidence to say the user + // doesn't want semicolons, because they may not even be done writing that statement. + if withSemicolon == 0 && withoutSemicolon <= 1 { + return true + } + + // If even 2/5 places have a semicolon, the user probably wants semicolons + if withoutSemicolon == 0 { + return true + } + return withSemicolon/withoutSemicolon > 1/nStatementsToObserve +} + +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() +} diff --git a/internal/ls/organizeimports.go b/internal/ls/organizeimports/organizeimports.go similarity index 57% rename from internal/ls/organizeimports.go rename to internal/ls/organizeimports/organizeimports.go index 973fa269e1..f44271f109 100644 --- a/internal/ls/organizeimports.go +++ b/internal/ls/organizeimports/organizeimports.go @@ -1,14 +1,12 @@ -package ls +package organizeimports import ( "cmp" "math" - "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/ls/lsutil" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -23,119 +21,11 @@ var ( ) // statement = anyImportOrRequireStatement -func getImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int { +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 (l *LanguageService) compareModuleSpecifiers( - a *ImportFix, // !!! ImportFixWithModuleSpecifier - b *ImportFix, // !!! ImportFixWithModuleSpecifier - importingFile *ast.SourceFile, // | FutureSourceFile, - 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, l.UserPreferences()); comparison != 0 { - return comparison - } - if comparison := compareNodeCoreModuleSpecifiers(a.moduleSpecifier, b.moduleSpecifier, importingFile, l.GetProgram()); 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" -} - func getOrganizeImportsOrdinalStringComparer(ignoreCase bool) func(a, b string) int { if ignoreCase { return stringutil.CompareStringsCaseInsensitiveEslintCompatible @@ -185,10 +75,10 @@ func getExternalModuleName(specifier *ast.Expression) string { func compareModuleSpecifiersWorker(m1 *ast.Expression, m2 *ast.Expression, comparer func(a, b string) int) int { name1 := getExternalModuleName(m1) name2 := getExternalModuleName(m2) - if cmp := compareBooleans(name1 == "", name2 == ""); cmp != 0 { + if cmp := core.CompareBooleans(name1 == "", name2 == ""); cmp != 0 { return cmp } - if cmp := compareBooleans(tspath.IsExternalModuleNameRelative(name1), tspath.IsExternalModuleNameRelative(name2)); cmp != 0 { + if cmp := core.CompareBooleans(tspath.IsExternalModuleNameRelative(name1), tspath.IsExternalModuleNameRelative(name2)); cmp != 0 { return cmp } return comparer(name1, name2) @@ -235,7 +125,7 @@ func getImportKindOrder(s1 *ast.Statement) int { } // compareImportsOrRequireStatements compares two import or require statements for sorting -func compareImportsOrRequireStatements(s1 *ast.Statement, s2 *ast.Statement, comparer func(a, b string) int) int { +func CompareImportsOrRequireStatements(s1 *ast.Statement, s2 *ast.Statement, comparer func(a, b string) int) int { if cmp := compareModuleSpecifiersWorker(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer); cmp != 0 { return cmp } @@ -243,8 +133,8 @@ func compareImportsOrRequireStatements(s1 *ast.Statement, s2 *ast.Statement, com } // compareImportOrExportSpecifiers compares two import or export specifiers -func compareImportOrExportSpecifiers(s1 *ast.Node, s2 *ast.Node, comparer func(a, b string) int, preferences *UserPreferences) int { - typeOrder := OrganizeImportsTypeOrderLast +func compareImportOrExportSpecifiers(s1 *ast.Node, s2 *ast.Node, comparer func(a, b string) int, preferences *lsutil.UserPreferences) int { + typeOrder := lsutil.OrganizeImportsTypeOrderLast if preferences != nil { typeOrder = preferences.OrganizeImportsTypeOrder } @@ -253,23 +143,23 @@ func compareImportOrExportSpecifiers(s1 *ast.Node, s2 *ast.Node, comparer func(a s2Name := s2.Name().Text() switch typeOrder { - case OrganizeImportsTypeOrderFirst: - if cmp := compareBooleans(s2.IsTypeOnly(), s1.IsTypeOnly()); cmp != 0 { + case lsutil.OrganizeImportsTypeOrderFirst: + if cmp := core.CompareBooleans(s2.IsTypeOnly(), s1.IsTypeOnly()); cmp != 0 { return cmp } return comparer(s1Name, s2Name) - case OrganizeImportsTypeOrderInline: + case lsutil.OrganizeImportsTypeOrderInline: return comparer(s1Name, s2Name) default: // OrganizeImportsTypeOrderLast - if cmp := compareBooleans(s1.IsTypeOnly(), s2.IsTypeOnly()); cmp != 0 { + if cmp := core.CompareBooleans(s1.IsTypeOnly(), s2.IsTypeOnly()); cmp != 0 { return cmp } return comparer(s1Name, s2Name) } } -// getNamedImportSpecifierComparer returns a comparer function for import/export specifiers -func getNamedImportSpecifierComparer(preferences *UserPreferences, comparer func(a, b string) int) func(s1, s2 *ast.Node) int { +// GetNamedImportSpecifierComparer returns a comparer function for import/export specifiers +func GetNamedImportSpecifierComparer(preferences *lsutil.UserPreferences, comparer func(a, b string) int) func(s1, s2 *ast.Node) int { if comparer == nil { ignoreCase := false if preferences != nil && !preferences.OrganizeImportsIgnoreCase.IsUnknown() { @@ -283,19 +173,19 @@ func getNamedImportSpecifierComparer(preferences *UserPreferences, comparer func } // getImportSpecifierInsertionIndex finds the insertion index for a new import specifier -func getImportSpecifierInsertionIndex(sortedImports []*ast.Node, newImport *ast.Node, comparer func(s1, s2 *ast.Node) int) int { +func GetImportSpecifierInsertionIndex(sortedImports []*ast.Node, newImport *ast.Node, comparer func(s1, s2 *ast.Node) int) int { return core.FirstResult(core.BinarySearchUniqueFunc(sortedImports, func(mid int, value *ast.Node) int { return comparer(value, newImport) })) } // getOrganizeImportsStringComparerWithDetection detects the string comparer to use based on existing imports -func (l *LanguageService) getOrganizeImportsStringComparerWithDetection(originalImportDecls []*ast.Statement) (comparer func(a, b string) int, isSorted bool) { - result := detectModuleSpecifierCaseBySort([][]*ast.Statement{originalImportDecls}, getComparers(l.UserPreferences())) +func GetOrganizeImportsStringComparerWithDetection(originalImportDecls []*ast.Statement, preferences *lsutil.UserPreferences) (comparer func(a, b string) int, isSorted bool) { + result := detectModuleSpecifierCaseBySort([][]*ast.Statement{originalImportDecls}, getComparers(preferences)) return result.comparer, result.isSorted } -func getComparers(preferences *UserPreferences) []func(a string, b string) int { +func getComparers(preferences *lsutil.UserPreferences) []func(a string, b string) int { if preferences != nil { switch preferences.OrganizeImportsIgnoreCase { case core.TSTrue: @@ -370,12 +260,11 @@ func measureSortedness[T any](arr []T, comparer func(a, b T) int) int { return i } -// getNamedImportSpecifierComparerWithDetection detects the appropriate comparer for named imports -func (l *LanguageService) getNamedImportSpecifierComparerWithDetection(importDecl *ast.Node, sourceFile *ast.SourceFile) (specifierComparer func(s1, s2 *ast.Node) int, isSorted core.Tristate) { - preferences := l.UserPreferences() - specifierComparer = getNamedImportSpecifierComparer(preferences, getComparers(preferences)[0]) +// GetNamedImportSpecifierComparerWithDetection detects the appropriate comparer for named imports +func GetNamedImportSpecifierComparerWithDetection(importDecl *ast.Node, sourceFile *ast.SourceFile, preferences *lsutil.UserPreferences) (specifierComparer func(s1, s2 *ast.Node) int, isSorted core.Tristate) { + specifierComparer = GetNamedImportSpecifierComparer(preferences, getComparers(preferences)[0]) // Try to detect from the current import declaration - if (preferences == nil || preferences.OrganizeImportsIgnoreCase.IsUnknown() || preferences.OrganizeImportsTypeOrder == OrganizeImportsTypeOrderLast) && + if (preferences == nil || preferences.OrganizeImportsIgnoreCase.IsUnknown() || preferences.OrganizeImportsTypeOrder == lsutil.OrganizeImportsTypeOrderLast) && importDecl.Kind == ast.KindImportDeclaration { // For now, just return the default comparer // Full detection logic would require porting detectNamedImportOrganizationBySort diff --git a/internal/ls/source_map.go b/internal/ls/source_map.go index c970070558..bbda05ddfe 100644 --- a/internal/ls/source_map.go +++ b/internal/ls/source_map.go @@ -3,6 +3,7 @@ package ls import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/debug" + "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/sourcemap" "github.com/microsoft/typescript-go/internal/tspath" @@ -13,7 +14,7 @@ func (l *LanguageService) getMappedLocation(fileName string, fileRange core.Text if startPos == nil { lspRange := l.createLspRangeFromRange(fileRange, l.getScript(fileName)) return lsproto.Location{ - Uri: FileNameToDocumentURI(fileName), + Uri: lsconv.FileNameToDocumentURI(fileName), Range: *lspRange, } } @@ -28,7 +29,7 @@ func (l *LanguageService) getMappedLocation(fileName string, fileRange core.Text newRange := core.NewTextRange(startPos.Pos, endPos.Pos) lspRange := l.createLspRangeFromRange(newRange, l.getScript(startPos.FileName)) return lsproto.Location{ - Uri: FileNameToDocumentURI(startPos.FileName), + Uri: lsconv.FileNameToDocumentURI(startPos.FileName), Range: *lspRange, } } diff --git a/internal/ls/symbols.go b/internal/ls/symbols.go index eb4726d246..ca56bc4951 100644 --- a/internal/ls/symbols.go +++ b/internal/ls/symbols.go @@ -11,6 +11,7 @@ import ( "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/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" @@ -195,7 +196,7 @@ type DeclarationInfo struct { matchScore int } -func ProvideWorkspaceSymbols(ctx context.Context, programs []*compiler.Program, converters *Converters, query string) (lsproto.WorkspaceSymbolResponse, error) { +func ProvideWorkspaceSymbols(ctx context.Context, programs []*compiler.Program, converters *lsconv.Converters, query string) (lsproto.WorkspaceSymbolResponse, error) { // Obtain set of non-declaration source files from all active programs. var sourceFiles collections.Set[*ast.SourceFile] for _, program := range programs { diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 09d5a070a4..3a90865186 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -1,7 +1,6 @@ package ls import ( - "cmp" "fmt" "iter" "slices" @@ -15,33 +14,14 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/jsnum" + "github.com/microsoft/typescript-go/internal/ls/lsconv" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" - "github.com/microsoft/typescript-go/internal/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 -// ComparePositions(pos, other) == cmp.Compare(pos, other) -func ComparePositions(pos, other lsproto.Position) int { - if lineComp := cmp.Compare(pos.Line, other.Line); lineComp != 0 { - return lineComp - } - return cmp.Compare(pos.Character, other.Character) -} - -// Implements a cmp.Compare like function for two *lsproto.Range -// CompareRanges(lsRange, other) == cmp.Compare(lsrange, other) -// -// Range.Start is compared before Range.End -func CompareRanges(lsRange, other *lsproto.Range) int { - if startComp := ComparePositions(lsRange.Start, other.Start); startComp != 0 { - return startComp - } - return ComparePositions(lsRange.End, other.End) -} - var quoteReplacer = strings.NewReplacer("'", `\'`, `\"`, `"`) func IsInString(sourceFile *ast.SourceFile, position int, previousToken *ast.Node) bool { @@ -442,7 +422,7 @@ func (l *LanguageService) createLspRangeFromBounds(start, end int, file *ast.Sou return &lspRange } -func (l *LanguageService) createLspRangeFromRange(textRange core.TextRange, script Script) *lsproto.Range { +func (l *LanguageService) createLspRangeFromRange(textRange core.TextRange, script lsconv.Script) *lsproto.Range { lspRange := l.converters.ToLSPRange(script, textRange) return &lspRange } @@ -451,7 +431,7 @@ func (l *LanguageService) createLspPosition(position int, file *ast.SourceFile) return l.converters.PositionToLineAndCharacter(file, core.TextPos(position)) } -func quote(file *ast.SourceFile, preferences *UserPreferences, text string) string { +func quote(file *ast.SourceFile, preferences *lsutil.UserPreferences, text string) string { // Editors can pass in undefined or empty string - we want to infer the preference in those cases. quotePreference := getQuotePreference(file, preferences) quoted, _ := core.StringifyJson(text, "" /*prefix*/, "" /*indent*/) @@ -475,7 +455,7 @@ func quotePreferenceFromString(str *ast.StringLiteral) quotePreference { return quotePreferenceDouble } -func getQuotePreference(sourceFile *ast.SourceFile, preferences *UserPreferences) quotePreference { +func getQuotePreference(sourceFile *ast.SourceFile, preferences *lsutil.UserPreferences) quotePreference { if preferences.QuotePreference != "" && preferences.QuotePreference != "auto" { if preferences.QuotePreference == "single" { return quotePreferenceSingle @@ -496,64 +476,6 @@ func isNonContextualKeyword(token ast.Kind) bool { return ast.IsKeywordKind(token) && !ast.IsContextualKeyword(token) } -func probablyUsesSemicolons(file *ast.SourceFile) bool { - withSemicolon := 0 - withoutSemicolon := 0 - nStatementsToObserve := 5 - - var visit func(node *ast.Node) bool - visit = func(node *ast.Node) bool { - if node.Flags&ast.NodeFlagsReparsed != 0 { - return false - } - if lsutil.SyntaxRequiresTrailingSemicolonOrASI(node.Kind) { - lastToken := lsutil.GetLastToken(node, file) - if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken { - withSemicolon++ - } else { - withoutSemicolon++ - } - } else if lsutil.SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) { - lastToken := lsutil.GetLastToken(node, file) - if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken { - withSemicolon++ - } else if lastToken != nil && lastToken.Kind != ast.KindCommaToken { - lastTokenLine, _ := scanner.GetECMALineAndCharacterOfPosition( - file, - astnav.GetStartOfNode(lastToken, file, false /*includeJSDoc*/)) - nextTokenLine, _ := scanner.GetECMALineAndCharacterOfPosition( - file, - scanner.GetRangeOfTokenAtPosition(file, lastToken.End()).Pos()) - // Avoid counting missing semicolon in single-line objects: - // `function f(p: { x: string /*no semicolon here is insignificant*/ }) {` - if lastTokenLine != nextTokenLine { - withoutSemicolon++ - } - } - } - - if withSemicolon+withoutSemicolon >= nStatementsToObserve { - return true - } - - return node.ForEachChild(visit) - } - - file.ForEachChild(visit) - - // One statement missing a semicolon isn't sufficient evidence to say the user - // doesn't want semicolons, because they may not even be done writing that statement. - if withSemicolon == 0 && withoutSemicolon <= 1 { - return true - } - - // If even 2/5 places have a semicolon, the user probably wants semicolons - if withoutSemicolon == 0 { - return true - } - return withSemicolon/withoutSemicolon > 1/nStatementsToObserve -} - var typeKeywords *collections.Set[ast.Kind] = collections.NewSetFromItems( ast.KindAnyKeyword, ast.KindAssertsKeyword, diff --git a/internal/lsp/lsproto/util.go b/internal/lsp/lsproto/util.go new file mode 100644 index 0000000000..917d9e18d7 --- /dev/null +++ b/internal/lsp/lsproto/util.go @@ -0,0 +1,25 @@ +package lsproto + +import ( + "cmp" +) + +// Implements a cmp.Compare like function for two Position +// ComparePositions(pos, other) == cmp.Compare(pos, other) +func ComparePositions(pos, other Position) int { + if lineComp := cmp.Compare(pos.Line, other.Line); lineComp != 0 { + return lineComp + } + return cmp.Compare(pos.Character, other.Character) +} + +// Implements a cmp.Compare like function for two *Range +// CompareRanges(lsRange, other) == cmp.Compare(lsrange, other) +// +// Range.Start is compared before Range.End +func CompareRanges(lsRange, other *Range) int { + if startComp := ComparePositions(lsRange.Start, other.Start); startComp != 0 { + return startComp + } + return ComparePositions(lsRange.End, other.End) +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 275e1c92df..d5a2aca5a3 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -15,6 +15,8 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsconv" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/project/ata" @@ -218,7 +220,7 @@ func (s *Server) RefreshDiagnostics(ctx context.Context) error { return nil } -func (s *Server) RequestConfiguration(ctx context.Context) (*ls.UserPreferences, error) { +func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserPreferences, error) { if s.initializeParams.Capabilities == nil || s.initializeParams.Capabilities.Workspace == nil || !ptrIsTrue(s.initializeParams.Capabilities.Workspace.Configuration) { // if no configuration request capapbility, return default preferences @@ -848,7 +850,7 @@ func (s *Server) handleCompletionItemResolve(ctx context.Context, params *lsprot if err != nil { return nil, err } - languageService, err := s.session.GetLanguageService(ctx, ls.FileNameToDocumentURI(data.FileName)) + languageService, err := s.session.GetLanguageService(ctx, lsconv.FileNameToDocumentURI(data.FileName)) if err != nil { return nil, err } diff --git a/internal/project/overlayfs.go b/internal/project/overlayfs.go index 26c6e2aa68..a42ca8f46e 100644 --- a/internal/project/overlayfs.go +++ b/internal/project/overlayfs.go @@ -6,7 +6,7 @@ import ( "sync" "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/sourcemap" "github.com/microsoft/typescript-go/internal/tspath" @@ -25,7 +25,7 @@ type FileHandle interface { Version() int32 MatchesDiskText() bool IsOverlay() bool - LSPLineMap() *ls.LSPLineMap + LSPLineMap() *lsconv.LSPLineMap ECMALineInfo() *sourcemap.ECMALineInfo Kind() core.ScriptKind } @@ -36,7 +36,7 @@ type fileBase struct { hash xxh3.Uint128 lineMapOnce sync.Once - lineMap *ls.LSPLineMap + lineMap *lsconv.LSPLineMap lineInfoOnce sync.Once lineInfo *sourcemap.ECMALineInfo } @@ -53,9 +53,9 @@ func (f *fileBase) Content() string { return f.content } -func (f *fileBase) LSPLineMap() *ls.LSPLineMap { +func (f *fileBase) LSPLineMap() *lsconv.LSPLineMap { f.lineMapOnce.Do(func() { - f.lineMap = ls.ComputeLSPLineStarts(f.content) + f.lineMap = lsconv.ComputeLSPLineStarts(f.content) }) return f.lineMap } @@ -304,7 +304,7 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma uri.FileName(), events.openChange.Content, events.openChange.Version, - ls.LanguageKindToScriptKind(events.openChange.LanguageKind), + lsconv.LanguageKindToScriptKind(events.openChange.LanguageKind), ) continue } @@ -335,7 +335,7 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma panic("overlay not found for changed file: " + uri) } for _, change := range events.changes { - converters := ls.NewConverters(fs.positionEncoding, func(fileName string) *ls.LSPLineMap { + converters := lsconv.NewConverters(fs.positionEncoding, func(fileName string) *lsconv.LSPLineMap { return o.LSPLineMap() }) for _, textChange := range change.Changes { diff --git a/internal/project/session.go b/internal/project/session.go index ce00336111..9e5a17aac6 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -14,6 +14,7 @@ import ( "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/project/background" @@ -80,8 +81,8 @@ type Session struct { programCounter *programCounter // read-only after initialization - initialPreferences *ls.UserPreferences - userPreferences *ls.UserPreferences // !!! update to Config + initialPreferences *lsutil.UserPreferences + userPreferences *lsutil.UserPreferences // !!! update to Config compilerOptionsForInferredProjects *core.CompilerOptions typingsInstaller *ata.TypingsInstaller backgroundQueue *background.Queue @@ -185,14 +186,14 @@ func (s *Session) GetCurrentDirectory() string { } // Gets current UserPreferences, always a copy -func (s *Session) UserPreferences() *ls.UserPreferences { +func (s *Session) UserPreferences() *lsutil.UserPreferences { s.configRWMu.Lock() defer s.configRWMu.Unlock() return s.userPreferences.Copy() } // Gets original UserPreferences of the session -func (s *Session) NewUserPreferences() *ls.UserPreferences { +func (s *Session) NewUserPreferences() *lsutil.UserPreferences { return s.initialPreferences.CopyOrDefault() } @@ -201,14 +202,14 @@ func (s *Session) Trace(msg string) { panic("ATA module resolution should not use tracing") } -func (s *Session) Configure(userPreferences *ls.UserPreferences) { +func (s *Session) Configure(userPreferences *lsutil.UserPreferences) { s.configRWMu.Lock() defer s.configRWMu.Unlock() s.pendingConfigChanges = true s.userPreferences = userPreferences } -func (s *Session) InitializeWithConfig(userPreferences *ls.UserPreferences) { +func (s *Session) InitializeWithConfig(userPreferences *lsutil.UserPreferences) { s.initialPreferences = userPreferences.CopyOrDefault() s.Configure(s.initialPreferences) } diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 9c90c21455..29f3c374f2 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -9,7 +9,8 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/format" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsconv" + "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/project/dirty" @@ -27,7 +28,7 @@ type Snapshot struct { // so can be a pointer. sessionOptions *SessionOptions toPath func(fileName string) tspath.Path - converters *ls.Converters + converters *lsconv.Converters // Immutable state, cloned between snapshots fs *snapshotFS @@ -64,7 +65,7 @@ func NewSnapshot( compilerOptionsForInferredProjects: compilerOptionsForInferredProjects, config: config, } - s.converters = ls.NewConverters(s.sessionOptions.PositionEncoding, s.LSPLineMap) + s.converters = lsconv.NewConverters(s.sessionOptions.PositionEncoding, s.LSPLineMap) s.refCount.Store(1) return s } @@ -79,7 +80,7 @@ func (s *Snapshot) GetFile(fileName string) FileHandle { return s.fs.GetFile(fileName) } -func (s *Snapshot) LSPLineMap(fileName string) *ls.LSPLineMap { +func (s *Snapshot) LSPLineMap(fileName string) *lsconv.LSPLineMap { if file := s.fs.GetFile(fileName); file != nil { return file.LSPLineMap() } @@ -93,7 +94,7 @@ func (s *Snapshot) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo { return nil } -func (s *Snapshot) UserPreferences() *ls.UserPreferences { +func (s *Snapshot) UserPreferences() *lsutil.UserPreferences { return s.config.tsUserPreferences } @@ -101,7 +102,7 @@ func (s *Snapshot) FormatOptions() *format.FormatCodeSettings { return s.config.formatOptions } -func (s *Snapshot) Converters() *ls.Converters { +func (s *Snapshot) Converters() *lsconv.Converters { return s.converters } @@ -145,8 +146,8 @@ type SnapshotChange struct { } type Config struct { - tsUserPreferences *ls.UserPreferences - // jsUserPreferences *ls.UserPreferences + tsUserPreferences *lsutil.UserPreferences + // jsUserPreferences *lsutil.UserPreferences formatOptions *format.FormatCodeSettings // tsserverOptions } diff --git a/internal/project/untitled_test.go b/internal/project/untitled_test.go index 54d3cfa763..0291394040 100644 --- a/internal/project/untitled_test.go +++ b/internal/project/untitled_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/microsoft/typescript-go/internal/bundled" - "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" "gotest.tools/v3/assert" @@ -23,7 +23,7 @@ func TestUntitledReferences(t *testing.T) { convertedFileName := untitledURI.FileName() t.Logf("URI '%s' converts to filename '%s'", untitledURI, convertedFileName) - backToURI := ls.FileNameToDocumentURI(convertedFileName) + backToURI := lsconv.FileNameToDocumentURI(convertedFileName) t.Logf("Filename '%s' converts back to URI '%s'", convertedFileName, backToURI) if string(backToURI) != string(untitledURI) { diff --git a/internal/tspath/path.go b/internal/tspath/path.go index fae3423721..de09221507 100644 --- a/internal/tspath/path.go +++ b/internal/tspath/path.go @@ -1127,3 +1127,7 @@ func getCommonParentsWorker(componentGroups [][]string, minComponents int, optio return [][]string{componentGroups[0][:maxDepth]} } + +func CompareNumberOfDirectorySeparators(path1, path2 string) int { + return cmp.Compare(strings.Count(path1, "/"), strings.Count(path2, "/")) +}