@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
@@ -1,7 +1,6 @@
1
- import type { Externals } from '../common/index.js';
1
+ import type { Externals, Logger } from '../common/index.js';
2
2
  import type { Dependency } from './Dependency.js';
3
3
  import type { RootUriString } from './fileUtil.js';
4
- import type { Logger } from './Logger.js';
5
4
  export interface UriProtocolSupporter {
6
5
  /**
7
6
  * @throws
@@ -1,15 +1,15 @@
1
+ import type { Logger } from '../common/index.js';
1
2
  import { Lazy } from '../common/index.js';
2
3
  import type { AstNode } from '../node/index.js';
3
4
  import type { Parser } from '../parser/index.js';
4
5
  import type { Formatter } from '../processor/formatter/index.js';
5
- import type { Checker, Colorizer, Completer, InlayHintProvider } from '../processor/index.js';
6
+ import type { Binder, Checker, Colorizer, Completer, InlayHintProvider } from '../processor/index.js';
6
7
  import type { Linter } from '../processor/linter/Linter.js';
7
8
  import type { SignatureHelpProvider } from '../processor/SignatureHelpProvider.js';
8
- import type { UriBinder } from '../symbol/index.js';
9
9
  import type { DependencyKey, DependencyProvider } from './Dependency.js';
10
10
  import type { FileExtension } from './fileUtil.js';
11
- import type { Logger } from './Logger.js';
12
11
  import type { SymbolRegistrar } from './SymbolRegistrar.js';
12
+ import type { UriBinder, UriSorter, UriSorterRegistration } from './UriProcessor.js';
13
13
  export interface LanguageOptions {
14
14
  /**
15
15
  * An array of extensions of files corresponding to the language. Each extension should include the leading dot (`.`).
@@ -58,6 +58,9 @@ export declare class MetaRegistry {
58
58
  * @returns The language ID registered for the file extension, or `undefined`.
59
59
  */
60
60
  getLanguageID(fileExtension: FileExtension): string | undefined;
61
+ hasBinder<N extends AstNode>(type: N['type']): boolean;
62
+ getBinder<N extends AstNode>(type: N['type']): Binder<N>;
63
+ registerBinder<N extends AstNode>(type: N['type'], binder: Binder<N>): void;
61
64
  hasChecker<N extends AstNode>(type: N['type']): boolean;
62
65
  getChecker<N extends AstNode>(type: N['type']): Checker<N>;
63
66
  registerChecker<N extends AstNode>(type: N['type'], checker: Checker<N>): void;
@@ -101,6 +104,8 @@ export declare class MetaRegistry {
101
104
  get symbolRegistrars(): Map<string, SymbolRegistrarRegistration>;
102
105
  registerUriBinder(uriBinder: UriBinder): void;
103
106
  get uriBinders(): Set<UriBinder>;
107
+ setUriSorter(uriSorter: UriSorterRegistration): void;
108
+ get uriSorter(): UriSorter;
104
109
  }
105
110
  export {};
106
111
  //# sourceMappingURL=MetaRegistry.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { Lazy } from '../common/index.js';
2
- import { checker, colorizer, completer, formatter, linter } from '../processor/index.js';
2
+ import { binder, checker, colorizer, completer, formatter, linter } from '../processor/index.js';
3
3
  /* istanbul ignore next */
4
4
  /**
5
5
  * The meta registry of Spyglass. You can register new parsers, processors, and languages here.
@@ -9,6 +9,7 @@ export class MetaRegistry {
9
9
  * A map from language IDs to language options.
10
10
  */
11
11
  #languages = new Map();
12
+ #binders = new Map();
12
13
  #checkers = new Map();
13
14
  #colorizers = new Map();
14
15
  #completers = new Map();
@@ -20,7 +21,9 @@ export class MetaRegistry {
20
21
  #signatureHelpProviders = new Set();
21
22
  #symbolRegistrars = new Map();
22
23
  #uriBinders = new Set();
24
+ #uriSorter = () => 0;
23
25
  constructor() {
26
+ binder.registerBinders(this);
24
27
  checker.registerCheckers(this);
25
28
  colorizer.registerColorizers(this);
26
29
  completer.registerCompleters(this);
@@ -68,6 +71,15 @@ export class MetaRegistry {
68
71
  }
69
72
  return undefined;
70
73
  }
74
+ hasBinder(type) {
75
+ return this.#binders.has(type);
76
+ }
77
+ getBinder(type) {
78
+ return this.#binders.get(type) ?? binder.fallback;
79
+ }
80
+ registerBinder(type, binder) {
81
+ this.#binders.set(type, binder);
82
+ }
71
83
  hasChecker(type) {
72
84
  return this.#checkers.has(type);
73
85
  }
@@ -177,5 +189,12 @@ export class MetaRegistry {
177
189
  get uriBinders() {
178
190
  return this.#uriBinders;
179
191
  }
192
+ setUriSorter(uriSorter) {
193
+ const nextSorter = this.#uriSorter;
194
+ this.#uriSorter = (a, b) => uriSorter(a, b, nextSorter);
195
+ }
196
+ get uriSorter() {
197
+ return this.#uriSorter;
198
+ }
180
199
  }
181
200
  //# sourceMappingURL=MetaRegistry.js.map
@@ -1,4 +1,4 @@
1
- import { Logger } from './Logger.js';
1
+ import { Logger } from '../common/index.js';
2
2
  /**
3
3
  * @example
4
4
  * ```typescript
@@ -27,7 +27,8 @@ export declare class ProfilerFactory {
27
27
  #private;
28
28
  private readonly logger;
29
29
  constructor(logger: Logger, enabledProfilers: string[]);
30
- get(id: string): Profiler;
30
+ get(id: string, style: 'top-n', n: number): Profiler;
31
+ get(id: string, style?: 'total'): Profiler;
31
32
  static noop(): ProfilerFactory;
32
33
  }
33
34
  //# sourceMappingURL=Profiler.d.ts.map
@@ -1,6 +1,53 @@
1
- import { Logger } from './Logger.js';
1
+ import { Dev, Logger } from '../common/index.js';
2
+ class TopNImpl {
3
+ id;
4
+ logger;
5
+ n;
6
+ #finalized = false;
7
+ #startTime;
8
+ #lastTime;
9
+ #taskCount = 0;
10
+ #topTasks = [];
11
+ #minTime = Infinity;
12
+ #maxTime = 0;
13
+ constructor(id, logger, n) {
14
+ this.id = id;
15
+ this.logger = logger;
16
+ this.n = n;
17
+ this.#startTime = this.#lastTime = performance.now();
18
+ }
19
+ task(name) {
20
+ if (this.#finalized) {
21
+ throw new Error('The profiler has already been finalized');
22
+ }
23
+ this.#taskCount++;
24
+ const time = performance.now();
25
+ const duration = time - this.#lastTime;
26
+ this.#lastTime = time;
27
+ this.#minTime = Math.min(this.#minTime, duration);
28
+ this.#maxTime = Math.max(this.#maxTime, duration);
29
+ this.#topTasks.push([name, duration]);
30
+ this.#topTasks.sort((a, b) => b[1] - a[1]);
31
+ if (this.#topTasks.length > this.n) {
32
+ this.#topTasks = this.#topTasks.slice(0, -1);
33
+ }
34
+ return this;
35
+ }
36
+ finalize() {
37
+ this.#finalized = true;
38
+ const longestTaskNameLength = this.#topTasks.reduce((length, [name]) => Math.max(length, name.length), 0);
39
+ const totalDuration = this.#lastTime - this.#startTime;
40
+ this.logger.info(`[Profiler: ${this.id}] == Summary ==`);
41
+ this.logger.info(`[Profiler: ${this.id}] Total tasks: ${this.#taskCount} done in ${totalDuration} ms`);
42
+ this.logger.info(`[Profiler: ${this.id}] Min/Avg/Max: ${this.#minTime} / ${totalDuration / this.#taskCount} / ${this.#maxTime} ms`);
43
+ this.logger.info(`[Profiler: ${this.id}] Top ${Math.min(this.n, this.#topTasks.length)} task(s):`);
44
+ for (const [name, time] of this.#topTasks) {
45
+ this.logger.info(`[Profiler: ${this.id}] ${name}${' '.repeat(longestTaskNameLength - name.length)} - ${time} ms (${time / totalDuration * 100}%)`);
46
+ }
47
+ }
48
+ }
2
49
  const TotalTaskName = 'Total';
3
- class ProfilerImpl {
50
+ class TotalImpl {
4
51
  id;
5
52
  logger;
6
53
  #finalized = false;
@@ -18,10 +65,11 @@ class ProfilerImpl {
18
65
  throw new Error('The profiler is finalized.');
19
66
  }
20
67
  const time = performance.now();
21
- this.#tasks.push([name, time - this.#lastTime]);
68
+ const duration = time - this.#lastTime;
22
69
  this.#lastTime = time;
70
+ this.#tasks.push([name, duration]);
23
71
  this.#longestTaskNameLength = Math.max(this.#longestTaskNameLength, name.length);
24
- this.logger.info(`[Profiler: ${this.id}] Done: ${name}`);
72
+ this.logger.info(`[Profiler: ${this.id}] Done: ${name} in ${duration} ms`);
25
73
  return this;
26
74
  }
27
75
  finalize() {
@@ -45,9 +93,13 @@ export class ProfilerFactory {
45
93
  this.logger = logger;
46
94
  this.#enabledProfilers = new Set(enabledProfilers);
47
95
  }
48
- get(id) {
96
+ get(id, style = 'total', n) {
49
97
  if (this.#enabledProfilers.has(id)) {
50
- return new ProfilerImpl(id, this.logger);
98
+ switch (style) {
99
+ case 'top-n': return new TopNImpl(id, this.logger, n);
100
+ case 'total': return new TotalImpl(id, this.logger);
101
+ default: return Dev.assertNever(style);
102
+ }
51
103
  }
52
104
  else {
53
105
  return new NoopImpl();
@@ -1,6 +1,7 @@
1
1
  import type { TextDocumentContentChangeEvent } from 'vscode-languageserver-textdocument';
2
2
  import { TextDocument } from 'vscode-languageserver-textdocument';
3
3
  import type { ExternalEventEmitter, Externals } from '../common/index.js';
4
+ import { Logger } from '../common/index.js';
4
5
  import type { AstNode } from '../node/index.js';
5
6
  import { FileNode } from '../node/index.js';
6
7
  import type { LanguageError } from '../source/index.js';
@@ -10,11 +11,12 @@ import type { Config } from './Config.js';
10
11
  import { Downloader } from './Downloader.js';
11
12
  import { FileService } from './FileService.js';
12
13
  import type { RootUriString } from './fileUtil.js';
13
- import { Logger } from './Logger.js';
14
14
  import { MetaRegistry } from './MetaRegistry.js';
15
15
  import { ProfilerFactory } from './Profiler.js';
16
16
  export declare type ProjectInitializerContext = Pick<Project, 'cacheRoot' | 'config' | 'downloader' | 'externals' | 'logger' | 'meta' | 'projectRoot'>;
17
- export declare type ProjectInitializer = (this: void, ctx: ProjectInitializerContext) => PromiseLike<Record<string, string> | void> | Record<string, string> | void;
17
+ export declare type SyncProjectInitializer = (this: void, ctx: ProjectInitializerContext) => Record<string, string> | void;
18
+ export declare type AsyncProjectInitializer = (this: void, ctx: ProjectInitializerContext) => PromiseLike<Record<string, string> | void>;
19
+ export declare type ProjectInitializer = SyncProjectInitializer | AsyncProjectInitializer;
18
20
  export interface ProjectOptions {
19
21
  cacheRoot: RootUriString;
20
22
  defaultConfig?: Config;
@@ -51,9 +53,43 @@ interface SymbolRegistrarEvent {
51
53
  id: string;
52
54
  checksum: string | undefined;
53
55
  }
54
- export declare type ProjectData = Pick<Project, 'cacheRoot' | 'config' | 'downloader' | 'ensureParsedAndChecked' | 'externals' | 'fs' | 'get' | 'logger' | 'meta' | 'profilers' | 'projectRoot' | 'roots' | 'symbols' | 'ctx'>;
56
+ export declare type ProjectData = Pick<Project, 'cacheRoot' | 'config' | 'downloader' | 'ensureBindingStarted' | 'externals' | 'fs' | 'logger' | 'meta' | 'profilers' | 'projectRoot' | 'roots' | 'symbols' | 'ctx'>;
55
57
  /**
56
58
  * Manage all tracked documents and errors.
59
+ *
60
+ * The four stages of processing a document:
61
+ * 1. `read` - read the file from the external file system as a `TextDocument`.
62
+ * 2. `parse` - Parse the `TextDocument` into an `AstNode`.
63
+ * 3. `bind` - Bind the `AstNode` and populate both the global symbol table and the local symbol tables on the nodes.
64
+ * 4. `check` (includes `lint`) - Check the `AstNode` with information from the symbol tables.
65
+ *
66
+ * **Caching**
67
+ *
68
+ * The global symbol table along with a list of file URIs and checksums is cached in memory and is periodically saved to disk.
69
+ *
70
+ * 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.
71
+ *
72
+ * Some `TextDocument`s may be cached to avoid excessive reading from the file system.
73
+ *
74
+ * **INIT and READY**
75
+ *
76
+ * When a new instance of the {@link Project} class is constructed, its INIT and READY processes are immediately started in serial.
77
+ *
78
+ * During the INIT process of the project, the config and language feature initialization are processed.
79
+ * The Promise returned by the {@link init} function resolves when the INIT process is complete.
80
+ *
81
+ * During the READY process of the project, the whole project is analyzed mainly to populate the global symbol table.
82
+ * The Promise returned by the {@link ready} function resolves when the READY process is complete.
83
+ *
84
+ * The following generally happens during the READY process:
85
+ * 1. A list of file URIs under the project is obtained.
86
+ * 2. The global symbol cache, if available, is loaded and validated against the know list of files.
87
+ * A list of files that need to be (re)processed is returned in this step.
88
+ * 3. For each files in the new list, the file is read, parsed, bound, and checked.
89
+ *
90
+ * **EDITING**
91
+ *
92
+ * 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.
57
93
  */
58
94
  export declare class Project implements ExternalEventEmitter {
59
95
  #private;
@@ -119,7 +155,13 @@ export declare class Project implements ExternalEventEmitter {
119
155
  constructor({ cacheRoot, defaultConfig, downloader, externals, fs, initializers, logger, profilers, projectRoot, }: ProjectOptions);
120
156
  private setInitPromise;
121
157
  private setReadyPromise;
158
+ /**
159
+ * Load the config file and initialize parsers and processors.
160
+ */
122
161
  init(): Promise<this>;
162
+ /**
163
+ * Finish the initial run of parsing, binding, and checking the entire project.
164
+ */
123
165
  ready(): Promise<this>;
124
166
  /**
125
167
  * Behavior of the `Project` instance is undefined after this function has settled.
@@ -128,41 +170,30 @@ export declare class Project implements ExternalEventEmitter {
128
170
  restart(): Promise<void>;
129
171
  resetCache(): void;
130
172
  normalizeUri(uri: string): string;
131
- /**
132
- * @returns The language ID of the file, or the file extension without the leading dot.
133
- */
134
- private getLanguageID;
135
- /**
136
- * @returns The cached `TextDocument` and `AstNode` for the URI, or `undefined` when such data isn't available in cache.
137
- */
138
- get(uri: string): DocAndNode | undefined;
139
- /**
140
- * @throws FS-related errors
141
- */
142
- ensureParsed(uri: string): Promise<DocAndNode | undefined>;
143
- private parseAndCache;
173
+ private static readonly TextDocumentCacheMaxLength;
174
+ private removeCachedTextDocument;
175
+ private read;
144
176
  private parse;
145
- private cache;
177
+ private bind;
146
178
  private check;
147
- ensureChecked(doc: TextDocument, node: FileNode<AstNode>): Promise<void>;
148
179
  private lint;
149
- ensureLinted(doc: TextDocument, node: FileNode<AstNode>): void;
150
- ensureParsedAndChecked(uri: string): Promise<DocAndNode | undefined>;
151
- ensureParsedAndCheckedOnlyWhenReady(uri: string): Promise<DocAndNode | undefined>;
152
- private bind;
180
+ ensureBindingStarted(uri: string): Promise<void>;
181
+ private bindUri;
153
182
  /**
154
183
  * Notify that a new document was opened in the editor.
155
184
  */
156
- onDidOpen(uri: string, languageID: string, version: number, content: string): void;
185
+ onDidOpen(uri: string, languageID: string, version: number, content: string): Promise<void>;
157
186
  /**
158
187
  * Notify that an existing document was changed in the editor.
159
188
  * @throws If there is no `TextDocument` corresponding to the URI.
160
189
  */
161
- onDidChange(uri: string, changes: TextDocumentContentChangeEvent[], version: number): void;
190
+ onDidChange(uri: string, changes: TextDocumentContentChangeEvent[], version: number): Promise<void>;
162
191
  /**
163
192
  * Notify that an existing document was closed in the editor.
164
193
  */
165
194
  onDidClose(uri: string): void;
195
+ ensureClientManagedChecked(uri: string): Promise<DocAndNode | undefined>;
196
+ getClientManaged(uri: string): DocAndNode | undefined;
166
197
  showCacheRoot(): Promise<void>;
167
198
  private tryClearingCache;
168
199
  private shouldRemove;