From 8c0cc7bd2a61c9227a7dc30c1ea184439b424125 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Wed, 27 May 2026 18:03:56 -0700 Subject: [PATCH 1/6] Add failing test case. --- ...tEditsForFileRenameSolutionNoCrash_test.go | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 internal/fourslash/tests/getEditsForFileRenameSolutionNoCrash_test.go diff --git a/internal/fourslash/tests/getEditsForFileRenameSolutionNoCrash_test.go b/internal/fourslash/tests/getEditsForFileRenameSolutionNoCrash_test.go new file mode 100644 index 00000000000..8dcb429f011 --- /dev/null +++ b/internal/fourslash/tests/getEditsForFileRenameSolutionNoCrash_test.go @@ -0,0 +1,48 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetEditsForFileRenameSolutionNoCrash(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + // The parent-directory solution tsconfig only references the composite child + // project, so when the child file is opened the solution is created as an + // ancestor project without ever building its program (it stays nil). Renaming + // a file in the child project must not crash when iterating that nil-program + // solution project. + const content = ` +// @Filename: /tsconfig.json +{ + "files": [], + "references": [ + { "path": "./src/tsconfig.json" } + ] +} + +// @Filename: /src/tsconfig.json +{ + "compilerOptions": { + "composite": true + }, + "files": ["./a.ts", "./b.ts"] +} + +// @Filename: /src/a.ts +import { b } from "./b"; +b; + +// @Filename: /src/b.ts +export const b = 0;` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyWillRenameFilesEdits(t, "/src/b.ts", "/src/c.ts", map[string]string{ + "/src/a.ts": `import { b } from "./c"; +b; +`, + }, nil /*preferences*/) +} From 70187372a702bb8d3722fc7abf2fa84515660675 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Wed, 27 May 2026 18:07:40 -0700 Subject: [PATCH 2/6] Guard on empty program. --- internal/ls/file_rename.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/ls/file_rename.go b/internal/ls/file_rename.go index cd36bb074fb..ff56653e485 100644 --- a/internal/ls/file_rename.go +++ b/internal/ls/file_rename.go @@ -26,6 +26,9 @@ type toImport struct { func (l *LanguageService) GetEditsForFileRename(ctx context.Context, oldURI lsproto.DocumentUri, newURI lsproto.DocumentUri) []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile { program := l.GetProgram() + if program == nil { + return nil + } oldPath := oldURI.FileName() newPath := newURI.FileName() From d35f5d4cb6c4f3db33f1d01882cb4571fad9eb76 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Wed, 27 May 2026 18:11:50 -0700 Subject: [PATCH 3/6] Avoid "NoCrash" as part of the test name. --- ...t.go => getEditsForFileRenameWithSolutionConfigFile_test.go} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename internal/fourslash/tests/{getEditsForFileRenameSolutionNoCrash_test.go => getEditsForFileRenameWithSolutionConfigFile_test.go} (94%) diff --git a/internal/fourslash/tests/getEditsForFileRenameSolutionNoCrash_test.go b/internal/fourslash/tests/getEditsForFileRenameWithSolutionConfigFile_test.go similarity index 94% rename from internal/fourslash/tests/getEditsForFileRenameSolutionNoCrash_test.go rename to internal/fourslash/tests/getEditsForFileRenameWithSolutionConfigFile_test.go index 8dcb429f011..85baaf9c7c6 100644 --- a/internal/fourslash/tests/getEditsForFileRenameSolutionNoCrash_test.go +++ b/internal/fourslash/tests/getEditsForFileRenameWithSolutionConfigFile_test.go @@ -7,7 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/testutil" ) -func TestGetEditsForFileRenameSolutionNoCrash(t *testing.T) { +func TestGetEditsForFileRenameWithSolutionConfigFile(t *testing.T) { t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") // The parent-directory solution tsconfig only references the composite child From 8f42192593eff1852d949bad49b35b9b2c73c12f Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Thu, 28 May 2026 14:01:37 -0700 Subject: [PATCH 4/6] Attach `*CommandLines` to `LanguageService` instances and use those in case `program` is `nil`. --- internal/ls/autoimport/fix.go | 2 +- internal/ls/autoimport/import_adder.go | 2 +- internal/ls/change/tracker.go | 3 +-- ...s_fixclassincorrectlyimplementsinterface.go | 4 ++-- .../ls/codeactions_fixmissingtypeannotation.go | 4 ++-- internal/ls/file_rename.go | 18 ++++++++++-------- internal/ls/languageservice.go | 4 ++++ internal/ls/organizeimports.go | 2 +- internal/project/session.go | 10 +++++----- 9 files changed, 27 insertions(+), 22 deletions(-) diff --git a/internal/ls/autoimport/fix.go b/internal/ls/autoimport/fix.go index 4103cdc72b9..bedbd7b5585 100644 --- a/internal/ls/autoimport/fix.go +++ b/internal/ls/autoimport/fix.go @@ -59,7 +59,7 @@ func (f *Fix) Edits( preferences lsutil.UserPreferences, ) ([]*lsproto.TextEdit, string) { locale := locale.FromContext(ctx) - tracker := change.NewTracker(ctx, compilerOptions, formatOptions, converters) + tracker := change.NewTracker(ctx, compilerOptions.NewLine.GetNewLineCharacter(), formatOptions, converters) switch f.Kind { case lsproto.AutoImportFixKindUseNamespace: description := addNamespaceQualifier(f, tracker, file, locale) diff --git a/internal/ls/autoimport/import_adder.go b/internal/ls/autoimport/import_adder.go index ab0603d65be..4dd03d77212 100644 --- a/internal/ls/autoimport/import_adder.go +++ b/internal/ls/autoimport/import_adder.go @@ -117,7 +117,7 @@ func (adder *importAdder) AddImportFromExportedSymbol(exportedSymbol *ast.Symbol func (adder *importAdder) Edits() []*lsproto.TextEdit { // !!! organize imports? - tracker := change.NewTracker(adder.ctx, adder.view.program.Options(), adder.formatOptions, adder.converters) + tracker := change.NewTracker(adder.ctx, adder.view.program.Options().NewLine.GetNewLineCharacter(), adder.formatOptions, adder.converters) quotePreference := lsutil.GetQuotePreference(adder.view.importingFile, adder.preferences) for _, fix := range adder.addToNamespace { addNamespaceQualifier(fix, tracker, adder.view.importingFile, locale.Default) diff --git a/internal/ls/change/tracker.go b/internal/ls/change/tracker.go index e4549087ae5..101821bc786 100644 --- a/internal/ls/change/tracker.go +++ b/internal/ls/change/tracker.go @@ -103,9 +103,8 @@ type deletedNode struct { node *ast.Node } -func NewTracker(ctx context.Context, compilerOptions *core.CompilerOptions, formatOptions lsutil.FormatCodeSettings, converters *lsconv.Converters) *Tracker { +func NewTracker(ctx context.Context, newLine string, formatOptions lsutil.FormatCodeSettings, converters *lsconv.Converters) *Tracker { emitContext := printer.NewEmitContext() - newLine := compilerOptions.NewLine.GetNewLineCharacter() ctx = format.WithFormatCodeSettings(ctx, formatOptions, newLine) // !!! formatSettings in context? return &Tracker{ EmitContext: emitContext, diff --git a/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go b/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go index 756b1b340a0..a6c669695b7 100644 --- a/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go +++ b/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go @@ -44,7 +44,7 @@ func getCodeActionsToFixClassIncorrectlyImplementsInterface(context context.Cont var actions []*CodeAction for _, implementedTypeNode := range implementsTypes { - changeTracker := change.NewTracker(context, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + changeTracker := change.NewTracker(context, fixContext.Program.Options().NewLine.GetNewLineCharacter(), fixContext.LS.FormatOptions(), fixContext.LS.converters) importAdder, err := createImportAdder(context, fixContext, typeChecker) if err != nil { return nil, err @@ -70,7 +70,7 @@ func getAllCodeActionsToFixClassIncorrectlyImplementsInterface(context context.C typeChecker, done := fixContext.Program.GetTypeCheckerForFile(context, fixContext.SourceFile) defer done() - changeTracker := change.NewTracker(context, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + changeTracker := change.NewTracker(context, fixContext.Program.Options().NewLine.GetNewLineCharacter(), fixContext.LS.FormatOptions(), fixContext.LS.converters) importAdder, err := createImportAdder(context, fixContext, typeChecker) if err != nil { return nil, err diff --git a/internal/ls/codeactions_fixmissingtypeannotation.go b/internal/ls/codeactions_fixmissingtypeannotation.go index 0ea960949d8..f9b5381a971 100644 --- a/internal/ls/codeactions_fixmissingtypeannotation.go +++ b/internal/ls/codeactions_fixmissingtypeannotation.go @@ -136,7 +136,7 @@ func getAllIsolatedDeclarationsCodeActions(ctx context.Context, fixContext *Code ch, done := fixContext.Program.GetTypeCheckerForFile(ctx, fixContext.SourceFile) defer done() - changeTracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + changeTracker := change.NewTracker(ctx, fixContext.Program.Options().NewLine.GetNewLineCharacter(), fixContext.LS.FormatOptions(), fixContext.LS.converters) fixer := &isolatedDeclarationsFixer{ sourceFile: fixContext.SourceFile, @@ -173,7 +173,7 @@ func getAllIsolatedDeclarationsCodeActions(ctx context.Context, fixContext *Code } func tryCodeAction(ctx context.Context, fixContext *CodeFixContext, ch *checker.Checker, fn func(*isolatedDeclarationsFixer) string) *CodeAction { - changeTracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + changeTracker := change.NewTracker(ctx, fixContext.Program.Options().NewLine.GetNewLineCharacter(), fixContext.LS.FormatOptions(), fixContext.LS.converters) var importAdder autoimport.ImportAdder // importAdder may be nil if the auto-import registry is not available; diff --git a/internal/ls/file_rename.go b/internal/ls/file_rename.go index ff56653e485..7e3f1dda85d 100644 --- a/internal/ls/file_rename.go +++ b/internal/ls/file_rename.go @@ -26,17 +26,20 @@ type toImport struct { func (l *LanguageService) GetEditsForFileRename(ctx context.Context, oldURI lsproto.DocumentUri, newURI lsproto.DocumentUri) []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile { program := l.GetProgram() - if program == nil { - return nil - } oldPath := oldURI.FileName() newPath := newURI.FileName() oldToNew := l.createPathUpdater(oldPath, newPath) - changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters) - l.updateTsconfigFiles(program, changeTracker, oldToNew, oldPath, newPath) - l.updateImportsForFileRename(program, changeTracker, oldToNew) + newLine := "\n" + if program != nil { + newLine = program.Options().NewLine.GetNewLineCharacter() + } + changeTracker := change.NewTracker(ctx, newLine, l.FormatOptions(), l.converters) + l.updateTsconfigFiles(l.commandLine, changeTracker, oldToNew, oldPath, newPath) + if program != nil { + l.updateImportsForFileRename(program, changeTracker, oldToNew) + } var documentChanges []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile @@ -94,8 +97,7 @@ func (l *LanguageService) createPathUpdater(oldPath string, newPath string) path } } -func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeTracker *change.Tracker, oldToNew pathUpdater, oldPath string, newPath string) { - commandLine := program.CommandLine() +func (l *LanguageService) updateTsconfigFiles(commandLine *tsoptions.ParsedCommandLine, changeTracker *change.Tracker, oldToNew pathUpdater, oldPath string, newPath string) { if commandLine == nil || commandLine.ConfigFile == nil { return } diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index 7c7e1aced75..4092a18c36f 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/sourcemap" + "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs/vfsmatch" ) @@ -17,6 +18,7 @@ type LanguageService struct { host Host activeConfig lsutil.UserPreferences program *compiler.Program + commandLine *tsoptions.ParsedCommandLine converters *lsconv.Converters documentPositionMappers map[string]*sourcemap.DocumentPositionMapper } @@ -24,6 +26,7 @@ type LanguageService struct { func NewLanguageService( projectPath tspath.Path, program *compiler.Program, + commandLine *tsoptions.ParsedCommandLine, host Host, activeFile string, ) *LanguageService { @@ -31,6 +34,7 @@ func NewLanguageService( projectPath: projectPath, host: host, program: program, + commandLine: commandLine, converters: host.Converters(), activeConfig: host.GetPreferences(activeFile), documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{}, diff --git a/internal/ls/organizeimports.go b/internal/ls/organizeimports.go index ea558136d66..745c8f650a9 100644 --- a/internal/ls/organizeimports.go +++ b/internal/ls/organizeimports.go @@ -27,7 +27,7 @@ func (l *LanguageService) OrganizeImports( program *compiler.Program, kind lsproto.CodeActionKind, ) map[string][]*lsproto.TextEdit { - changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters) + changeTracker := change.NewTracker(ctx, program.Options().NewLine.GetNewLineCharacter(), l.FormatOptions(), l.converters) shouldSort := kind == lsproto.CodeActionKindSourceSortImports || kind == lsproto.CodeActionKindSourceOrganizeImports shouldCombine := shouldSort shouldRemove := kind == lsproto.CodeActionKindSourceRemoveUnusedImports || kind == lsproto.CodeActionKindSourceOrganizeImports diff --git a/internal/project/session.go b/internal/project/session.go index 7c9d250f81e..4535b0fb0ba 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -977,7 +977,7 @@ func (s *Session) getSnapshotAndDefaultProject(ctx context.Context, uri lsproto. if project == nil { return nil, nil, nil, fmt.Errorf("no project found for URI %s", uri) } - return snapshot, project, ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName()), nil + return snapshot, project, ls.NewLanguageService(project.configFilePath, project.GetProgram(), project.CommandLine, snapshot, uri.FileName()), nil } func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUri) (*ls.LanguageService, error) { @@ -1025,7 +1025,7 @@ func (s *Session) GetLanguageServicesForDocuments(ctx context.Context, uris []ls projects := snapshot.ProjectCollection.Projects() services := make([]*ls.LanguageService, 0, len(projects)) for _, project := range projects { - services = append(services, ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, activeFile)) + services = append(services, ls.NewLanguageService(project.configFilePath, project.GetProgram(), project.CommandLine, snapshot, activeFile)) } return services } @@ -1045,7 +1045,7 @@ func (s *Session) GetLanguageServiceForProjectWithFile(ctx context.Context, proj if !project.HasFile(uri.FileName()) { return nil } - return ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName()) + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), project.CommandLine, snapshot, uri.FileName()) } // WithSnapshotLoadingProjectTree acquires a ref'd snapshot with the @@ -1079,7 +1079,7 @@ func (s *Session) GetCurrentLanguageServiceWithAutoImports(ctx context.Context, if project == nil { return nil, fmt.Errorf("no project found for URI %s", uri) } - return ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName()), nil + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), project.CommandLine, snapshot, uri.FileName()), nil } // WithLanguageServiceAndSnapshot synchronously acquires a ref'd snapshot and @@ -1140,7 +1140,7 @@ func (s *Session) GetLanguageServiceWithAutoImports(ctx context.Context, baseSna s.adoptSnapshotChange(baseSnapshot, newSnapshot) }) - return ls.NewLanguageService(project.configFilePath, project.GetProgram(), newSnapshot, uri.FileName()), nil + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), project.CommandLine, newSnapshot, uri.FileName()), nil } // adoptSnapshotChange promotes a cloned snapshot as the session's current From 3652fbe91b71527af98f52f4e854c865003b3fd3 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Fri, 29 May 2026 15:13:41 -0700 Subject: [PATCH 5/6] Revert "Attach `*CommandLines` to `LanguageService` instances and use those in case `program` is `nil`." This reverts commit 8f42192593eff1852d949bad49b35b9b2c73c12f. --- internal/ls/autoimport/fix.go | 2 +- internal/ls/autoimport/import_adder.go | 2 +- internal/ls/change/tracker.go | 3 ++- ...s_fixclassincorrectlyimplementsinterface.go | 4 ++-- .../ls/codeactions_fixmissingtypeannotation.go | 4 ++-- internal/ls/file_rename.go | 18 ++++++++---------- internal/ls/languageservice.go | 4 ---- internal/ls/organizeimports.go | 2 +- internal/project/session.go | 10 +++++----- 9 files changed, 22 insertions(+), 27 deletions(-) diff --git a/internal/ls/autoimport/fix.go b/internal/ls/autoimport/fix.go index bedbd7b5585..4103cdc72b9 100644 --- a/internal/ls/autoimport/fix.go +++ b/internal/ls/autoimport/fix.go @@ -59,7 +59,7 @@ func (f *Fix) Edits( preferences lsutil.UserPreferences, ) ([]*lsproto.TextEdit, string) { locale := locale.FromContext(ctx) - tracker := change.NewTracker(ctx, compilerOptions.NewLine.GetNewLineCharacter(), formatOptions, converters) + tracker := change.NewTracker(ctx, compilerOptions, formatOptions, converters) switch f.Kind { case lsproto.AutoImportFixKindUseNamespace: description := addNamespaceQualifier(f, tracker, file, locale) diff --git a/internal/ls/autoimport/import_adder.go b/internal/ls/autoimport/import_adder.go index 4dd03d77212..ab0603d65be 100644 --- a/internal/ls/autoimport/import_adder.go +++ b/internal/ls/autoimport/import_adder.go @@ -117,7 +117,7 @@ func (adder *importAdder) AddImportFromExportedSymbol(exportedSymbol *ast.Symbol func (adder *importAdder) Edits() []*lsproto.TextEdit { // !!! organize imports? - tracker := change.NewTracker(adder.ctx, adder.view.program.Options().NewLine.GetNewLineCharacter(), adder.formatOptions, adder.converters) + tracker := change.NewTracker(adder.ctx, adder.view.program.Options(), adder.formatOptions, adder.converters) quotePreference := lsutil.GetQuotePreference(adder.view.importingFile, adder.preferences) for _, fix := range adder.addToNamespace { addNamespaceQualifier(fix, tracker, adder.view.importingFile, locale.Default) diff --git a/internal/ls/change/tracker.go b/internal/ls/change/tracker.go index 101821bc786..e4549087ae5 100644 --- a/internal/ls/change/tracker.go +++ b/internal/ls/change/tracker.go @@ -103,8 +103,9 @@ type deletedNode struct { node *ast.Node } -func NewTracker(ctx context.Context, newLine string, formatOptions lsutil.FormatCodeSettings, converters *lsconv.Converters) *Tracker { +func NewTracker(ctx context.Context, compilerOptions *core.CompilerOptions, formatOptions lsutil.FormatCodeSettings, converters *lsconv.Converters) *Tracker { emitContext := printer.NewEmitContext() + newLine := compilerOptions.NewLine.GetNewLineCharacter() ctx = format.WithFormatCodeSettings(ctx, formatOptions, newLine) // !!! formatSettings in context? return &Tracker{ EmitContext: emitContext, diff --git a/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go b/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go index a6c669695b7..756b1b340a0 100644 --- a/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go +++ b/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go @@ -44,7 +44,7 @@ func getCodeActionsToFixClassIncorrectlyImplementsInterface(context context.Cont var actions []*CodeAction for _, implementedTypeNode := range implementsTypes { - changeTracker := change.NewTracker(context, fixContext.Program.Options().NewLine.GetNewLineCharacter(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + changeTracker := change.NewTracker(context, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) importAdder, err := createImportAdder(context, fixContext, typeChecker) if err != nil { return nil, err @@ -70,7 +70,7 @@ func getAllCodeActionsToFixClassIncorrectlyImplementsInterface(context context.C typeChecker, done := fixContext.Program.GetTypeCheckerForFile(context, fixContext.SourceFile) defer done() - changeTracker := change.NewTracker(context, fixContext.Program.Options().NewLine.GetNewLineCharacter(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + changeTracker := change.NewTracker(context, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) importAdder, err := createImportAdder(context, fixContext, typeChecker) if err != nil { return nil, err diff --git a/internal/ls/codeactions_fixmissingtypeannotation.go b/internal/ls/codeactions_fixmissingtypeannotation.go index f9b5381a971..0ea960949d8 100644 --- a/internal/ls/codeactions_fixmissingtypeannotation.go +++ b/internal/ls/codeactions_fixmissingtypeannotation.go @@ -136,7 +136,7 @@ func getAllIsolatedDeclarationsCodeActions(ctx context.Context, fixContext *Code ch, done := fixContext.Program.GetTypeCheckerForFile(ctx, fixContext.SourceFile) defer done() - changeTracker := change.NewTracker(ctx, fixContext.Program.Options().NewLine.GetNewLineCharacter(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + changeTracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) fixer := &isolatedDeclarationsFixer{ sourceFile: fixContext.SourceFile, @@ -173,7 +173,7 @@ func getAllIsolatedDeclarationsCodeActions(ctx context.Context, fixContext *Code } func tryCodeAction(ctx context.Context, fixContext *CodeFixContext, ch *checker.Checker, fn func(*isolatedDeclarationsFixer) string) *CodeAction { - changeTracker := change.NewTracker(ctx, fixContext.Program.Options().NewLine.GetNewLineCharacter(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + changeTracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) var importAdder autoimport.ImportAdder // importAdder may be nil if the auto-import registry is not available; diff --git a/internal/ls/file_rename.go b/internal/ls/file_rename.go index 7e3f1dda85d..ff56653e485 100644 --- a/internal/ls/file_rename.go +++ b/internal/ls/file_rename.go @@ -26,20 +26,17 @@ type toImport struct { func (l *LanguageService) GetEditsForFileRename(ctx context.Context, oldURI lsproto.DocumentUri, newURI lsproto.DocumentUri) []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile { program := l.GetProgram() + if program == nil { + return nil + } oldPath := oldURI.FileName() newPath := newURI.FileName() oldToNew := l.createPathUpdater(oldPath, newPath) - newLine := "\n" - if program != nil { - newLine = program.Options().NewLine.GetNewLineCharacter() - } - changeTracker := change.NewTracker(ctx, newLine, l.FormatOptions(), l.converters) - l.updateTsconfigFiles(l.commandLine, changeTracker, oldToNew, oldPath, newPath) - if program != nil { - l.updateImportsForFileRename(program, changeTracker, oldToNew) - } + changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters) + l.updateTsconfigFiles(program, changeTracker, oldToNew, oldPath, newPath) + l.updateImportsForFileRename(program, changeTracker, oldToNew) var documentChanges []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile @@ -97,7 +94,8 @@ func (l *LanguageService) createPathUpdater(oldPath string, newPath string) path } } -func (l *LanguageService) updateTsconfigFiles(commandLine *tsoptions.ParsedCommandLine, changeTracker *change.Tracker, oldToNew pathUpdater, oldPath string, newPath string) { +func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeTracker *change.Tracker, oldToNew pathUpdater, oldPath string, newPath string) { + commandLine := program.CommandLine() if commandLine == nil || commandLine.ConfigFile == nil { return } diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index 4092a18c36f..7c7e1aced75 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -8,7 +8,6 @@ import ( "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/sourcemap" - "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs/vfsmatch" ) @@ -18,7 +17,6 @@ type LanguageService struct { host Host activeConfig lsutil.UserPreferences program *compiler.Program - commandLine *tsoptions.ParsedCommandLine converters *lsconv.Converters documentPositionMappers map[string]*sourcemap.DocumentPositionMapper } @@ -26,7 +24,6 @@ type LanguageService struct { func NewLanguageService( projectPath tspath.Path, program *compiler.Program, - commandLine *tsoptions.ParsedCommandLine, host Host, activeFile string, ) *LanguageService { @@ -34,7 +31,6 @@ func NewLanguageService( projectPath: projectPath, host: host, program: program, - commandLine: commandLine, converters: host.Converters(), activeConfig: host.GetPreferences(activeFile), documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{}, diff --git a/internal/ls/organizeimports.go b/internal/ls/organizeimports.go index 745c8f650a9..ea558136d66 100644 --- a/internal/ls/organizeimports.go +++ b/internal/ls/organizeimports.go @@ -27,7 +27,7 @@ func (l *LanguageService) OrganizeImports( program *compiler.Program, kind lsproto.CodeActionKind, ) map[string][]*lsproto.TextEdit { - changeTracker := change.NewTracker(ctx, program.Options().NewLine.GetNewLineCharacter(), l.FormatOptions(), l.converters) + changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters) shouldSort := kind == lsproto.CodeActionKindSourceSortImports || kind == lsproto.CodeActionKindSourceOrganizeImports shouldCombine := shouldSort shouldRemove := kind == lsproto.CodeActionKindSourceRemoveUnusedImports || kind == lsproto.CodeActionKindSourceOrganizeImports diff --git a/internal/project/session.go b/internal/project/session.go index 4535b0fb0ba..7c9d250f81e 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -977,7 +977,7 @@ func (s *Session) getSnapshotAndDefaultProject(ctx context.Context, uri lsproto. if project == nil { return nil, nil, nil, fmt.Errorf("no project found for URI %s", uri) } - return snapshot, project, ls.NewLanguageService(project.configFilePath, project.GetProgram(), project.CommandLine, snapshot, uri.FileName()), nil + return snapshot, project, ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName()), nil } func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUri) (*ls.LanguageService, error) { @@ -1025,7 +1025,7 @@ func (s *Session) GetLanguageServicesForDocuments(ctx context.Context, uris []ls projects := snapshot.ProjectCollection.Projects() services := make([]*ls.LanguageService, 0, len(projects)) for _, project := range projects { - services = append(services, ls.NewLanguageService(project.configFilePath, project.GetProgram(), project.CommandLine, snapshot, activeFile)) + services = append(services, ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, activeFile)) } return services } @@ -1045,7 +1045,7 @@ func (s *Session) GetLanguageServiceForProjectWithFile(ctx context.Context, proj if !project.HasFile(uri.FileName()) { return nil } - return ls.NewLanguageService(project.configFilePath, project.GetProgram(), project.CommandLine, snapshot, uri.FileName()) + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName()) } // WithSnapshotLoadingProjectTree acquires a ref'd snapshot with the @@ -1079,7 +1079,7 @@ func (s *Session) GetCurrentLanguageServiceWithAutoImports(ctx context.Context, if project == nil { return nil, fmt.Errorf("no project found for URI %s", uri) } - return ls.NewLanguageService(project.configFilePath, project.GetProgram(), project.CommandLine, snapshot, uri.FileName()), nil + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, uri.FileName()), nil } // WithLanguageServiceAndSnapshot synchronously acquires a ref'd snapshot and @@ -1140,7 +1140,7 @@ func (s *Session) GetLanguageServiceWithAutoImports(ctx context.Context, baseSna s.adoptSnapshotChange(baseSnapshot, newSnapshot) }) - return ls.NewLanguageService(project.configFilePath, project.GetProgram(), project.CommandLine, newSnapshot, uri.FileName()), nil + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), newSnapshot, uri.FileName()), nil } // adoptSnapshotChange promotes a cloned snapshot as the session's current From 8e4be4041176a471ac05ef898173dd15b44074bc Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Fri, 5 Jun 2026 12:14:51 -0700 Subject: [PATCH 6/6] Don't create `LanguageService`s without programs. --- internal/ls/file_rename.go | 3 --- internal/project/session.go | 7 ++++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/ls/file_rename.go b/internal/ls/file_rename.go index ff56653e485..cd36bb074fb 100644 --- a/internal/ls/file_rename.go +++ b/internal/ls/file_rename.go @@ -26,9 +26,6 @@ type toImport struct { func (l *LanguageService) GetEditsForFileRename(ctx context.Context, oldURI lsproto.DocumentUri, newURI lsproto.DocumentUri) []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile { program := l.GetProgram() - if program == nil { - return nil - } oldPath := oldURI.FileName() newPath := newURI.FileName() diff --git a/internal/project/session.go b/internal/project/session.go index 7c9d250f81e..bd037b1a051 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -1025,7 +1025,12 @@ func (s *Session) GetLanguageServicesForDocuments(ctx context.Context, uris []ls projects := snapshot.ProjectCollection.Projects() services := make([]*ls.LanguageService, 0, len(projects)) for _, project := range projects { - services = append(services, ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot, activeFile)) + program := project.GetProgram() + if program == nil { + continue + } + + services = append(services, ls.NewLanguageService(project.configFilePath, program, snapshot, activeFile)) } return services }