Implement LSP-based code edits with file renaming#3358
Implement LSP-based code edits with file renaming#3358Andarist wants to merge 22 commits intomicrosoft:mainfrom
Conversation
6810db7 to
7615f8a
Compare
| exhaustiveCaseCompletions7 | ||
| exhaustiveCaseCompletions8 | ||
| formatOnEnterInComment | ||
| getEditsForFileRename_caseInsensitive |
There was a problem hiding this comment.
Some tests are manual because we needed to specify case insensitivity, but most are because I changed fourslash such that the expected file content map uses file names after the rename.
|
|
||
| if target == nil { | ||
| // First fall back: try every file in the program to see if any of them would match the import specifier, and if so, obtain the updated specifier for that file. | ||
| if updated := getUpdatedImportSpecifierFromMovedSourceFiles(program, sourceFile, importLiteral, oldToNew, newImportFromPath, userPreferences); updated != "" && updated != importLiteral.Text() { |
There was a problem hiding this comment.
This allows us to properly rename in some cases where there's a module resolution error and the import doesn't resolve to a renamed file because of the error. It's kinda inefficient though, so maybe we should just say if you have resolution errors, it's too bad, but I left it for now.
| }, true | ||
| } | ||
|
|
||
| func (l *LanguageService) getNewFileNameForModuleRename(oldPath, specifierText, newName string) string { |
There was a problem hiding this comment.
This used to be done in the built-in extension.
| return "" | ||
| } | ||
|
|
||
| func getDeclarationEmitExtensionForPath(fileName string) string { |
| } | ||
| } | ||
|
|
||
| //// [/myproject/dependency/FnS.ts] *modified* |
There was a problem hiding this comment.
These baselines changed basically because I made fourslash's editScript also update its vfs, to avoid inconsistencies between the vfs and fourslash's script infos.
There was a problem hiding this comment.
Pull request overview
This PR adds LSP-driven support for updating imports/configs during file (and directory) renames, including returning RenameFile document changes for module-specifier renames and computing additional workspace edits via workspace/willRenameFiles. It also extends declaration-extension handling to support arbitrary extensions (e.g. .d.css.ts) and expands fourslash tooling/tests to cover the new behavior.
Changes:
- Implement
workspace/willRenameFileshandling and integrate rename of module specifiers with file renames + follow-up edits. - Add
LanguageService.GetEditsForFileRenameto compute import/reference/tsconfig updates (plus companion renames like.d.css.ts->.css). - Update fourslash harness + converter tooling and add/convert tests for file-rename edit computation.
Reviewed changes
Copilot reviewed 64 out of 64 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| testdata/baselines/reference/fourslash/state/findAllRefsDoesNotTryToSearchProjectAfterItsUpdateDoesNotIncludeTheFile.baseline | Baseline update reflecting modified-file tracking. |
| testdata/baselines/reference/fourslash/state/declarationMapsRenameWithSourceMapsNotSolutionEditEnd.baseline | Baseline update for rename state output. |
| testdata/baselines/reference/fourslash/state/declarationMapsRenameWithSourceMapsNotSolutionEdit.baseline | Baseline update for rename state output. |
| testdata/baselines/reference/fourslash/state/declarationMapsRenameWithSourceMapsEditEnd.baseline | Baseline update for rename state output. |
| testdata/baselines/reference/fourslash/state/declarationMapsRenameWithSourceMapsEdit.baseline | Baseline update for rename state output. |
| testdata/baselines/reference/fourslash/state/declarationMapsRenameWithProjectReferencesEditEnd.baseline | Baseline update for rename state output. |
| testdata/baselines/reference/fourslash/state/declarationMapsRenameWithProjectReferencesEdit.baseline | Baseline update for rename state output. |
| testdata/baselines/reference/fourslash/state/declarationMapsRenameWithDisableSourceOfProjectReferenceRedirectEditEnd.baseline | Baseline update for rename state output. |
| testdata/baselines/reference/fourslash/state/declarationMapsRenameWithDisableSourceOfProjectReferenceRedirectEdit.baseline | Baseline update for rename state output. |
| internal/tspath/extension.go | Generalize .d.*.ts handling and declaration emit extension selection for arbitrary extensions. |
| internal/testutil/harnessutil/harnessutil.go | Export/expand test-config option parsing (with optional unknown-option allowance). |
| internal/project/snapshot.go | Expose Snapshot.FileExists for LS features needing snapshot FS existence checks. |
| internal/project/session.go | Add helper to construct language services for rename processing. |
| internal/outputpaths/outputpaths.go | Centralize declaration emit extension logic via tspath. |
| internal/modulespecifiers/specifiers.go | Introduce UpdateModuleSpecifier that accepts user preferences (for rename updates). |
| internal/lsp/server.go | Register/handle workspace/willRenameFiles and route textDocument/rename via server to support file renames. |
| internal/ls/rename.go | Extend rename info to include file rename intent; add client capability helpers; compute correct target file for specifier rename. |
| internal/ls/host.go | Add FileExists to LS host interface for rename companion file checks. |
| internal/ls/file_rename.go | New core implementation computing edits (imports/refs/tsconfig) and related file renames for file/directory rename. |
| internal/ls/crossproject.go | Combine rename responses including DocumentChanges (not just Changes). |
| internal/ls/codeactions_importfixes.go | Dedupe identical auto-import fix suggestions. |
| internal/ls/autoimport/specifiers.go | Adjust module specifier generation for symlinked package exports (realpath handling). |
| internal/fourslash/tests/renameImportSpecifierNoResourceOperations_test.go | New test ensuring rename fails when resource operations aren’t supported. |
| internal/fourslash/tests/manual/getEditsForFileRename_unaffectedNonRelativePath_test.go | Manual test for non-relative path stability under rename. |
| internal/fourslash/tests/manual/getEditsForFileRename_subDir_test.go | Manual test for moving file into subdir updates relative imports. |
| internal/fourslash/tests/manual/getEditsForFileRename_shortenRelativePaths_test.go | Manual test for shortening relative paths after rename. |
| internal/fourslash/tests/manual/getEditsForFileRename_resolveJsonModule_test.go | Manual test for JSON module import updates under rename. |
| internal/fourslash/tests/manual/getEditsForFileRename_preservePathEnding_test.go | Manual test preserving specifier endings like ., ./index, explicit extensions. |
| internal/fourslash/tests/manual/getEditsForFileRename_preferences_test.go | Manual test honoring module specifier + quote preferences. |
| internal/fourslash/tests/manual/getEditsForFileRename_jsExtension_test.go | Manual test for .js extension preservation/updates. |
| internal/fourslash/tests/manual/getEditsForFileRename_directory_up_test.go | Manual test for directory rename moving up the tree. |
| internal/fourslash/tests/manual/getEditsForFileRename_directory_down_test.go | Manual test for directory rename moving down the tree. |
| internal/fourslash/tests/manual/getEditsForFileRename_caseInsensitive_test.go | Manual test for case-insensitive FS behavior. |
| internal/fourslash/tests/getEditsForFileRename_jsRename_test.go | New rename test covering .js-style specifier rename -> file rename. |
| internal/fourslash/tests/getEditsForFileRename_cssImport4_test.go | New test intended to cover fallback behavior when file operations aren’t supported. |
| internal/fourslash/tests/getEditsForFileRename_cssImport3_test.go | New test for css import specifier rename behavior. |
| internal/fourslash/tests/getEditsForFileRename_cssImport2_test.go | New test for willRenameFiles edits when renaming .d.css.ts. |
| internal/fourslash/tests/gen/getEditsForFileRename_unresolvableNodeModule_test.go | Converted/generated coverage for rename edits. |
| internal/fourslash/tests/gen/getEditsForFileRename_unresolvableImport_test.go | Converted/generated coverage for rename edits. |
| internal/fourslash/tests/gen/getEditsForFileRename_tsconfig_test.go | Converted/generated coverage for tsconfig path updates. |
| internal/fourslash/tests/gen/getEditsForFileRename_tsconfig_include_noChange_test.go | Converted/generated coverage for include behavior. |
| internal/fourslash/tests/gen/getEditsForFileRename_tsconfig_include_add_test.go | Converted/generated coverage for include insertion behavior. |
| internal/fourslash/tests/gen/getEditsForFileRename_tsconfig_empty_include_test.go | Converted/generated coverage for empty include behavior. |
| internal/fourslash/tests/gen/getEditsForFileRename_test.go | Converted/generated base coverage for file rename edits. |
| internal/fourslash/tests/gen/getEditsForFileRename_symlink_test.go | Converted/generated coverage for symlink scenarios. |
| internal/fourslash/tests/gen/getEditsForFileRename_renameToIndex_test.go | Converted/generated coverage for renaming to index. |
| internal/fourslash/tests/gen/getEditsForFileRename_renameFromIndex_test.go | Converted/generated coverage for renaming from index. |
| internal/fourslash/tests/gen/getEditsForFileRename_notAffectedByJsFile_test.go | Converted/generated coverage for TS-vs-JS sibling file behavior. |
| internal/fourslash/tests/gen/getEditsForFileRename_nodeModuleDirectoryCase_test.go | Converted/generated coverage for node_modules dir casing rename. |
| internal/fourslash/tests/gen/getEditsForFileRename_keepFileExtensions_test.go | Converted/generated coverage for keeping .js in specifiers under Node16/Next modes. |
| internal/fourslash/tests/gen/getEditsForFileRename_js_simple_test.go | Converted/generated coverage for basic JS rename edits. |
| internal/fourslash/tests/gen/getEditsForFileRename_directory_test.go | Converted/generated coverage for directory rename edits. |
| internal/fourslash/tests/gen/getEditsForFileRename_directory_noUpdateNodeModulesImport_test.go | Converted/generated coverage ensuring node module imports aren’t rewritten. |
| internal/fourslash/tests/gen/getEditsForFileRename_casing_test.go | Converted/generated coverage for preserving path casing. |
| internal/fourslash/tests/gen/getEditsForFileRename_amd_test.go | Converted/generated coverage for classic/AMD-ish resolution scenario. |
| internal/fourslash/tests/gen/getEditsForFileRename_ambientModule_test.go | Converted/generated coverage ensuring ambient modules unaffected. |
| internal/fourslash/test_parser.go | Allow certain global directives alongside config files (e.g. symlink/useCaseSensitiveFileNames). |
| internal/fourslash/fourslash.go | Major harness updates: apply multi-edits safely, emulate rename + willRenameFiles flows, default capabilities updates. |
| internal/fourslash/_scripts/unparsedTests.txt | Remove now-supported getEditsForFileRename entries from “unparsed”. |
| internal/fourslash/_scripts/manualTests.txt | Add manual test names for conversion workflow. |
| internal/fourslash/_scripts/failingTests.txt | Update failing-test list after symlink handling changes. |
| internal/fourslash/_scripts/convertFourslash.mts | Add conversion support for verify.getEditsForFileRename. |
| internal/diagnostics/extraDiagnosticMessages.json | Add new editor-capability error message for file rename. |
| internal/diagnostics/diagnostics_generated.go | Regenerate diagnostics to include new message code. |
| if tspath.IsDeclarationFileName(oldPath) && tspath.IsDeclarationFileName(newPath) { | ||
| dtsExt := tspath.GetDeclarationFileExtension(oldPath) | ||
| originalExtensions := tspath.GetPossibleOriginalInputExtensionForExtension(dtsExt) | ||
| for _, ext := range originalExtensions { | ||
| oldOriginalPath := tspath.ChangeFullExtension(oldPath, ext) | ||
| if l.host.FileExists(oldOriginalPath) { | ||
| newDtsExt := tspath.GetDeclarationFileExtension(oldPath) | ||
| newOriginalExtensions := tspath.GetPossibleOriginalInputExtensionForExtension(newDtsExt) | ||
| if slices.Contains(newOriginalExtensions, ext) { | ||
| newOriginalPath := tspath.ChangeFullExtension(newPath, ext) |
There was a problem hiding this comment.
In the declaration-file rename companion logic, newDtsExt is derived from oldPath instead of newPath. This breaks the check when the declaration extension changes during rename (e.g. .d.css.ts -> .d.scss.ts), and can incorrectly skip/allow renaming the original file. Use tspath.GetDeclarationFileExtension(newPath) (and consider computing both old/new once outside the loop).
| func (s *Session) GetLanguageServicesForDocuments(ctx context.Context, uris []lsproto.DocumentUri) []*ls.LanguageService { | ||
| snapshot := s.getSnapshot( | ||
| ctx, | ||
| ResourceRequest{Documents: uris}, | ||
| false, /*callerRef*/ | ||
| ) | ||
|
|
||
| activeFile := "" | ||
| if len(uris) > 0 { | ||
| activeFile = uris[0].FileName() | ||
| } | ||
|
|
||
| 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)) | ||
| } | ||
| return services |
There was a problem hiding this comment.
GetLanguageServicesForDocuments currently creates a LanguageService for every project in the snapshot, even if none of the uris belong to most projects. In large workspaces this makes willRenameFiles scale with total project count. Consider filtering to only projects that contain any of the provided documents (e.g. via snapshot.GetProjectsContainingFile per URI, then dedupe) before constructing services.
There was a problem hiding this comment.
This sounds plausible but doesn't work, because we need to visit all projects that can depend on the project that contains the file.
Fixes #2244 and fuzzer crash from #3187 (comment).
The core implementation in
file_rename.gois mostly a translation from the Strada code, except I deleted some code that seemed to be trying to do the same thing in 3 different ways (resolve a module specifier).Notable changes include making file rename work when you rename a specifier for an arbitrary extension; see tests with names ending in
cssImport.I also removed Strada's source map supporting code because it didn't seem to work for file renames, only for directory renames, so I thought we might as well declare that scenario as out of scope (one shouldn't be renaming an output directory or output files...).
The overall flow is also new because of how LSP works: the server subscribes to receive
willRenameFilesrequests from the client for supported extensions. Then, if the user renames a file directly, the server will compute any additional edits and renames needed. If a user invokes regular rename on a module specifier, the server will reply with a file rename document change, and then the client should send awillRenameFilesrequest, and the server computes edits.I decided to not support a bunch of cases:
app.cssupon a rename ofapp.d.css, or the other way around, because supporting both leads to a cycle ofwillRenameFiles. I decided for the latter, because it naturally supports the scenario where the rename is initiated from renaming an import specifier (which resolves to a declaration file and is the thing we ask the client to rename).package.jsonwillRenameFilesand only return changes.