@spyglassmc/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/lib/common/Dev.d.ts +11 -0
  2. package/lib/common/Dev.js +90 -0
  3. package/lib/{service → common}/Logger.d.ts +0 -0
  4. package/lib/{service → common}/Logger.js +0 -0
  5. package/lib/common/Operations.d.ts +12 -0
  6. package/lib/common/Operations.js +33 -0
  7. package/lib/common/ReadonlyProxy.d.ts +9 -0
  8. package/lib/common/ReadonlyProxy.js +23 -0
  9. package/lib/common/StateProxy.d.ts +35 -0
  10. package/lib/common/StateProxy.js +69 -0
  11. package/lib/common/externals/NodeJsExternals.js +1 -0
  12. package/lib/common/index.d.ts +6 -0
  13. package/lib/common/index.js +6 -0
  14. package/lib/common/util.d.ts +33 -5
  15. package/lib/common/util.js +43 -11
  16. package/lib/node/AstNode.d.ts +9 -8
  17. package/lib/node/CommentNode.d.ts +5 -4
  18. package/lib/node/CommentNode.js +4 -6
  19. package/lib/node/FileNode.d.ts +5 -1
  20. package/lib/node/FileNode.js +1 -1
  21. package/lib/node/FloatNode.d.ts +1 -1
  22. package/lib/node/IntegerNode.d.ts +1 -1
  23. package/lib/node/LiteralNode.d.ts +1 -1
  24. package/lib/node/LongNode.d.ts +1 -1
  25. package/lib/node/ResourceLocationNode.d.ts +3 -3
  26. package/lib/node/StringNode.d.ts +1 -1
  27. package/lib/node/SymbolNode.d.ts +1 -1
  28. package/lib/parser/resourceLocation.js +0 -8
  29. package/lib/parser/string.d.ts +2 -2
  30. package/lib/parser/symbol.js +1 -7
  31. package/lib/parser/util.js +0 -2
  32. package/lib/processor/InlayHintProvider.d.ts +2 -1
  33. package/lib/processor/SignatureHelpProvider.d.ts +2 -1
  34. package/lib/processor/binder/Binder.d.ts +26 -0
  35. package/lib/processor/binder/Binder.js +18 -0
  36. package/lib/processor/binder/builtin.d.ts +27 -0
  37. package/lib/processor/binder/builtin.js +116 -0
  38. package/lib/processor/binder/index.d.ts +3 -0
  39. package/lib/processor/binder/index.js +3 -0
  40. package/lib/processor/checker/builtin.js +5 -5
  41. package/lib/processor/colorizer/Colorizer.d.ts +2 -1
  42. package/lib/processor/completer/Completer.d.ts +2 -1
  43. package/lib/processor/completer/builtin.d.ts +3 -2
  44. package/lib/processor/completer/builtin.js +1 -1
  45. package/lib/processor/formatter/Formatter.d.ts +2 -1
  46. package/lib/processor/index.d.ts +1 -0
  47. package/lib/processor/index.js +1 -0
  48. package/lib/processor/linter/Linter.d.ts +2 -1
  49. package/lib/processor/linter/builtin.d.ts +2 -1
  50. package/lib/processor/util.d.ts +3 -13
  51. package/lib/processor/util.js +0 -10
  52. package/lib/service/CacheService.d.ts +2 -0
  53. package/lib/service/CacheService.js +3 -0
  54. package/lib/service/Context.d.ts +15 -12
  55. package/lib/service/Context.js +12 -6
  56. package/lib/service/Downloader.d.ts +1 -2
  57. package/lib/service/FileService.d.ts +1 -2
  58. package/lib/service/MetaRegistry.d.ts +8 -3
  59. package/lib/service/MetaRegistry.js +20 -1
  60. package/lib/service/Profiler.d.ts +3 -2
  61. package/lib/service/Profiler.js +58 -6
  62. package/lib/service/Project.d.ts +55 -24
  63. package/lib/service/Project.js +265 -166
  64. package/lib/service/Service.d.ts +1 -1
  65. package/lib/service/UriProcessor.d.ts +5 -0
  66. package/lib/service/UriProcessor.js +2 -0
  67. package/lib/service/index.d.ts +1 -2
  68. package/lib/service/index.js +1 -2
  69. package/lib/symbol/Symbol.d.ts +3 -2
  70. package/lib/symbol/SymbolUtil.d.ts +8 -7
  71. package/lib/symbol/SymbolUtil.js +31 -9
  72. package/lib/symbol/index.d.ts +0 -1
  73. package/lib/symbol/index.js +0 -1
  74. package/package.json +2 -2
  75. package/lib/service/Operations.d.ts +0 -8
  76. package/lib/service/Operations.js +0 -20
  77. package/lib/symbol/UriBinder.d.ts +0 -3
  78. package/lib/symbol/UriBinder.js +0 -2
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { TextDocument } from 'vscode-languageserver-textdocument';
8
- import { bufferToString, CachePromise } from '../common/index.js';
8
+ import { bufferToString, Logger, SingletonPromise, StateProxy } from '../common/index.js';
9
9
  import { FileNode } from '../node/index.js';
