forked from sveltejs/language-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
LSAndTSDocResolver.ts
186 lines (166 loc) · 6.71 KB
/
LSAndTSDocResolver.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import ts from 'typescript';
import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
import { Document, DocumentManager } from '../../lib/documents';
import { LSConfigManager } from '../../ls-config';
import { debounceSameArg, normalizePath, pathToUrl } from '../../utils';
import { DocumentSnapshot, SvelteDocumentSnapshot } from './DocumentSnapshot';
import {
getService,
getServiceForTsconfig,
forAllServices,
LanguageServiceContainer,
LanguageServiceDocumentContext
} from './service';
import { GlobalSnapshotsManager, SnapshotManager } from './SnapshotManager';
interface LSAndTSDocResolverOptions {
notifyExceedSizeLimit?: () => void;
/**
* True, if used in the context of svelte-check
*/
isSvelteCheck?: boolean;
/**
* This should only be set via svelte-check. Makes sure all documents are resolved to that tsconfig. Has to be absolute.
*/
tsconfigPath?: string;
onProjectReloaded?: () => void;
watchTsConfig?: boolean
}
export class LSAndTSDocResolver {
constructor(
private readonly docManager: DocumentManager,
private readonly workspaceUris: string[],
private readonly configManager: LSConfigManager,
private readonly options?: LSAndTSDocResolverOptions
) {
const handleDocumentChange = (document: Document) => {
// This refreshes the document in the ts language service
this.getSnapshot(document);
};
docManager.on(
'documentChange',
debounceSameArg(
handleDocumentChange,
(newDoc, prevDoc) => newDoc.uri === prevDoc?.uri,
1000
)
);
// New files would cause typescript to rebuild its type-checker.
// 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);
}
/**
* Create a svelte document -> should only be invoked with svelte files.
*/
private createDocument = (fileName: string, content: string) => {
const uri = pathToUrl(fileName);
const document = this.docManager.openDocument({
text: content,
uri
});
this.docManager.lockDocument(uri);
return document;
};
private globalSnapshotsManager = new GlobalSnapshotsManager();
private extendedConfigCache = new Map<string, ts.ExtendedConfigCacheEntry>();
private get lsDocumentContext(): LanguageServiceDocumentContext {
return {
ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx',
createDocument: this.createDocument,
useNewTransformation: this.configManager.getConfig().svelte.useNewTransformation,
transformOnTemplateError: !this.options?.isSvelteCheck,
globalSnapshotsManager: this.globalSnapshotsManager,
notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit,
extendedConfigCache: this.extendedConfigCache,
onProjectReloaded: this.options?.onProjectReloaded,
watchTsConfig: !!this.options?.watchTsConfig
};
}
async getLSForPath(path: string) {
return (await this.getTSService(path)).getService();
}
async getLSAndTSDoc(document: Document): Promise<{
tsDoc: SvelteDocumentSnapshot;
lang: ts.LanguageService;
userPreferences: ts.UserPreferences;
}> {
const lang = await this.getLSForPath(document.getFilePath() || '');
const tsDoc = await this.getSnapshot(document);
const userPreferences = this.getUserPreferences(tsDoc.scriptKind);
return { tsDoc, lang, userPreferences };
}
/**
* Retrieves and updates the snapshot for the given document or path from
* the ts service it primarely belongs into.
* The update is mirrored in all other services, too.
*/
async getSnapshot(document: Document): Promise<SvelteDocumentSnapshot>;
async getSnapshot(pathOrDoc: string | Document): Promise<DocumentSnapshot>;
async getSnapshot(pathOrDoc: string | Document) {
const filePath = typeof pathOrDoc === 'string' ? pathOrDoc : pathOrDoc.getFilePath() || '';
const tsService = await this.getTSService(filePath);
return tsService.updateSnapshot(pathOrDoc);
}
/**
* Updates snapshot path in all existing ts services and retrieves snapshot
*/
async updateSnapshotPath(oldPath: string, newPath: string): Promise<DocumentSnapshot> {
await this.deleteSnapshot(oldPath);
return this.getSnapshot(newPath);
}
/**
* Deletes snapshot in all existing ts services
*/
async deleteSnapshot(filePath: string) {
await forAllServices((service) => service.deleteSnapshot(filePath));
this.docManager.releaseDocument(pathToUrl(filePath));
}
/**
* Updates project files in all existing ts services
*/
async updateProjectFiles() {
await forAllServices((service) => service.updateProjectFiles());
}
/**
* Updates file in all ts services where it exists
*/
async updateExistingTsOrJsFile(
path: string,
changes?: TextDocumentContentChangeEvent[]
): Promise<void> {
path = normalizePath(path);
// Only update once because all snapshots are shared between
// services. Since we don't have a current version of TS/JS
// files, the operation wouldn't be idempotent.
let didUpdate = false;
await forAllServices((service) => {
if (service.hasFile(path) && !didUpdate) {
didUpdate = true;
service.updateTsOrJsFile(path, changes);
}
});
}
/**
* @internal Public for tests only
*/
async getSnapshotManager(filePath: string): Promise<SnapshotManager> {
return (await this.getTSService(filePath)).snapshotManager;
}
async getTSService(filePath?: string): Promise<LanguageServiceContainer> {
if (this.options?.tsconfigPath) {
return getServiceForTsconfig(this.options?.tsconfigPath, this.lsDocumentContext);
}
if (!filePath) {
throw new Error('Cannot call getTSService without filePath and without tsconfigPath');
}
return getService(filePath, this.workspaceUris, this.lsDocumentContext);
}
private getUserPreferences(scriptKind: ts.ScriptKind): ts.UserPreferences {
const configLang =
scriptKind === ts.ScriptKind.TS || scriptKind === ts.ScriptKind.TSX
? 'typescript'
: 'javascript';
return this.configManager.getTsUserPreferences(configLang);
}
}