diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index 2f9cafe7d..a37fd0f2d 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -55,7 +55,10 @@ export class LSAndTSDocResolver { // Open it immediately to reduce rebuilds in the startup // where multiple files and their dependencies // being loaded in a short period of times - docManager.on('documentOpen', handleDocumentChange); + docManager.on('documentOpen', (document) => { + handleDocumentChange(document); + docManager.lockDocument(document.uri); + }); } /** @@ -121,9 +124,12 @@ export class LSAndTSDocResolver { /** * Updates snapshot path in all existing ts services and retrieves snapshot */ - async updateSnapshotPath(oldPath: string, newPath: string): Promise { - await this.deleteSnapshot(oldPath); - return this.getSnapshot(newPath); + async updateSnapshotPath(oldPath: string, newPath: string): Promise { + for (const snapshot of this.globalSnapshotsManager.getByPrefix(oldPath)) { + await this.deleteSnapshot(snapshot.filePath); + } + // This may not be a file but a directory, still try + await this.getSnapshot(newPath); } /** @@ -131,7 +137,13 @@ export class LSAndTSDocResolver { */ async deleteSnapshot(filePath: string) { await forAllServices((service) => service.deleteSnapshot(filePath)); - this.docManager.releaseDocument(pathToUrl(filePath)); + const uri = pathToUrl(filePath); + if (this.docManager.get(uri)) { + // Guard this call, due to race conditions it may already have been closed; + // also this may not be a Svelte file + this.docManager.closeDocument(uri); + } + this.docManager.releaseDocument(uri); } /** diff --git a/packages/language-server/src/plugins/typescript/SnapshotManager.ts b/packages/language-server/src/plugins/typescript/SnapshotManager.ts index e5fb6b57b..c1d0876f7 100644 --- a/packages/language-server/src/plugins/typescript/SnapshotManager.ts +++ b/packages/language-server/src/plugins/typescript/SnapshotManager.ts @@ -23,6 +23,13 @@ export class GlobalSnapshotsManager { return this.documents.get(fileName); } + getByPrefix(path: string) { + path = normalizePath(path); + return Array.from(this.documents.entries()) + .filter((doc) => doc[0].startsWith(path)) + .map((doc) => doc[1]); + } + set(fileName: string, document: DocumentSnapshot) { fileName = normalizePath(fileName); this.documents.set(fileName, document); @@ -112,6 +119,7 @@ export class SnapshotManager { // and set them "manually" in the set/update methods. if (!document) { this.documents.delete(fileName); + this.projectFiles = this.projectFiles.filter((s) => s !== fileName); } else if (this.documents.has(fileName)) { this.documents.set(fileName, document); } @@ -169,7 +177,6 @@ export class SnapshotManager { delete(fileName: string): void { fileName = normalizePath(fileName); - this.projectFiles = this.projectFiles.filter((s) => s !== fileName); this.globalSnapshotsManager.delete(fileName); } diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index 02588fc5b..541898d77 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -40,7 +40,12 @@ import { scriptElementKindToCompletionItemKind } from '../utils'; import { getJsDocTemplateCompletion } from './getJsDocTemplateCompletion'; -import { findContainingNode, getComponentAtPosition, isPartOfImportStatement } from './utils'; +import { + findContainingNode, + getComponentAtPosition, + isKitTypePath, + isPartOfImportStatement +} from './utils'; export interface CompletionEntryWithIdentifier extends ts.CompletionEntry, TextDocumentIdentifier { position: Position; @@ -271,7 +276,7 @@ export class CompletionsProviderImpl implements CompletionsProvider(); for (const c of completionItems) { - if (c.data?.source?.includes('.svelte-kit/types')) { + if (isKitTypePath(c.data?.source)) { $typeImports.set(c.label, c); } } diff --git a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts index 3b8c9ceb4..f53a526c2 100644 --- a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts @@ -1,3 +1,4 @@ +import path from 'path'; import { OptionalVersionedTextDocumentIdentifier, TextDocumentEdit, @@ -9,7 +10,7 @@ import { urlToPath } from '../../../utils'; import { FileRename, UpdateImportsProvider } from '../../interfaces'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; import { convertRange } from '../utils'; -import { SnapshotMap } from './utils'; +import { isKitTypePath, SnapshotMap } from './utils'; export class UpdateImportsProviderImpl implements UpdateImportsProvider { constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} @@ -24,17 +25,51 @@ export class UpdateImportsProviderImpl implements UpdateImportsProvider { const ls = await this.getLSForPath(newPath); // `getEditsForFileRename` might take a while - const fileChanges = ls.getEditsForFileRename(oldPath, newPath, {}, {}); + const fileChanges = ls + .getEditsForFileRename(oldPath, newPath, {}, {}) + // Assumption: Updating imports will not create new files, and to make sure just filter those out + // who - for whatever reason - might be new ones. + .filter((change) => !change.isNewFile || change.fileName === oldPath); await this.lsAndTsDocResolver.updateSnapshotPath(oldPath, newPath); + + const editInOldPath = fileChanges.find( + (change) => + change.fileName.startsWith(oldPath) && + (oldPath.includes(newPath) || !change.fileName.startsWith(newPath)) + ); + const editInNewPath = fileChanges.find( + (change) => + change.fileName.startsWith(newPath) && + (newPath.includes(oldPath) || !change.fileName.startsWith(oldPath)) + ); const updateImportsChanges = fileChanges - // Assumption: Updating imports will not create new files, and to make sure just filter those out - // who - for whatever reason - might be new ones. - .filter((change) => !change.isNewFile || change.fileName === oldPath) - // The language service might want to do edits to the old path, not the new path -> rewire it. - // If there is a better solution for this, please file a PR :) + .filter((change) => { + if (isKitTypePath(change.fileName)) { + // These types are generated from the route files, so we don't want to update them + return false; + } + if (!editInOldPath || !editInNewPath) { + return true; + } + // If both present, take the one that has more text changes to it (more likely to be the correct one) + return editInOldPath.textChanges.length > editInNewPath.textChanges.length + ? change !== editInNewPath + : change !== editInOldPath; + }) .map((change) => { - change.fileName = change.fileName.replace(oldPath, newPath); + if (change === editInOldPath) { + // The language service might want to do edits to the old path, not the new path -> rewire it. + // If there is a better solution for this, please file a PR :) + change.fileName = change.fileName.replace(oldPath, newPath); + } + change.textChanges = change.textChanges.filter( + (textChange) => + // Filter out changes to './$type' imports for Kit route files, + // you'll likely want these to stay as-is + !isKitTypePath(textChange.newText) || + !path.basename(change.fileName).startsWith('+') + ); return change; }); diff --git a/packages/language-server/src/plugins/typescript/features/utils.ts b/packages/language-server/src/plugins/typescript/features/utils.ts index cc0de7a72..6176250fc 100644 --- a/packages/language-server/src/plugins/typescript/features/utils.ts +++ b/packages/language-server/src/plugins/typescript/features/utils.ts @@ -288,3 +288,7 @@ function gatherDescendants( } export const gatherIdentifiers = (node: ts.Node) => gatherDescendants(node, ts.isIdentifier); + +export function isKitTypePath(path?: string): boolean { + return !!path?.includes('.svelte-kit/types'); +}