10
10
  import { file } from '../parser/index.js';
11
11
  import { traversePreOrder } from '../processor/index.js';
@@ -13,30 +13,64 @@ import { Source } from '../source/index.js';
13
13
  import { SymbolUtil } from '../symbol/index.js';
14
14
  import { CacheService } from './CacheService.js';
15
15
  import { ConfigService, LinterConfigValue } from './Config.js';
16
- import { CheckerContext, LinterContext, ParserContext, UriBinderContext } from './Context.js';
16
+ import { BinderContext, CheckerContext, LinterContext, ParserContext, UriBinderContext } from './Context.js';
17
17
  import { DependencyKey } from './Dependency.js';
18
18
  import { Downloader } from './Downloader.js';
19
19
  import { LinterErrorReporter } from './ErrorReporter.js';
20
20
  import { ArchiveUriSupporter, FileService, FileUriSupporter } from './FileService.js';
21
21
  import { fileUtil } from './fileUtil.js';
22
- import { Logger } from './Logger.js';
23
22
  import { MetaRegistry } from './MetaRegistry.js';
24
23
  import { ProfilerFactory } from './Profiler.js';
25
24
  const CacheAutoSaveInterval = 600000; // 10 Minutes.
26
25
  /* istanbul ignore next */
27
26
  /**
28
27
  * Manage all tracked documents and errors.
28
+ *
29
+ * The four stages of processing a document:
30
+ * 1. `read` - read the file from the external file system as a `TextDocument`.
31
+ * 2. `parse` - Parse the `TextDocument` into an `AstNode`.
32
+ * 3. `bind` - Bind the `AstNode` and populate both the global symbol table and the local symbol tables on the nodes.
33
+ * 4. `check` (includes `lint`) - Check the `AstNode` with information from the symbol tables.
34
+ *
35
+ * **Caching**
36
+ *
37
+ * The global symbol table along with a list of file URIs and checksums is cached in memory and is periodically saved to disk.
38
+ *
39
+ * The `TextDocument`s and file `AstNode`s (including their local symbol tables) managed by the client are stored in memory until the client sends a `didClose` notification.
40
+ *
41
+ * Some `TextDocument`s may be cached to avoid excessive reading from the file system.
42
+ *
43
+ * **INIT and READY**
44
+ *
45
+ * When a new instance of the {@link Project} class is constructed, its INIT and READY processes are immediately started in serial.
46
+ *
47
+ * During the INIT process of the project, the config and language feature initialization are processed.
48
+ * The Promise returned by the {@link init} function resolves when the INIT process is complete.
49
+ *
50
+ * During the READY process of the project, the whole project is analyzed mainly to populate the global symbol table.
51
+ * The Promise returned by the {@link ready} function resolves when the READY process is complete.
52
+ *
53
+ * The following generally happens during the READY process:
54
+ * 1. A list of file URIs under the project is obtained.
55
+ * 2. The global symbol cache, if available, is loaded and validated against the know list of files.
56
+ * A list of files that need to be (re)processed is returned in this step.
57
+ * 3. For each files in the new list, the file is read, parsed, bound, and checked.
58
+ *
59
+ * **EDITING**
60
+ *
61
+ * After the READY process is complete, editing text documents as signaled by the client or the file watcher results in the file being re-processed.
29
62
  */
