|
3 | 3 |
|
4 | 4 | "use strict";
|
5 | 5 |
|
6 |
| -let assert = require("assert"); |
7 | 6 | let vscode = require("vscode");
|
8 | 7 | let {
|
9 | 8 | DiagnosticSeverity,
|
10 |
| - ProcessCrashed, |
| 9 | + DocumentLinter, |
11 | 10 | createProcessFactoryAsync,
|
12 | 11 | } = require("quick-lint-js-wasm/quick-lint-js.js");
|
13 | 12 |
|
14 | 13 | // TODO(strager): Allow developers to reload the .wasm file.
|
15 | 14 | let processFactoryPromise = createProcessFactoryAsync();
|
16 | 15 |
|
17 |
| -let DocumentLinterState = { |
18 |
| - // A DocumentForVSCode hasn't been created yet. |
19 |
| - NO_PARSER: "NO_PARSER", |
20 |
| - |
21 |
| - // A DocumentForVSCode is in the process of being created. |
22 |
| - CREATING_PARSER: "CREATING_PARSER", |
23 |
| - |
24 |
| - // A DocumentForVSCode has been created, but it has no text. |
25 |
| - PARSER_UNINITIALIZED: "PARSER_UNINITIALIZED", |
26 |
| - |
27 |
| - // A DocumentForVSCode has been created, and its text is up-to-date with the |
28 |
| - // vscode.Document. |
29 |
| - PARSER_LOADED: "PARSER_LOADED", |
30 |
| - |
31 |
| - // The DocumentForVSCode's Process crashed, and we are creating a new Process |
32 |
| - // and DocumentForVSCode. |
33 |
| - // |
34 |
| - // Document changes should be queued. |
35 |
| - RECOVERING: "RECOVERING", |
36 |
| - |
37 |
| - DISPOSED: "DISPOSED", |
38 |
| -}; |
39 |
| - |
40 |
| -class LintingCrashed extends Error { |
41 |
| - constructor(originalError) { |
42 |
| - super(String(originalError)); |
43 |
| - this.originalError = originalError; |
44 |
| - } |
45 |
| -} |
46 |
| -exports.LintingCrashed = LintingCrashed; |
47 |
| - |
48 |
| -class DocumentLinterDisposed extends Error {} |
49 |
| -exports.DocumentLinterDisposed = DocumentLinterDisposed; |
50 |
| - |
51 |
| -class DocumentLinter { |
52 |
| - // document has the following methods: |
53 |
| - // |
54 |
| - // getText(): string; |
55 |
| - // setDiagnostics(diagnostics: Object[]): void; |
56 |
| - // removeDiagnostics(): void; |
57 |
| - constructor(document, processFactoryPromise) { |
58 |
| - this._document = document; |
59 |
| - this._processFactoryPromise = processFactoryPromise; |
60 |
| - this._state = DocumentLinterState.NO_PARSER; |
61 |
| - |
62 |
| - // Used only in states: CREATING_PARSER |
63 |
| - this._parserPromise = null; |
64 |
| - |
65 |
| - // Used only in states: PARSER_UNINITIALIZED, PARSER_LOADED |
66 |
| - this._parser = null; |
67 |
| - |
68 |
| - // Used only in states: PARSER_LOADED, RECOVERING |
69 |
| - this._pendingChanges = []; |
70 |
| - |
71 |
| - // Used only in states: RECOVERING |
72 |
| - this._recoveryPromise = null; |
73 |
| - } |
74 |
| - |
75 |
| - async _createParser() { |
76 |
| - assert.strictEqual(this._state, DocumentLinterState.NO_PARSER); |
77 |
| - this._state = DocumentLinterState.CREATING_PARSER; |
78 |
| - this._parserPromise = (async () => { |
79 |
| - let factory = await this._processFactoryPromise; |
80 |
| - // TODO(strager): Reuse processes across documents. |
81 |
| - let process = await factory.createProcessAsync(); |
82 |
| - let parser = await process.createDocumentForVSCodeAsync(); |
83 |
| - |
84 |
| - if (this._state === DocumentLinterState.DISPOSED) { |
85 |
| - parser.dispose(); |
86 |
| - throw new DocumentLinterDisposed(); |
87 |
| - } |
88 |
| - assert.strictEqual(this._state, DocumentLinterState.CREATING_PARSER); |
89 |
| - this._parser = parser; |
90 |
| - this._state = DocumentLinterState.PARSER_UNINITIALIZED; |
91 |
| - return parser; |
92 |
| - })(); |
93 |
| - return await this._parserPromise; |
94 |
| - } |
95 |
| - |
96 |
| - async disposeAsync() { |
97 |
| - let oldState = this._state; |
98 |
| - this._state = DocumentLinterState.DISPOSED; |
99 |
| - switch (oldState) { |
100 |
| - case DocumentLinterState.NO_PARSER: |
101 |
| - break; |
102 |
| - |
103 |
| - case DocumentLinterState.CREATING_PARSER: |
104 |
| - case DocumentLinterState.PARSER_UNINITIALIZED: |
105 |
| - case DocumentLinterState.PARSER_LOADED: { |
106 |
| - try { |
107 |
| - await this._parserPromise; |
108 |
| - } catch (e) { |
109 |
| - if (!(e instanceof DocumentLinterDisposed)) { |
110 |
| - throw e; |
111 |
| - } |
112 |
| - } |
113 |
| - if (this._parser !== null) { |
114 |
| - this._parser.dispose(); |
115 |
| - } |
116 |
| - break; |
117 |
| - } |
118 |
| - |
119 |
| - case DocumentLinterState.DISPOSED: |
120 |
| - // TODO(strager): Should double-dispose be okay? |
121 |
| - throw new DocumentLinterDisposed(); |
122 |
| - |
123 |
| - default: |
124 |
| - throw new Error(`Unexpected linter state: ${this._state}`); |
125 |
| - } |
126 |
| - this._document.removeDiagnostics(); |
127 |
| - } |
128 |
| - |
129 |
| - dispose() { |
130 |
| - logAsyncErrors(async () => { |
131 |
| - await this.disposeAsync(); |
132 |
| - }); |
133 |
| - } |
134 |
| - |
135 |
| - async editorChangedVisibilityAsync() { |
136 |
| - switch (this._state) { |
137 |
| - case DocumentLinterState.NO_PARSER: |
138 |
| - await this._createParser(); |
139 |
| - await this.editorChangedVisibilityAsync(); |
140 |
| - break; |
141 |
| - |
142 |
| - case DocumentLinterState.CREATING_PARSER: |
143 |
| - await this._parserPromise; |
144 |
| - await this.editorChangedVisibilityAsync(); |
145 |
| - break; |
146 |
| - |
147 |
| - case DocumentLinterState.PARSER_UNINITIALIZED: |
148 |
| - await this._initializeParserAsync(); |
149 |
| - break; |
150 |
| - |
151 |
| - case DocumentLinterState.PARSER_LOADED: |
152 |
| - // No changes could have been made with the editor closed. Ignore. |
153 |
| - break; |
154 |
| - |
155 |
| - case DocumentLinterState.DISPOSED: |
156 |
| - throw new DocumentLinterDisposed(); |
157 |
| - |
158 |
| - default: |
159 |
| - throw new Error(`Unexpected linter state: ${this._state}`); |
160 |
| - } |
161 |
| - } |
162 |
| - |
163 |
| - async textChangedAsync(changes) { |
164 |
| - // BEGIN CRITICAL SECTION (no awaiting below) |
165 |
| - switch (this._state) { |
166 |
| - case DocumentLinterState.NO_PARSER: |
167 |
| - // END CRITICAL SECTION (no awaiting above) |
168 |
| - await this._createParser(); |
169 |
| - await this._initializeParserAsync(); |
170 |
| - break; |
171 |
| - |
172 |
| - case DocumentLinterState.CREATING_PARSER: |
173 |
| - // END CRITICAL SECTION (no awaiting above) |
174 |
| - await this._parserPromise; |
175 |
| - await this._initializeParserAsync(); |
176 |
| - break; |
177 |
| - |
178 |
| - case DocumentLinterState.PARSER_UNINITIALIZED: |
179 |
| - // END CRITICAL SECTION (no awaiting above) |
180 |
| - await this._initializeParserAsync(); |
181 |
| - break; |
182 |
| - |
183 |
| - case DocumentLinterState.PARSER_LOADED: |
184 |
| - this._pendingChanges.push(...changes); |
185 |
| - try { |
186 |
| - for (let change of this._pendingChanges) { |
187 |
| - this._parser.replaceText(change.range, change.text); |
188 |
| - } |
189 |
| - this._pendingChanges.length = 0; |
190 |
| - // END CRITICAL SECTION (no awaiting above) |
191 |
| - |
192 |
| - let diags = this._parser.lint(); |
193 |
| - this._document.setDiagnostics(diags); |
194 |
| - } catch (e) { |
195 |
| - // END CRITICAL SECTION (no awaiting above) |
196 |
| - if (e instanceof ProcessCrashed) { |
197 |
| - await this._recoverFromCrashAsync(e); |
198 |
| - } else { |
199 |
| - throw e; |
200 |
| - } |
201 |
| - } |
202 |
| - break; |
203 |
| - |
204 |
| - case DocumentLinterState.RECOVERING: |
205 |
| - this._pendingChanges.push(...changes); |
206 |
| - // END CRITICAL SECTION (no awaiting above) |
207 |
| - await this._recoveryPromise; |
208 |
| - // Changes should have been applied during recovery. |
209 |
| - assert.strictEqual(this._pendingChanges.includes(changes[0]), false); |
210 |
| - break; |
211 |
| - |
212 |
| - case DocumentLinterState.DISPOSED: |
213 |
| - throw new DocumentLinterDisposed(); |
214 |
| - |
215 |
| - default: |
216 |
| - throw new Error(`Unexpected linter state: ${this._state}`); |
217 |
| - } |
218 |
| - } |
219 |
| - |
220 |
| - // Transition: PARSER_UNINITIALIZED -> PARSER_LOADED (or NO_PARSER on error) |
221 |
| - async _initializeParserAsync() { |
222 |
| - // BEGIN CRITICAL SECTION (no awaiting below) |
223 |
| - assert.strictEqual(this._state, DocumentLinterState.PARSER_UNINITIALIZED); |
224 |
| - try { |
225 |
| - this._parser.replaceText( |
226 |
| - { |
227 |
| - start: { line: 0, character: 0 }, |
228 |
| - end: { line: 0, character: 0 }, |
229 |
| - }, |
230 |
| - this._document.getText() |
231 |
| - ); |
232 |
| - this._pendingChanges.length = 0; |
233 |
| - this._state = DocumentLinterState.PARSER_LOADED; |
234 |
| - // END CRITICAL SECTION (no awaiting above) |
235 |
| - |
236 |
| - let diags = this._parser.lint(); |
237 |
| - this._document.setDiagnostics(diags); |
238 |
| - } catch (e) { |
239 |
| - if (e instanceof ProcessCrashed) { |
240 |
| - await this._recoverFromCrashAsync(e); |
241 |
| - } else { |
242 |
| - throw e; |
243 |
| - } |
244 |
| - } |
245 |
| - } |
246 |
| - |
247 |
| - // Transition: any -> RECOVERING -> PARSER_LOADED (or NO_PARSER on error) |
248 |
| - async _recoverFromCrashAsync(error) { |
249 |
| - // BEGIN CRITICAL SECTION (no awaiting below) |
250 |
| - console.warn( |
251 |
| - `[quick-lint-js] warning: Parser process crashed. Recovering: ${error.stack}` |
252 |
| - ); |
253 |
| - this._state = DocumentLinterState.RECOVERING; |
254 |
| - this._recoveryPromise = (async () => { |
255 |
| - let diags; |
256 |
| - try { |
257 |
| - // TODO(strager): Reuse processes across documents. |
258 |
| - let factory = await this._processFactoryPromise; |
259 |
| - let process = await factory.createProcessAsync(); |
260 |
| - let parser = await process.createDocumentForVSCodeAsync(); |
261 |
| - |
262 |
| - // BEGIN CRITICAL SECTION (no awaiting below) |
263 |
| - assert.strictEqual(this._state, DocumentLinterState.RECOVERING); |
264 |
| - parser.replaceText( |
265 |
| - { |
266 |
| - start: { line: 0, character: 0 }, |
267 |
| - end: { line: 0, character: 0 }, |
268 |
| - }, |
269 |
| - this._document.getText() |
270 |
| - ); |
271 |
| - this._pendingChanges.length = 0; |
272 |
| - this._parser = parser; |
273 |
| - this._state = DocumentLinterState.PARSER_LOADED; |
274 |
| - // END CRITICAL SECTION (no awaiting above) |
275 |
| - |
276 |
| - diags = parser.lint(); |
277 |
| - } catch (e) { |
278 |
| - this._parser = null; |
279 |
| - this._parserPromise = null; |
280 |
| - this._state = DocumentLinterState.NO_PARSER; |
281 |
| - if (e instanceof ProcessCrashed) { |
282 |
| - throw new LintingCrashed(e); |
283 |
| - } else { |
284 |
| - throw e; |
285 |
| - } |
286 |
| - } |
287 |
| - this._document.setDiagnostics(diags); |
288 |
| - })(); |
289 |
| - // END CRITICAL SECTION (no awaiting above) |
290 |
| - await this._recoveryPromise; |
291 |
| - } |
292 |
| -} |
293 |
| - |
294 | 16 | class VSCodeDocumentLinter {
|
295 | 17 | constructor(document, diagnosticCollection) {
|
296 | 18 | this._documentLinter = new DocumentLinter(
|
|
0 commit comments