@spyglassmc/core 0.4.43 → 0.4.44

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.
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import picomatch from 'picomatch';
8
8
  import { TextDocument } from 'vscode-languageserver-textdocument';
9
- import { bufferToString, Logger, normalizeUri, SingletonPromise, StateProxy, } from '../common/index.js';
9
+ import { bufferToString, Logger, normalizeUri, SingletonPromise, StateProxy, UriStore, } from '../common/index.js';
10
10
  import { FileNode } from '../node/index.js';
11
11
  import { file } from '../parser/index.js';
12
12
  import { traversePreOrder } from '../processor/index.js';
@@ -73,11 +73,13 @@ export class Project {
73
73
  #symbolUpToDateUris = new Set();
74
74
  #eventEmitter;
75
75
  #initializers;
76
+ #watcher;
77
+ get watchedFiles() {
78
+ return this.#watcher?.watchedFiles ?? new UriStore();
79
+ }
76
80
  #initPromise;
77
81
  #readyPromise;
78
- #watchedFiles = new Set();
79
- #watcher;
80
- #watcherReady = false;
82
+ #isInitialized = false;
81
83
  #isReady = false;
82
84
  get isReady() {
83
85
  return this.#isReady;
@@ -150,7 +152,7 @@ export class Project {
150
152
  * are not loaded into the memory.
151
153
  */
152
154
  getTrackedFiles() {
153
- const supportedFiles = [...this.#dependencyFiles ?? [], ...this.#watchedFiles];
155
+ const supportedFiles = [...this.#dependencyFiles ?? [], ...this.watchedFiles];
154
156
  this.logger.info(`[Project#getTrackedFiles] Listed ${supportedFiles.length} supported files`);
155
157
  return supportedFiles;
156
158
  }
@@ -171,12 +173,11 @@ export class Project {
171
173
  this.logger.info(`[Project] [init] cacheRoot = ${cacheRoot}`);
172
174
  this.logger.info(`[Project] [init] projectRoots = ${projectRoots.join(' ')}`);
173
175
  this.#configService.on('changed', ({ config }) => {
176
+ const oldConfig = this.config;
174
177
  this.config = config;
175
178
  this.logger.info('[Project] [Config] Changed');
176
- this.emit('configChanged', config);
179
+ this.emit('configChanged', { oldConfig, newConfig: config });
177
180
  }).on('error', ({ error, uri }) => this.logger.error(`[Project] [Config] Failed loading ${uri}`, error));
178
- this.setInitPromise();
179
- this.setReadyPromise();
180
181
  this.#cacheSaverIntervalId = setInterval(() => this.cacheService.save(), CacheAutoSaveInterval);
181
182
  this.on('documentUpdated', ({ doc, node }) => {
182
183
  // if (!this.#isReady) {
@@ -217,7 +218,14 @@ export class Project {
217
218
  Promise.all(promises).catch(e => this.logger.error('[Project#ready] Error occurred when rechecking client managed files after READY', e));
218
219
  });
219
220
  }
220
- setInitPromise() {
221
+ /**
222
+ * Load the config file and initialize parsers and processors.
223
+ */
224
+ async init() {
225
+ return (this.#initPromise ??= this.#init());
226
+ }
227
+ async #init() {
228
+ this.#isInitialized = false;
221
229
  const callIntializers = async () => {
222
230
  const initCtx = {
223
231
  cacheRoot: this.cacheRoot,
@@ -240,20 +248,30 @@ export class Project {
240
248
  });
241
249
  this.#ctx = ctx;
242
250
  };
243
- const init = async () => {
244
- const __profiler = this.profilers.get('project#init');
245
- const { symbols } = await this.cacheService.load();
246
- this.symbols = new SymbolUtil(symbols, this.externals.event.EventEmitter);
247
- this.symbols.buildCache();
248
- __profiler.task('Load Cache');
249
- this.config = await this.#configService.load();
250
- __profiler.task('Load Config');
251
- await callIntializers();
252
- __profiler.task('Initialize').finalize();
253
- };
254
- this.#initPromise = init();
251
+ const __profiler = this.profilers.get('project#init');
252
+ const { symbols } = await this.cacheService.load();
253
+ this.symbols = new SymbolUtil(symbols, this.externals.event.EventEmitter);
254
+ this.symbols.buildCache();
255
+ __profiler.task('Load Cache');
256
+ this.config = await this.#configService.load();
257
+ __profiler.task('Load Config');
258
+ await callIntializers();
259
+ __profiler.task('Initialize').finalize();
260
+ this.#isInitialized = true;
261
+ return this;
262
+ }
263
+ /**
264
+ * Finish the initial run of parsing, binding, and checking the entire project.
265
+ */
266
+ async ready(options = {}) {
267
+ return (this.#readyPromise ??= this.#ready(options));
255
268
  }
256
- setReadyPromise() {
269
+ async #ready({ projectRootsWatcher } = {}) {
270
+ if (!this.#isInitialized) {
271
+ throw new Error('Project.ready() must be called after Project.init() resolves');
272
+ }
273
+ this.#isReady = false;
274
+ this.#watcher = projectRootsWatcher;
257
275
  const getDependencies = async () => {
258
276
  const dependencies = [];
259
277
  for (const input of this.config.env.dependencies) {
@@ -292,119 +310,92 @@ export class Project {
292
310
  this.fs.register('file:', fileUriSupporter, true);
293
311
  this.fs.register(ArchiveUriSupporter.Protocol, archiveUriSupporter, true);
294
312
  };
295
- const listProjectFiles = () => new Promise((resolve) => {
296
- if (this.projectRoots.length === 0) {
297
- resolve();
313
+ const listProjectFiles = async () => {
314
+ if (!this.#watcher) {
298
315
  return;
299
316
  }
300
- this.#watchedFiles.clear();
301
- this.#watcherReady = false;
302
- this.#watcher = this.externals.fs.watch(this.projectRoots, {
303
- usePolling: this.config.env.useFilePolling,
304
- }).once('ready', () => {
305
- this.#watcherReady = true;
306
- resolve();
307
- }).on('add', (uri) => {
308
- if (this.shouldExclude(uri)) {
309
- return;
310
- }
311
- this.#watchedFiles.add(uri);
312
- if (this.#watcherReady) {
313
- this.emit('fileCreated', { uri });
314
- }
315
- }).on('change', (uri) => {
317
+ this.#watcher
318
+ .on('add', (uri) => {
316
319
  if (this.shouldExclude(uri)) {
317
320
  return;
318
321
  }
319
- if (this.#watcherReady) {
320
- this.emit('fileModified', { uri });
321
- }
322
- }).on('unlink', (uri) => {
322
+ this.emit('fileCreated', { uri });
323
+ })
324
+ .on('change', (uri) => {
323
325
  if (this.shouldExclude(uri)) {
324
326
  return;
325
327
  }
326
- this.#watchedFiles.delete(uri);
327
- if (this.#watcherReady) {
328
- this.emit('fileDeleted', { uri });
329
- }
330
- }).on('error', (e) => {
331
- this.logger.error('[Project] [chokidar]', e);
332
- });
333
- });
334
- const ready = async () => {
335
- await this.init();
336
- const __profiler = this.profilers.get('project#ready');
337
- await Promise.all([listDependencyFiles(), listProjectFiles()]);
338
- this.#dependencyFiles = new Set([...this.fs.listFiles()]
339
- .filter((uri) => !this.shouldExclude(uri)));
340
- this.#dependencyRoots = new Set(this.fs.listRoots());
341
- this.updateRoots();
342
- __profiler.task('List URIs');
343
- for (const [id, { checksum, registrar }] of this.meta.symbolRegistrars) {
344
- const cacheChecksum = this.cacheService.checksums.symbolRegistrars[id];
345
- if (cacheChecksum === undefined || checksum !== cacheChecksum) {
346
- this.symbols.clear({ contributor: `symbol_registrar/${id}` });
347
- this.symbols.contributeAs(`symbol_registrar/${id}`, () => {
348
- registrar(this.symbols, { logger: this.logger });
349
- });
350
- this.emit('symbolRegistrarExecuted', { id, checksum });
351
- }
352
- else {
353
- this.logger.info(`[SymbolRegistrar] Skipped “${id}” thanks to cache ${checksum}`);
354
- }
355
- }
356
- __profiler.task('Register Symbols');
357
- for (const [uri, values] of Object.entries(this.cacheService.errors)) {
358
- this.emit('documentErrored', { errors: values, uri });
359
- }
360
- __profiler.task('Pop Errors');
361
- const { addedFiles, changedFiles, removedFiles } = await this.cacheService.validate();
362
- for (const uri of removedFiles) {
328
+ this.emit('fileModified', { uri });
329
+ })
330
+ .on('unlink', (uri) => {
331
+ // No `this.shouldExclude(uri)` check here as `unlink` events may be sent for
332
+ // hot-reload file exclusions. We want to be able to clean up the symbols for these
333
+ // excluded files.
363
334
  this.emit('fileDeleted', { uri });
335
+ })
336
+ .on('error', (e) => {
337
+ this.logger.error('[Project#watcher]', e);
338
+ });
339
+ await this.#watcher.ready();
340
+ };
341
+ const __profiler = this.profilers.get('project#ready');
342
+ await Promise.all([listDependencyFiles(), listProjectFiles()]);
343
+ this.#dependencyFiles = new Set([...this.fs.listFiles()]
344
+ .filter((uri) => !this.shouldExclude(uri)));
345
+ this.#dependencyRoots = new Set(this.fs.listRoots());
346
+ this.updateRoots();
347
+ __profiler.task('List URIs');
348
+ for (const [id, { checksum, registrar }] of this.meta.symbolRegistrars) {
349
+ const cacheChecksum = this.cacheService.checksums.symbolRegistrars[id];
350
+ if (cacheChecksum === undefined || checksum !== cacheChecksum) {
351
+ this.symbols.clear({ contributor: `symbol_registrar/${id}` });
352
+ this.symbols.contributeAs(`symbol_registrar/${id}`, () => {
353
+ registrar(this.symbols, { logger: this.logger });
354
+ });
355
+ this.emit('symbolRegistrarExecuted', { id, checksum });
364
356
  }
365
- __profiler.task('Validate Cache');
366
- if (addedFiles.length > 0) {
367
- this.bindUri(addedFiles);
368
- }
369
- __profiler.task('Bind URIs');
370
- const files = [...addedFiles, ...changedFiles].sort(this.meta.uriSorter);
371
- __profiler.task('Sort URIs');
372
- const fileCountByExtension = new Map();
373
- for (const file of files) {
374
- const ext = fileUtil.extname(file)?.replace(/^\./, '');
375
- if (ext) {
376
- fileCountByExtension.set(ext, (fileCountByExtension.get(ext) ?? 0) + 1);
377
- }
378
- }
379
- this.logger.info(`[Project#ready] == Files to bind ==`);
380
- for (const [ext, count] of fileCountByExtension.entries()) {
381
- this.logger.info(`[Project#ready] File extension ${ext}: ${count}`);
357
+ else {
358
+ this.logger.info(`[SymbolRegistrar] Skipped “${id}” thanks to cache ${checksum}`);
382
359
  }
383
- const __bindProfiler = this.profilers.get('project#ready#bind', 'top-n', 50);
384
- for (const uri of files) {
385
- await this.ensureBindingStarted(uri);
386
- __bindProfiler.task(uri);
360
+ }
361
+ __profiler.task('Register Symbols');
362
+ for (const [uri, values] of Object.entries(this.cacheService.errors)) {
363
+ this.emit('documentErrored', { errors: values, uri });
364
+ }
365
+ __profiler.task('Pop Errors');
366
+ const { addedFiles, changedFiles, removedFiles } = await this.cacheService.validate();
367
+ this.logger.info(`[Project#ready] Files added/changed/removed: ${addedFiles.length}/${changedFiles.length}/${removedFiles.length}`);
368
+ for (const uri of removedFiles) {
369
+ this.emit('fileDeleted', { uri });
370
+ }
371
+ __profiler.task('Validate Cache');
372
+ if (addedFiles.length > 0) {
373
+ this.bindUri(addedFiles);
374
+ }
375
+ __profiler.task('Bind URIs');
376
+ const files = [...addedFiles, ...changedFiles].sort(this.meta.uriSorter);
377
+ __profiler.task('Sort URIs');
378
+ const fileCountByExtension = new Map();
379
+ for (const file of files) {
380
+ const ext = fileUtil.extname(file)?.replace(/^\./, '');
381
+ if (ext) {
382
+ fileCountByExtension.set(ext, (fileCountByExtension.get(ext) ?? 0) + 1);
387
383
  }
388
- __bindProfiler.finalize();
389
- __profiler.task('Bind Files');
390
- __profiler.finalize();
391
- this.emit('ready', {});
392
- };
393
- this.#isReady = false;
394
- this.#readyPromise = ready();
395
- }
396
- /**
397
- * Load the config file and initialize parsers and processors.
398
- */
399
- async init() {
400
- await this.#initPromise;
401
- return this;
402
- }
403
- /**
404
- * Finish the initial run of parsing, binding, and checking the entire project.
405
- */
406
- async ready() {
407
- await this.#readyPromise;
384
+ }
385
+ this.logger.info(`[Project#ready] == Files to bind ==`);
386
+ for (const [ext, count] of fileCountByExtension.entries()) {
387
+ this.logger.info(`[Project#ready] File extension ${ext}: ${count}`);
388
+ }
389
+ const __bindProfiler = this.profilers.get('project#ready#bind', 'top-n', 50);
390
+ for (const uri of files) {
391
+ await this.ensureBindingStarted(uri);
392
+ __bindProfiler.task(uri);
393
+ }
394
+ __bindProfiler.finalize();
395
+ __profiler.task('Bind Files');
396
+ __profiler.finalize();
397
+ this.emit('ready', {});
398
+ this.#isReady = true;
408
399
  return this;
409
400
  }
410
401
  /**
@@ -412,15 +403,14 @@ export class Project {
412
403
  */
413
404
  async close() {
414
405
  clearInterval(this.#cacheSaverIntervalId);
415
- await this.#watcher.close();
406
+ await this.#watcher?.close();
416
407
  await this.cacheService.save();
417
408
  }
418
409
  async restart() {
419
410
  try {
420
- await this.#watcher.close();
421
411
  this.#bindingInProgressUris.clear();
422
412
  this.#symbolUpToDateUris.clear();
423
- this.setReadyPromise();
413
+ this.#readyPromise = undefined;
424
414
  await this.ready();
425
415
  }
426
416
  catch (e) {
@@ -770,13 +760,16 @@ export class Project {
770
760
  shouldRemove(uri) {
771
761
  return (!this.#clientManagedUris.has(uri)
772
762
  && !this.#dependencyFiles?.has(uri)
773
- && !this.#watchedFiles.has(uri));
763
+ && !this.watchedFiles.has(uri));
774
764
  }
775
765
  isOnlyWatched(uri) {
776
- return (this.#watchedFiles.has(uri)
766
+ return (this.watchedFiles.has(uri)
777
767
  && !this.#clientManagedUris.has(uri)
778
768
  && !this.#dependencyFiles?.has(uri));
779
769
  }
770
+ async onEditorConfigurationUpdate(editorConfiguration) {
771
+ await this.#configService.onEditorConfigurationUpdate(editorConfiguration);
772
+ }
780
773
  }
781
774
  __decorate([
782
775
  SingletonPromise()
@@ -28,7 +28,7 @@ export declare class Service {
28
28
  getCodeActions(node: FileNode<AstNode>, doc: TextDocument, range: Range): readonly CodeAction[];
29
29
  getColorInfo(node: FileNode<AstNode>, doc: TextDocument): ColorInfo[];
30
30
  getColorPresentation(file: FileNode<AstNode>, doc: TextDocument, range: Range, color: Color): ColorPresentation[];
31
- complete(node: FileNode<AstNode>, doc: TextDocument, offset: number, triggerCharacter?: string): import("../index.js").CompletionItem[];
31
+ complete(node: FileNode<AstNode>, doc: TextDocument, offset: number, triggerCharacter?: string): import("../processor/index.js").CompletionItem[];
32
32
  dataHackPubify(initialism: string): string;
33
33
  format(node: FileNode<AstNode>, doc: TextDocument, tabSize: number, insertSpaces: boolean): string;
34
34
  getHover(file: FileNode<AstNode>, doc: TextDocument, offset: number): Hover | undefined;
@@ -1,4 +1,5 @@
1
1
  import type { Externals, FsLocation } from '../common/index.js';
2
+ import { Uri } from '../common/index.js';
2
3
  export type RootUriString = `${string}/`;
3
4
  export type FileExtension = `.${string}`;
4
5
  export declare namespace fileUtil {
@@ -35,6 +36,7 @@ export declare namespace fileUtil {
35
36
  function getRoot(uri: string, rootUris: readonly RootUriString[]): string | undefined;
36
37
  function isRootUri(uri: string): uri is RootUriString;
37
38
  function ensureEndingSlash(uri: string): RootUriString;
39
+ function trimEndingSlash(uri: string): string;
38
40
  function join(fromUri: string, toUri: string): string;
39
41
  function isFileUri(uri: string): boolean;
40
42
  /**
@@ -49,7 +51,7 @@ export declare namespace fileUtil {
49
51
  * @returns The part from the beginning of the URI to the last `/`.
50
52
  */
51
53
  function dirname(uri: string): string;
52
- function getParentOfFile(externals: Externals, path: FsLocation): FsLocation;
54
+ function getParentOfUri(uri: FsLocation): Uri;
53
55
  /**
54
56
  * @throws
55
57
  *
@@ -82,6 +82,10 @@ export var fileUtil;
82
82
  return isRootUri(uri) ? uri : `${uri}/`;
83
83
  }
84
84
  fileUtil.ensureEndingSlash = ensureEndingSlash;
85
+ function trimEndingSlash(uri) {
86
+ return isRootUri(uri) ? uri.slice(0, -1) : uri;
87
+ }
88
+ fileUtil.trimEndingSlash = trimEndingSlash;
85
89
  function join(fromUri, toUri) {
86
90
  return (ensureEndingSlash(fromUri) + (toUri.startsWith('/') ? toUri.slice(1) : toUri));
87
91
  }
@@ -115,10 +119,10 @@ export var fileUtil;
115
119
  }
116
120
  fileUtil.dirname = dirname;
117
121
  /* istanbul ignore next */
118
- function getParentOfFile(externals, path) {
119
- return new Uri('.', path);
122
+ function getParentOfUri(uri) {
123
+ return new Uri('.', trimEndingSlash(uri.toString()));
120
124
  }
121
- fileUtil.getParentOfFile = getParentOfFile;
125
+ fileUtil.getParentOfUri = getParentOfUri;
122
126
  /* istanbul ignore next */
123
127
  /**
124
128
  * @throws
@@ -145,7 +149,7 @@ export var fileUtil;
145
149
  * @param mode Default to `0o777` (`rwxrwxrwx`)
146
150
  */
147
151
  async function ensureParentOfFile(externals, path, mode = 0o777) {
148
- return ensureDir(externals, getParentOfFile(externals, path), mode);
152
+ return ensureDir(externals, getParentOfUri(path), mode);
149
153
  }
150
154
  fileUtil.ensureParentOfFile = ensureParentOfFile;
151
155
  async function chmod(externals, path, mode) {
@@ -6,6 +6,7 @@ export * from './ErrorReporter.js';
6
6
  export * from './fetcher.js';
7
7
  export { FileService, UriProtocolSupporter } from './FileService.js';
8
8
  export * from './fileUtil.js';
9
+ export * from './FileWatcher.js';
9
10
  export * from './Hover.js';
10
11
  export * from './MetaRegistry.js';
11
12
  export * from './Profiler.js';
@@ -7,6 +7,7 @@ export * from './ErrorReporter.js';
7
7
  export * from './fetcher.js';
8
8
  export { FileService } from './FileService.js';
9
9
  export * from './fileUtil.js';
10
+ export * from './FileWatcher.js';
10
11
  export * from './Hover.js';
11
12
  export * from './MetaRegistry.js';
12
13
  export * from './Profiler.js';