30
63
  export class Project {
31
64
  static RootSuffix = '/pack.mcmeta';
65
+ /** Prevent circular binding. */
66
+ #bindingInProgressUris = new Set();
32
67
  #cacheSaverIntervalId;
33
68
  cacheService;
34
- /**
35
- * URI of files that are currently managed by the language client.
36
- */
69
+ /** URI of files that are currently managed by the language client. */
37
70
  #clientManagedUris = new Set();
71
+ #clientManagedDocAndNodes = new Map();
38
72
  #configService;
39
- #docAndNodes = new Map();
73
+ #symbolUpToDateUris = new Set();
40
74
  #eventEmitter;
41
75
  #initializers;
42
76
  #initPromise;
@@ -146,9 +180,9 @@ export class Project {
146
180
  this.#cacheSaverIntervalId = setInterval(() => this.cacheService.save(), CacheAutoSaveInterval);
147
181
  this
148
182
  .on('documentUpdated', ({ doc, node }) => {
149
- if (!this.#isReady) {
150
- return;
151
- }
183
+ // if (!this.#isReady) {
184
+ // return
185
+ // }
152
186
  this.emit('documentErrorred', {
153
187
  doc,
154
188
  errors: FileNode.getErrors(node),
@@ -159,33 +193,31 @@ export class Project {
159
193
  if (uri.endsWith(Project.RootSuffix)) {
160
194
  this.updateRoots();
161
195
  }
162
- this.bind(uri);
163
- return this.ensureParsedAndChecked(uri);
196
+ this.bindUri(uri);
197
+ return this.ensureBindingStarted(uri);
164
198
  })
165
199
  .on('fileModified', async ({ uri }) => {
200
+ this.#symbolUpToDateUris.delete(uri);
166
201
  if (this.isOnlyWatched(uri)) {
167
- this.#docAndNodes.delete(uri);
168
- await this.ensureParsedAndChecked(uri);
202
+ await this.ensureBindingStarted(uri);
169
203
  }
170
204
  })
171
205
  .on('fileDeleted', ({ uri }) => {
172
206
  if (uri.endsWith(Project.RootSuffix)) {
173
207
  this.updateRoots();
174
208
  }
209
+ this.#symbolUpToDateUris.delete(uri);
175
210
  this.symbols.clear({ uri });
176
211
  this.tryClearingCache(uri);
177
212
  })
178
213
  .on('ready', () => {
179
214
  this.#isReady = true;
180
- // Recheck client managed files.
181
- const promises = [];
182
- for (const uri of this.#clientManagedUris) {
183
- const result = this.#docAndNodes.get(uri);
184
- if (result) {
185
- promises.push(this.check(result.doc, result.node));
186
- }
187
- }
188
- Promise.all(promises).catch(e => this.logger.error('[Project#ready] Error occurred when rechecking client managed files after ready', e));
215
+ // // Recheck client managed files after the READY process, as they may have incomplete results and are user-facing.
216
+ // const promises: Promise<unknown>[] = []
217
+ // for (const { doc, node } of this.#clientManagedDocAndNodes.values()) {
218
+ // promises.push(this.check(doc, node))
219
+ // }
220
+ // Promise.all(promises).catch(e => this.logger.error('[Project#ready] Error occurred when rechecking client managed files after READY', e))
189
221
  });
190
222
  }
191
223
  setInitPromise() {
@@ -291,8 +323,6 @@ export class Project {
291
323
  const ready = async () => {
292
324
  await this.init();
293
325
  const __profiler = this.profilers.get('project#ready');
294
- const ensureParsed = this.ensureParsed.bind(this);
295
- const ensureChecked = this.ensureChecked.bind(this);
296
326
  await Promise.all([
297
327
  listDependencyFiles(),
298
328
  listProjectFiles(),
@@ -300,7 +330,7 @@ export class Project {
300
330
  this.#dependencyFiles = new Set(this.fs.listFiles());
301
331
  this.#dependencyRoots = new Set(this.fs.listRoots());
302
332
  this.updateRoots();
303
- __profiler.task('List Files');
333
+ __profiler.task('List URIs');
304
334
  for (const [id, { checksum, registrar }] of this.meta.symbolRegistrars) {
305
335
  const cacheChecksum = this.cacheService.checksums.symbolRegistrars[id];
306
336
  if (cacheChecksum === undefined || checksum !== cacheChecksum) {
@@ -321,24 +351,33 @@ export class Project {
321
351
  }
322
352
  __profiler.task('Validate Cache');
323
353
  if (addedFiles.length > 0) {
324
- this.bind(addedFiles);
354
+ this.bindUri(addedFiles);
325
355
  }
326
356
  __profiler.task('Bind URIs');
327
- const files = [...addedFiles, ...changedFiles]; // FIXME: mcdoc files might need to be parsed and checked before others.
328
- // const docAndNodes = (await Promise.all(files.map(uri => limit(ensureParsed, uri)))).filter((r): r is DocAndNode => !!r)
329
- const docAndNodes = (await Promise.all(files.map(uri => ensureParsed(uri)))).filter((r) => !!r);
330
- __profiler.task('Parse Files');
331
- // await Promise.all(docAndNodes.map(({ doc, node }) => limit(ensureChecked, doc, node)))
332
- await Promise.all(docAndNodes.map(({ doc, node }) => ensureChecked(doc, node)));
333
- __profiler.task('Check Files').finalize();
357
+ const files = [...addedFiles, ...changedFiles].sort(this.meta.uriSorter);
358
+ __profiler.task('Sort URIs');
359
+ const __bindProfiler = this.profilers.get('project#ready#bind', 'top-n', 50);
360
+ for (const uri of files) {
361
+ await this.ensureBindingStarted(uri);
362
+ __bindProfiler.task(uri);
363
+ }
364
+ __bindProfiler.finalize();
365
+ __profiler.task('Bind Files');
366
+ __profiler.finalize();
334
367
  this.emit('ready', {});
335
368
  };
336
369
  this.#readyPromise = ready();
337
370
  }
371
+ /**
372
+ * Load the config file and initialize parsers and processors.
373
+ */
338
374
  async init() {
339
375
  await this.#initPromise;
340
376
  return this;
341
377
  }
378
+ /**
379
+ * Finish the initial run of parsing, binding, and checking the entire project.
380
+ */
342
381
  async ready() {
343
382
  await this.#readyPromise;
344
383
  return this;
@@ -367,136 +406,182 @@ export class Project {
367
406
  normalizeUri(uri) {
368
407
  return this.fs.mapFromDisk(this.externals.uri.normalize(uri));
369
408
  }
370
- /**
371
- * @returns The language ID of the file, or the file extension without the leading dot.
372
- */
373
- getLanguageID(uri) {
374
- uri = this.normalizeUri(uri);
375
- const ext = fileUtil.extname(uri) ?? '.plaintext';
376
- return this.meta.getLanguageID(ext) ?? ext.slice(1);
377
- }
378
- /**
379
- * @returns The cached `TextDocument` and `AstNode` for the URI, or `undefined` when such data isn't available in cache.
380
- */
381
- get(uri) {
382
- uri = this.normalizeUri(uri);
383
- return this.#docAndNodes.get(uri);
409
+ static TextDocumentCacheMaxLength = 268435456;
410
+ #textDocumentCache = new Map();
411
+ #textDocumentCacheLength = 0;
412
+ removeCachedTextDocument(uri) {
413
+ const doc = this.#textDocumentCache.get(uri);
414
+ if (doc && !(doc instanceof Promise)) {
415
+ this.#textDocumentCacheLength -= doc.getText().length;
416
+ }
417
+ this.#textDocumentCache.delete(uri);
384
418
  }
385
- /**
386
- * @throws FS-related errors
387
- */
388
- async ensureParsed(uri) {
419
+ async read(uri) {
420
+ const getLanguageID = (uri) => {
421
+ const ext = fileUtil.extname(uri) ?? '.plaintext';
422
+ return this.meta.getLanguageID(ext) ?? ext.slice(1);
423
+ };
424
+ const createTextDocument = async (uri) => {
425
+ const languageId = getLanguageID(uri);
426
+ if (!this.meta.isSupportedLanguage(languageId)) {
427
+ return undefined;
428
+ }
429
+ try {
430
+ const content = bufferToString(await this.fs.readFile(uri));
431
+ return TextDocument.create(uri, languageId, -1, content);
432
+ }
433
+ catch (e) {
434
+ this.logger.warn(`[Project] [read] Failed creating TextDocument for “${uri}”`, e);
435
+ return undefined;
436
+ }
437
+ };
438
+ const trimCache = () => {
439
+ const iterator = this.#textDocumentCache.keys();
440
+ while (this.#textDocumentCacheLength > Project.TextDocumentCacheMaxLength) {
441
+ const result = iterator.next();
442
+ if (result.done) {
443
+ throw new Error(`[Project] [read] Cache is too large with length ${this.#textDocumentCacheLength} even though it's empty; make sure to call 'removeCachedTextDocument()' instead of 'this.#textDocumentCache.delete()'`);
444
+ }
445
+ this.removeCachedTextDocument(result.value);
446
+ }
447
+ };
448
+ const getCacheHandlingPromise = async (uri) => {
449
+ if (this.#textDocumentCache.has(uri)) {
450
+ const ans = this.#textDocumentCache.get(uri);
451
+ // Move the entry to the end of the cache.
452
+ // The goal is that more-frequently-used entries are preferably not trimmed.
453
+ this.#textDocumentCache.delete(uri);
454
+ this.#textDocumentCache.set(uri, ans);
455
+ return ans;
456
+ }
457
+ else {
458
+ const promise = createTextDocument(uri);
459
+ this.#textDocumentCache.set(uri, promise);
460
+ // We replace the Promise in the cache with the TextDocument after it resolves,
461
+ // or removes it from the cache if it resolves to undefined.
462
+ const doc = await promise;
463
+ if (this.#textDocumentCache.get(uri) === promise) {
464
+ // The Promise in the cache is the same as the one we created earlier.
465
+ // This check is to make sure we don't set a wrong TextDocument to the cache in case the cache was modified elsewhere.
466
+ if (doc) {
467
+ this.#textDocumentCache.set(uri, doc);
468
+ this.#textDocumentCacheLength += doc.getText().length;
469
+ trimCache();
470
+ }
471
+ else {
472
+ this.#textDocumentCache.delete(uri);
473
+ }
474
+ }
475
+ return doc;
476
+ }
477
+ };
389
478
  uri = this.normalizeUri(uri);
390
- if (this.#docAndNodes.has(uri)) {
391
- return this.#docAndNodes.get(uri);
392
- }
393
- const languageID = this.getLanguageID(uri);
394
- if (!this.meta.isSupportedLanguage(languageID)) {
395
- return undefined;
396
- }
397
- try {
398
- const content = bufferToString(await this.fs.readFile(uri));
399
- const doc = TextDocument.create(uri, languageID, -1, content);
400
- return this.parseAndCache(doc);
401
- }
402
- catch (e) {
403
- this.logger.warn(`[Project] [ensureParsed] Failed for “${uri}”`, e);
404
- return undefined;
479
+ if (this.#clientManagedUris.has(uri)) {
480
+ const result = this.#clientManagedDocAndNodes.get(uri);
481
+ if (result) {
482
+ return result.doc;
483
+ }
484
+ throw new Error(`[Project] [read] Client-managed URI “${uri}” does not have a TextDocument in the cache`);
405
485
  }
406
- }
407
- parseAndCache(doc) {
408
- const node = this.parse(doc);
409
- return this.cache(doc, node);
486
+ return getCacheHandlingPromise(uri);
410
487
  }
411
488
  parse(doc) {
412
489
  const ctx = ParserContext.create(this, { doc });
413
490
  const src = new Source(doc.getText());
414
- let ans;
415
- ctx.symbols.clear({ contributor: 'parser', uri: doc.uri });
416
- ctx.symbols.contributeAs('parser', () => ans = file()(src, ctx));
417
- return ans;
491
+ const node = file()(src, ctx);
492
+ return node;
418
493
  }
419
- cache(doc, node) {
420
- const data = { doc, node };
421
- this.#docAndNodes.set(doc.uri, data);
422
- this.emit('documentUpdated', data);
423
- return data;
494
+ async bind(doc, node) {
495
+ if (node.binderErrors) {
496
+ return;
497
+ }
498
+ try {
499
+ this.#bindingInProgressUris.add(doc.uri);
500
+ const binder = this.meta.getBinder(node.type);
501
+ const ctx = BinderContext.create(this, { doc });
502
+ ctx.symbols.clear({ contributor: 'binder', uri: doc.uri });
503
+ await ctx.symbols.contributeAsAsync('binder', async () => {
504
+ const proxy = StateProxy.create(node);
505
+ await binder(proxy, ctx);
506
+ node.binderErrors = ctx.err.dump();
507
+ });
508
+ this.#bindingInProgressUris.delete(doc.uri);
509
+ this.#symbolUpToDateUris.add(doc.uri);
510
+ }
511
+ catch (e) {
512
+ this.logger.error(`[Project] [bind] Failed for “${doc.uri}” #${doc.version}`, e);
513
+ }
424
514
  }
425
515
  async check(doc, node) {
426
- const checker = this.meta.getChecker(node.type);
427
- const ctx = CheckerContext.create(this, { doc });
428
- ctx.symbols.clear({ contributor: 'checker', uri: doc.uri });
429
- await ctx.symbols.contributeAsAsync('checker', async () => {
430
- await checker(node, ctx);
431
- node.checkerErrors = ctx.err.dump();
432
- this.cache(doc, node);
433
- this.ensureLinted(doc, node);
434
- });
435
- }
436
- async ensureChecked(doc, node) {
437
- if (!node.checkerErrors) {
438
- try {
439
- return this.check(doc, node);
440
- }
441
- catch (e) {
442
- this.logger.error(`[Project] [ensuredChecked] Failed for “${doc.uri}” #${doc.version}`, e);
443
- }
516
+ if (node.checkerErrors) {
517
+ return;
518
+ }
519
+ try {
520
+ const checker = this.meta.getChecker(node.type);
521
+ const ctx = CheckerContext.create(this, { doc });
522
+ ctx.symbols.clear({ contributor: 'checker', uri: doc.uri });
523
+ await ctx.symbols.contributeAsAsync('checker', async () => {
524
+ await checker(StateProxy.create(node), ctx);
525
+ node.checkerErrors = ctx.err.dump();
526
+ this.lint(doc, node);
527
+ });
528
+ }
529
+ catch (e) {
530
+ this.logger.error(`[Project] [check] Failed for “${doc.uri}” #${doc.version}`, e);
444
531
  }
445
532
  }
446
533
  lint(doc, node) {
534
+ if (node.linterErrors) {
535
+ return;
536
+ }
447
537
  node.linterErrors = [];
448
- for (const [ruleName, rawValue] of Object.entries(this.config.lint)) {
449
- const result = LinterConfigValue.destruct(rawValue);
450
- if (!result) {
451
- // Rule is disabled (i.e. set to `null`) in the config.
452
- continue;
453
- }
454
- const { ruleSeverity, ruleValue } = result;
455
- const { configValidator, linter, nodePredicate } = this.meta.getLinter(ruleName);
456
- if (!configValidator(ruleName, ruleValue, this.logger)) {
457
- // Config value is invalid.
458
- continue;
459
- }
460
- const ctx = LinterContext.create(this, {
461
- doc,
462
- err: new LinterErrorReporter(ruleName, ruleSeverity),
463
- ruleName,
464
- ruleValue,
465
- });
466
- traversePreOrder(node, () => true, () => true, node => {
467
- if (nodePredicate(node)) {
468
- linter(node, ctx);
538
+ try {
539
+ for (const [ruleName, rawValue] of Object.entries(this.config.lint)) {
540
+ const result = LinterConfigValue.destruct(rawValue);
541
+ if (!result) {
542
+ // Rule is disabled (i.e. set to `null`) in the config.
543
+ continue;
469
544
  }
470
- });
471
- node.linterErrors.push(...ctx.err.dump());
472
- }
473
- this.cache(doc, node);
474
- }
475
- ensureLinted(doc, node) {
476
- if (!node.linterErrors) {
477
- try {
478
- this.lint(doc, node);
479
- }
480
- catch (e) {
481
- this.logger.error(`[Project] [ensureLinted] Failed for “${doc.uri}” #${doc.version}`, e);
545
+ const { ruleSeverity, ruleValue } = result;
546
+ const { configValidator, linter, nodePredicate } = this.meta.getLinter(ruleName);
547
+ if (!configValidator(ruleName, ruleValue, this.logger)) {
548
+ // Config value is invalid.
549
+ continue;
550
+ }
551
+ const ctx = LinterContext.create(this, {
552
+ doc,
553
+ err: new LinterErrorReporter(ruleName, ruleSeverity),
554
+ ruleName,
555
+ ruleValue,
556
+ });
557
+ traversePreOrder(node, () => true, () => true, node => {
558
+ if (nodePredicate(node)) {
559
+ const proxy = StateProxy.create(node);
560
+ linter(proxy, ctx);
561
+ }
562
+ });
563
+ node.linterErrors.push(...ctx.err.dump());
482
564
  }
483
565
  }
484
- }
485
- async ensureParsedAndChecked(uri) {
486
- const result = await this.ensureParsed(uri);
487
- if (result) {
488
- await this.ensureChecked(result.doc, result.node);
566
+ catch (e) {
567
+ this.logger.error(`[Project] [lint] Failed for “${doc.uri}” #${doc.version}`, e);
489
568
  }
490
- return result;
491
569
  }
492
- async ensureParsedAndCheckedOnlyWhenReady(uri) {
493
- const result = await this.ensureParsed(uri);
494
- if (this.#isReady && result) {
495
- await this.ensureChecked(result.doc, result.node);
570
+ // @SingletonPromise()
571
+ async ensureBindingStarted(uri) {
572
+ if (this.#symbolUpToDateUris.has(uri) || this.#bindingInProgressUris.has(uri)) {
573
+ return;
496
574
  }
497
- return result;
575
+ this.#bindingInProgressUris.add(uri);
576
+ const doc = await this.read(uri);
577
+ if (!doc || !(await this.cacheService.hasFileChangedSinceCache(doc))) {
578
+ return;
579
+ }
580
+ const node = this.parse(doc);
581
+ await this.bind(doc, node);
582
+ this.emit('documentUpdated', { doc, node });
498
583
  }
499
- bind(param) {
584
+ bindUri(param) {
500
585
  const ctx = UriBinderContext.create(this);
501
586
  if (typeof param === 'string') {
502
587
  ctx.symbols.clear({ contributor: 'uri_binder', uri: param });
@@ -511,35 +596,40 @@ export class Project {
511
596
  /**
512
597
  * Notify that a new document was opened in the editor.
513
598
  */
514
- onDidOpen(uri, languageID, version, content) {
599
+ async onDidOpen(uri, languageID, version, content) {
515
600
  uri = this.normalizeUri(uri);
516
601
  if (!fileUtil.isFileUri(uri)) {
517
602
  return; // We only accept `file:` scheme for client-managed URIs.
518
603
  }
519
- this.#clientManagedUris.add(uri);
520
604
  const doc = TextDocument.create(uri, languageID, version, content);
521
- const { node } = this.parseAndCache(doc);
605
+ const node = this.parse(doc);
606
+ this.#clientManagedUris.add(uri);
607
+ this.#clientManagedDocAndNodes.set(uri, { doc, node });
522
608
  if (this.#isReady) {
523
- this.check(doc, node);
609
+ await this.bind(doc, node);
610
+ await this.check(doc, node);
524
611
  }
525
612
  }
526
613
  /**
527
614
  * Notify that an existing document was changed in the editor.
528
615
  * @throws If there is no `TextDocument` corresponding to the URI.
529
616
  */
530
- onDidChange(uri, changes, version) {
617
+ async onDidChange(uri, changes, version) {
618
+ this.#symbolUpToDateUris.delete(uri);
531
619
  uri = this.normalizeUri(uri);
532
620
  if (!fileUtil.isFileUri(uri)) {
533
621
  return; // We only accept `file:` scheme for client-managed URIs.
534
622
  }
535
- const result = this.get(uri);
536
- if (!result) {
537
- throw new Error(`Document for “${uri}” is not cached. This should not happen. Did the language client send a didChange notification without sending a didOpen one?`);
623
+ const doc = this.#clientManagedDocAndNodes.get(uri)?.doc;
624
+ if (!doc) {
625
+ throw new Error(`TextDocument for “${uri}” is ${!doc ? 'not cached' : 'a Promise'}. This should not happen. Did the language client send a didChange notification without sending a didOpen one, or is there a logic error on our side resulting the 'read' function overriding the 'TextDocument' created in the 'didOpen' notification handler?`);
538
626
  }
539
- TextDocument.update(result.doc, changes, version);
540
- const { node } = this.parseAndCache(result.doc);
627
+ TextDocument.update(doc, changes, version);
628
+ const node = this.parse(doc);
629
+ this.#clientManagedDocAndNodes.set(uri, { doc, node });
541
630
  if (this.#isReady) {
542
- this.check(result.doc, node);
631
+ await this.bind(doc, node);
632
+ await this.check(doc, node);
543
633
  }
544
634
  }
545
635
  /**
@@ -551,8 +641,23 @@ export class Project {
551
641
  return; // We only accept `file:` scheme for client-managed URIs.
552
642
  }
553
643
  this.#clientManagedUris.delete(uri);
644
+ this.#clientManagedDocAndNodes.delete(uri);
554
645
  this.tryClearingCache(uri);
555
646
  }
647
+ async ensureClientManagedChecked(uri) {
648
+ const result = this.#clientManagedDocAndNodes.get(uri);
649
+ if (result && this.#isReady) {
650
+ const { doc, node } = result;
651
+ await this.bind(doc, node);
652
+ await this.check(doc, node);
653
+ this.emit('documentUpdated', { doc, node });
654
+ return { doc, node };
655
+ }
656
+ return undefined;
657
+ }
658
+ getClientManaged(uri) {
659
+ return this.#clientManagedDocAndNodes.get(uri);
660
+ }
556
661
  async showCacheRoot() {
557
662
  if (!this.#cacheRoot) {
558
663
  return;
@@ -566,7 +671,7 @@ export class Project {
566
671
  }
567
672
  tryClearingCache(uri) {
568
673
  if (this.shouldRemove(uri)) {
569
- this.#docAndNodes.delete(uri);
674
+ this.removeCachedTextDocument(uri);
570
675
  this.emit('documentRemoved', { uri });
571
676
  }
572
677
  }
@@ -578,18 +683,12 @@ export class Project {
578
683
  }
579
684
  }
580
685
  __decorate([
581
- CachePromise()
582
- ], Project.prototype, "ensureParsed", null);
686
+ SingletonPromise()
687
+ ], Project.prototype, "bind", null);
583
688
  __decorate([
584
- CachePromise()
689
+ SingletonPromise()
585
690
  ], Project.prototype, "check", null);
586
691
  __decorate([
587
- CachePromise()
588
- ], Project.prototype, "ensureChecked", null);
589
- __decorate([
590
- CachePromise()
591
- ], Project.prototype, "ensureParsedAndChecked", null);
592
- __decorate([
593
- CachePromise()
594
- ], Project.prototype, "ensureParsedAndCheckedOnlyWhenReady", null);
692
+ SingletonPromise()
693
+ ], Project.prototype, "ensureClientManagedChecked", null);
595
694
  //# sourceMappingURL=Project.js.map
@@ -1,4 +1,5 @@
1
1
  import type { TextDocument } from 'vscode-languageserver-textdocument';
2
+ import type { Logger } from '../common/index.js';
2
3
  import type { FileNode } from '../node/index.js';
3
4
  import { AstNode } from '../node/index.js';
4
5
  import type { Color, ColorInfo, ColorToken, InlayHint, SignatureHelp } from '../processor/index.js';
@@ -6,7 +7,6 @@ import { ColorPresentation } from '../processor/index.js';
6
7
  import { Range } from '../source/index.js';
7
8
  import type { SymbolUsageType } from '../symbol/index.js';
8
9
  import { Hover } from './Hover.js';
9
- import type { Logger } from './Logger.js';
10
10
  import { ProfilerFactory } from './Profiler.js';
11
11
  import type { ProjectOptions } from './Project.js';
12
12
  import { Project } from './Project.js';
@@ -0,0 +1,5 @@
1
+ import type { UriBinderContext } from './Context.js';
2
+ export declare type UriBinder = (uris: readonly string[], ctx: UriBinderContext) => void;
3
+ export declare type UriSorterRegistration = (this: void, a: string, b: string, next: UriSorter) => number;
4
+ export declare type UriSorter = (this: void, a: string, b: string) => number;
5
+ //# sourceMappingURL=UriProcessor.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=UriProcessor.js.map
@@ -7,12 +7,11 @@ export * from './ErrorReporter.js';
7
7
  export { FileService, UriProtocolSupporter } from './FileService.js';
8
8
  export * from './fileUtil.js';
9
9
  export * from './Hover.js';
10
- export * from './Logger.js';
11
10
  export * from './MetaRegistry.js';
12
- export * from './Operations.js';
13
11
  export * from './Profiler.js';
14
12
  export * from './Project.js';
15
13
  export * from './Service.js';
16
14
  export * from './SymbolLocations.js';
17
15
  export * from './SymbolRegistrar.js';
16
+ export * from './UriProcessor.js';
18
17
  //# sourceMappingURL=index.d.ts.map
@@ -8,12 +8,11 @@ export * from './ErrorReporter.js';
8
8
  export { FileService } from './FileService.js';
9
9
  export * from './fileUtil.js';
10
10
  export * from './Hover.js';
11
- export * from './Logger.js';
12
11
  export * from './MetaRegistry.js';
13
- export * from './Operations.js';
14
12
  export * from './Profiler.js';
15
13
  export * from './Project.js';
16
14
  export * from './Service.js';
17
15
  export * from './SymbolLocations.js';
18
16
  export * from './SymbolRegistrar.js';
17
+ export * from './UriProcessor.js';
19
18
  //# sourceMappingURL=index.js.map