Skip to content

Commit 79abac8

Browse files
authored
(feat) update imports for renames/moves files (#113)
* (feat) update imports for renames/moves files closes #111 * (fix) bump ts version, refresh files - Bumping ts version speeds up rename files - Not refreshing the files will result in the ts service not noticing some updates which would create a bug where if you rename a file and rename it again, the ts service would have old files and rename would not update all locations. * (fix) cleanup, tests * (fix) add tsconfig to testfiles folder to speed up tests Now that tsconfig is used, the ts service is many times faster because it only knows the files inside the test folder * (feat) add updating imports indicator * (fix) rewire imports changes of moved file 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 :)
1 parent 3f7e9c4 commit 79abac8

21 files changed

+373
-49
lines changed

packages/language-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@
5353
"prettier-plugin-svelte": "1.1.0",
5454
"source-map": "^0.7.3",
5555
"svelte": "3.19.2",
56-
"svelte2tsx": "~0.1.4",
5756
"svelte-preprocess": "~3.7.4",
57+
"svelte2tsx": "~0.1.4",
5858
"typescript": "*",
5959
"vscode-css-languageservice": "4.1.0",
6060
"vscode-emmet-helper": "1.2.17",

packages/language-server/src/lib/documents/DocumentManager.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { EventEmitter } from 'events';
22
import {
33
TextDocumentContentChangeEvent,
4-
TextDocumentIdentifier,
54
TextDocumentItem,
65
VersionedTextDocumentIdentifier,
76
} from 'vscode-languageserver';
@@ -16,6 +15,7 @@ export class DocumentManager {
1615
private emitter = new EventEmitter();
1716
public documents: Map<string, Document> = new Map();
1817
public locked = new Set<string>();
18+
public deleteCandidates = new Set<string>();
1919

2020
constructor(private createDocument: (textDocument: TextDocumentItem) => Document) {}
2121

@@ -39,17 +39,27 @@ export class DocumentManager {
3939
this.locked.add(uri);
4040
}
4141

42-
closeDocument(textDocument: TextDocumentIdentifier) {
43-
const document = this.documents.get(textDocument.uri);
42+
releaseDocument(uri: string): void {
43+
this.locked.delete(uri);
44+
if (this.deleteCandidates.has(uri)) {
45+
this.deleteCandidates.delete(uri);
46+
this.closeDocument(uri);
47+
}
48+
}
49+
50+
closeDocument(uri: string) {
51+
const document = this.documents.get(uri);
4452
if (!document) {
4553
throw new Error('Cannot call methods on an unopened document');
4654
}
4755

4856
this.notify('documentClose', document);
4957

5058
// Some plugin may prevent a document from actually being closed.
51-
if (!this.locked.has(textDocument.uri)) {
52-
this.documents.delete(textDocument.uri);
59+
if (!this.locked.has(uri)) {
60+
this.documents.delete(uri);
61+
} else {
62+
this.deleteCandidates.add(uri);
5363
}
5464
}
5565

packages/language-server/src/ls-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const defaultLSConfig: LSConfig = {
1212
definitions: { enable: true },
1313
documentSymbols: { enable: true },
1414
codeActions: { enable: true },
15+
rename: { enable: true },
1516
},
1617
css: {
1718
enable: true,
@@ -69,6 +70,9 @@ export interface LSTypescriptConfig {
6970
codeActions: {
7071
enable: boolean;
7172
};
73+
rename: {
74+
enable: boolean;
75+
};
7276
}
7377

7478
export interface LSCSSConfig {

packages/language-server/src/plugins/PluginHost.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@ import {
1717
FileChangeType,
1818
CompletionItem,
1919
CompletionContext,
20+
WorkspaceEdit,
2021
} from 'vscode-languageserver';
2122
import { LSConfig, LSConfigManager } from '../ls-config';
2223
import { DocumentManager } from '../lib/documents';
23-
import { LSProvider, Plugin, OnWatchFileChanges, AppCompletionItem } from './interfaces';
24+
import {
25+
LSProvider,
26+
Plugin,
27+
OnWatchFileChanges,
28+
AppCompletionItem,
29+
FileRename,
30+
} from './interfaces';
2431
import { Logger } from '../logger';
2532

2633
enum ExecuteMode {
@@ -222,6 +229,14 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
222229
);
223230
}
224231

232+
async updateImports(fileRename: FileRename): Promise<WorkspaceEdit | null> {
233+
return await this.execute<WorkspaceEdit>(
234+
'updateImports',
235+
[fileRename],
236+
ExecuteMode.FirstNonNull,
237+
);
238+
}
239+
225240
onWatchFileChanges(fileName: string, changeType: FileChangeType): void {
226241
for (const support of this.plugins) {
227242
support.onWatchFileChanges?.(fileName, changeType);

packages/language-server/src/plugins/interfaces.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
SymbolInformation,
1616
TextDocumentIdentifier,
1717
TextEdit,
18+
WorkspaceEdit,
1819
} from 'vscode-languageserver-types';
1920
import { Document } from '../lib/documents';
2021

@@ -85,6 +86,15 @@ export interface CodeActionsProvider {
8586
): Resolvable<CodeAction[]>;
8687
}
8788

89+
export interface FileRename {
90+
oldUri: string;
91+
newUri: string;
92+
}
93+
94+
export interface UpdateImportsProvider {
95+
updateImports(fileRename: FileRename): Resolvable<WorkspaceEdit | null>;
96+
}
97+
8898
export interface OnWatchFileChanges {
8999
onWatchFileChanges(fileName: string, changeType: FileChangeType): void;
90100
}
@@ -98,6 +108,7 @@ export type LSProvider = DiagnosticsProvider &
98108
ColorPresentationsProvider &
99109
DocumentSymbolsProvider &
100110
DefinitionsProvider &
111+
UpdateImportsProvider &
101112
CodeActionsProvider;
102113

103114
export type Plugin = Partial<LSProvider & OnWatchFileChanges>;

packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1-
import { DocumentManager, Document } from '../../lib/documents';
2-
import { pathToUrl } from '../../utils';
3-
import { getLanguageServiceForDocument } from './service';
1+
import ts from 'typescript';
2+
import { Document, DocumentManager } from '../../lib/documents';
3+
import { debounceSameArg, pathToUrl } from '../../utils';
44
import { DocumentSnapshot, SvelteDocumentSnapshot } from './DocumentSnapshot';
5-
import { findTsConfigPath } from './utils';
5+
import { getLanguageServiceForDocument, getLanguageServiceForPath, getService } from './service';
66
import { SnapshotManager } from './SnapshotManager';
7-
import ts from 'typescript';
7+
import { findTsConfigPath } from './utils';
88

99
export class LSAndTSDocResolver {
10-
constructor(private readonly docManager: DocumentManager) {}
10+
constructor(private readonly docManager: DocumentManager) {
11+
docManager.on(
12+
'documentChange',
13+
debounceSameArg(
14+
async (document: Document) => {
15+
// This refreshes the document in the ts language service
16+
this.getLSAndTSDoc(document);
17+
},
18+
(newDoc, prevDoc) => newDoc.uri === prevDoc?.uri,
19+
1000,
20+
),
21+
);
22+
}
1123

1224
/**
1325
* Create a svelte document -> should only be invoked with svelte files.
@@ -24,6 +36,10 @@ export class LSAndTSDocResolver {
2436
return document;
2537
};
2638

39+
getLSForPath(path: string) {
40+
return getLanguageServiceForPath(path, this.createDocument);
41+
}
42+
2743
getLSAndTSDoc(
2844
document: Document,
2945
): {
@@ -53,6 +69,16 @@ export class LSAndTSDocResolver {
5369
return tsDoc;
5470
}
5571

72+
updateSnapshotPath(oldPath: string, newPath: string): DocumentSnapshot {
73+
this.deleteSnapshot(oldPath);
74+
return this.getSnapshot(newPath);
75+
}
76+
77+
deleteSnapshot(filePath: string) {
78+
getService(filePath, this.createDocument).deleteDocument(filePath);
79+
this.docManager.releaseDocument(pathToUrl(filePath));
80+
}
81+
5682
getSnapshotManager(fileName: string): SnapshotManager {
5783
const tsconfigPath = findTsConfigPath(fileName);
5884
const snapshotManager = SnapshotManager.getFromTsConfigPath(tsconfigPath);

packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Position,
1313
Range,
1414
SymbolInformation,
15+
WorkspaceEdit,
1516
} from 'vscode-languageserver';
1617
import {
1718
Document,
@@ -30,15 +31,18 @@ import {
3031
DefinitionsProvider,
3132
DiagnosticsProvider,
3233
DocumentSymbolsProvider,
34+
FileRename,
3335
HoverProvider,
3436
OnWatchFileChanges,
37+
UpdateImportsProvider,
3538
} from '../interfaces';
3639
import { DocumentSnapshot, SnapshotFragment } from './DocumentSnapshot';
3740
import { CodeActionsProviderImpl } from './features/CodeActionsProvider';
3841
import {
3942
CompletionEntryWithIdentifer,
4043
CompletionsProviderImpl,
4144
} from './features/CompletionProvider';
45+
import { UpdateImportsProviderImpl } from './features/UpdateImportsProvider';
4246
import { LSAndTSDocResolver } from './LSAndTSDocResolver';
4347
import {
4448
convertRange,
@@ -55,18 +59,21 @@ export class TypeScriptPlugin
5559
DocumentSymbolsProvider,
5660
DefinitionsProvider,
5761
CodeActionsProvider,
62+
UpdateImportsProvider,
5863
OnWatchFileChanges,
5964
CompletionsProvider<CompletionEntryWithIdentifer> {
6065
private configManager: LSConfigManager;
6166
private readonly lsAndTsDocResolver: LSAndTSDocResolver;
6267
private readonly completionProvider: CompletionsProviderImpl;
6368
private readonly codeActionsProvider: CodeActionsProviderImpl;
69+
private readonly updateImportsProvider: UpdateImportsProviderImpl;
6470

6571
constructor(docManager: DocumentManager, configManager: LSConfigManager) {
6672
this.configManager = configManager;
6773
this.lsAndTsDocResolver = new LSAndTSDocResolver(docManager);
6874
this.completionProvider = new CompletionsProviderImpl(this.lsAndTsDocResolver);
6975
this.codeActionsProvider = new CodeActionsProviderImpl(this.lsAndTsDocResolver);
76+
this.updateImportsProvider = new UpdateImportsProviderImpl(this.lsAndTsDocResolver);
7077
}
7178

7279
async getDiagnostics(document: Document): Promise<Diagnostic[]> {
@@ -257,6 +264,14 @@ export class TypeScriptPlugin
257264
return this.codeActionsProvider.getCodeActions(document, range, context);
258265
}
259266

267+
async updateImports(fileRename: FileRename): Promise<WorkspaceEdit | null> {
268+
if (!this.featureEnabled('rename')) {
269+
return null;
270+
}
271+
272+
return this.updateImportsProvider.updateImports(fileRename);
273+
}
274+
260275
onWatchFileChanges(fileName: string, changeType: FileChangeType) {
261276
const scriptKind = getScriptKindFromFileName(fileName);
262277

@@ -290,6 +305,10 @@ export class TypeScriptPlugin
290305
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
291306
}
292307

308+
private getLSForPath(path: string) {
309+
return this.lsAndTsDocResolver.getLSForPath(path);
310+
}
311+
293312
private getSnapshot(filePath: string, document?: Document) {
294313
return this.lsAndTsDocResolver.getSnapshot(filePath, document);
295314
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
TextDocumentEdit,
3+
TextEdit,
4+
VersionedTextDocumentIdentifier,
5+
WorkspaceEdit,
6+
} from 'vscode-languageserver';
7+
import { Document, mapRangeToOriginal } from '../../../lib/documents';
8+
import { urlToPath } from '../../../utils';
9+
import { FileRename, UpdateImportsProvider } from '../../interfaces';
10+
import { SnapshotFragment } from '../DocumentSnapshot';
11+
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
12+
import { convertRange } from '../utils';
13+
14+
export class UpdateImportsProviderImpl implements UpdateImportsProvider {
15+
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
16+
17+
async updateImports(fileRename: FileRename): Promise<WorkspaceEdit | null> {
18+
const oldPath = urlToPath(fileRename.oldUri);
19+
const newPath = urlToPath(fileRename.newUri);
20+
if (!oldPath || !newPath) {
21+
return null;
22+
}
23+
24+
const ls = this.getLSForPath(newPath);
25+
// `getEditsForFileRename` might take a while
26+
const fileChanges = ls.getEditsForFileRename(oldPath, newPath, {}, {});
27+
28+
this.lsAndTsDocResolver.updateSnapshotPath(oldPath, newPath);
29+
const updateImportsChanges = fileChanges
30+
// Assumption: Updating imports will not create new files, and to make sure just filter those out
31+
// who - for whatever reason - might be new ones.
32+
.filter((change) => !change.isNewFile || change.fileName === oldPath)
33+
// The language service might want to do edits to the old path, not the new path -> rewire it.
34+
// If there is a better solution for this, please file a PR :)
35+
.map((change) => {
36+
change.fileName = change.fileName.replace(oldPath, newPath);
37+
return change;
38+
});
39+
40+
const docs = new Map<string, SnapshotFragment>();
41+
const documentChanges = await Promise.all(
42+
updateImportsChanges.map(async (change) => {
43+
let fragment = docs.get(change.fileName);
44+
if (!fragment) {
45+
fragment = await this.getSnapshot(change.fileName).getFragment();
46+
docs.set(change.fileName, fragment);
47+
}
48+
49+
return TextDocumentEdit.create(
50+
VersionedTextDocumentIdentifier.create(fragment.getURL(), null),
51+
change.textChanges.map((edit) => {
52+
const range = mapRangeToOriginal(
53+
fragment!,
54+
convertRange(fragment!, edit.span),
55+
);
56+
return TextEdit.replace(range, edit.newText);
57+
}),
58+
);
59+
}),
60+
);
61+
62+
return { documentChanges };
63+
}
64+
65+
private getLSForPath(path: string) {
66+
return this.lsAndTsDocResolver.getLSForPath(path);
67+
}
68+
69+
private getSnapshot(filePath: string, document?: Document) {
70+
return this.lsAndTsDocResolver.getSnapshot(filePath, document);
71+
}
72+
}

packages/language-server/src/plugins/typescript/module-loader.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ class ModuleResolutionCache {
3232
this.cache.set(this.getKey(moduleName, containingFile), resolvedModule);
3333
}
3434

35+
/**
36+
* Deletes module from cache. Call this if a file was deleted.
37+
* @param resolvedModuleName full path of the module
38+
*/
39+
delete(resolvedModuleName: string): void {
40+
this.cache.forEach((val, key) => {
41+
if (val.resolvedFileName === resolvedModuleName) {
42+
this.cache.delete(key);
43+
}
44+
});
45+
}
46+
3547
private getKey(moduleName: string, containingFile: string) {
3648
return containingFile + ':::' + ensureRealSvelteFilePath(moduleName);
3749
}
@@ -59,14 +71,15 @@ export function createSvelteModuleLoader(
5971
return {
6072
fileExists: svelteSys.fileExists,
6173
readFile: svelteSys.readFile,
74+
deleteFromModuleCache: (path: string) => moduleCache.delete(path),
6275
resolveModuleNames,
6376
};
6477

6578
function resolveModuleNames(
6679
moduleNames: string[],
6780
containingFile: string,
6881
): (ts.ResolvedModule | undefined)[] {
69-
return moduleNames.map(moduleName => {
82+
return moduleNames.map((moduleName) => {
7083
const cachedModule = moduleCache.get(moduleName, containingFile);
7184
if (cachedModule) {
7285
return cachedModule;

0 commit comments

Comments
 (0)