@hydranium/core 1.0.0-next.259 → 1.0.0-next.260

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 (67) hide show
  1. package/lib/langium/bootstrap.d.ts +11 -9
  2. package/lib/langium/bootstrap.d.ts.map +1 -1
  3. package/lib/langium/bootstrap.js +11 -9
  4. package/lib/langium/bootstrap.js.map +1 -1
  5. package/lib/langium/build-phase-pass/build-phase-pass-service.d.ts +1 -1
  6. package/lib/langium/build-phase-pass/build-phase-pass.d.ts +1 -1
  7. package/lib/langium/diagnostics/hydranium-langium-profiler.d.ts +9 -20
  8. package/lib/langium/diagnostics/hydranium-langium-profiler.d.ts.map +1 -1
  9. package/lib/langium/diagnostics/hydranium-langium-profiler.js +15 -24
  10. package/lib/langium/diagnostics/hydranium-langium-profiler.js.map +1 -1
  11. package/lib/langium/document-builder/document-builder.d.ts +22 -10
  12. package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
  13. package/lib/langium/document-builder/document-builder.js +25 -17
  14. package/lib/langium/document-builder/document-builder.js.map +1 -1
  15. package/lib/langium/integrity/integrity-service.d.ts +1 -3
  16. package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
  17. package/lib/langium/integrity/integrity-service.js +1 -3
  18. package/lib/langium/integrity/integrity-service.js.map +1 -1
  19. package/lib/langium/module.d.ts +1 -1
  20. package/lib/langium/module.js +1 -1
  21. package/lib/langium/module.js.map +1 -1
  22. package/lib/langium/scope/fallback-scope.d.ts +27 -0
  23. package/lib/langium/scope/fallback-scope.d.ts.map +1 -0
  24. package/lib/langium/scope/fallback-scope.js +35 -0
  25. package/lib/langium/scope/fallback-scope.js.map +1 -0
  26. package/lib/langium/scope/hydranium-scope-provider.d.ts +10 -9
  27. package/lib/langium/scope/hydranium-scope-provider.d.ts.map +1 -1
  28. package/lib/langium/scope/hydranium-scope-provider.js +15 -13
  29. package/lib/langium/scope/hydranium-scope-provider.js.map +1 -1
  30. package/lib/langium/scope/index.d.ts +1 -0
  31. package/lib/langium/scope/index.d.ts.map +1 -1
  32. package/lib/langium/scope/index.js +1 -0
  33. package/lib/langium/scope/index.js.map +1 -1
  34. package/lib/langium/scope/scope-extension-service.d.ts +41 -37
  35. package/lib/langium/scope/scope-extension-service.d.ts.map +1 -1
  36. package/lib/langium/scope/scope-extension-service.js +57 -29
  37. package/lib/langium/scope/scope-extension-service.js.map +1 -1
  38. package/lib/langium/shared-services.d.ts +2 -0
  39. package/lib/langium/shared-services.d.ts.map +1 -1
  40. package/lib/langium/shared-services.js.map +1 -1
  41. package/lib/node/rename-over-open-readers.d.ts +2 -0
  42. package/lib/node/rename-over-open-readers.d.ts.map +1 -1
  43. package/lib/node/rename-over-open-readers.js +6 -2
  44. package/lib/node/rename-over-open-readers.js.map +1 -1
  45. package/lib/testing/playwright/server-log-capture.d.ts +4 -0
  46. package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
  47. package/lib/testing/playwright/server-log-capture.js.map +1 -1
  48. package/lib/testing/stub-document-builder.d.ts +9 -2
  49. package/lib/testing/stub-document-builder.d.ts.map +1 -1
  50. package/lib/testing/stub-document-builder.js +25 -0
  51. package/lib/testing/stub-document-builder.js.map +1 -1
  52. package/package.json +5 -5
  53. package/src/langium/bootstrap.ts +11 -9
  54. package/src/langium/build-phase-pass/build-phase-pass-service.ts +1 -1
  55. package/src/langium/build-phase-pass/build-phase-pass.ts +1 -1
  56. package/src/langium/diagnostics/hydranium-langium-profiler.ts +15 -39
  57. package/src/langium/document-builder/document-builder.ts +37 -19
  58. package/src/langium/integrity/integrity-service.ts +2 -4
  59. package/src/langium/module.ts +2 -2
  60. package/src/langium/scope/fallback-scope.ts +38 -0
  61. package/src/langium/scope/hydranium-scope-provider.ts +26 -14
  62. package/src/langium/scope/index.ts +1 -0
  63. package/src/langium/scope/scope-extension-service.ts +74 -59
  64. package/src/langium/shared-services.ts +5 -0
  65. package/src/node/rename-over-open-readers.ts +7 -2
  66. package/src/testing/playwright/server-log-capture.ts +4 -0
  67. package/src/testing/stub-document-builder.ts +33 -2
@@ -7,9 +7,9 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import { type AstNode, AstUtils, type LangiumDocument, MapScope, type Scope, StreamScope, stream } from '@hydranium/langium';
10
+ import { type AstNode, AstUtils, type LangiumDocument } from '@hydranium/langium';
11
11
  import { type Disposable } from 'vscode-languageserver';
12
- import { type Tracer } from '@hydranium/protocol';
12
+ import { Format, Logger, type ProfileRecord, type ProfileSession, type Tracer } from '@hydranium/protocol';
13
13
  import { type LogNameOptions } from '../diagnostics/logger.js';
14
14
  import { type ServerLanguageServices } from '../language-module.js';
15
15
  import { Registry, type RegistryItem } from '../../util/registry.js';
@@ -49,8 +49,7 @@ export interface ScopeDescriptionAcceptor {
49
49
  * descriptions are constructed ahead of time, to avoid per-query
50
50
  * allocation. The pushed description's `tier` must be `'local'` or
51
51
  * `'universal'`; a description of any other tier is silently dropped,
52
- * because the per-tier query methods only return descriptions matching
53
- * the tier they were asked for.
52
+ * because {@link ScopeExtensionDescriptions} holds only those two.
54
53
  */
55
54
  push(description: TieredAstNodeDescription): void;
56
55
  }
@@ -87,38 +86,32 @@ export interface ScopeExtension extends RegistryItem {
87
86
  addDescriptions(context: AstNode, referenceType: string, document: LangiumDocument, accept: ScopeDescriptionAcceptor): void;
88
87
  }
89
88
 
89
+ /** The scope-extension descriptions for one reference, split by tier. */
90
+ export interface ScopeExtensionDescriptions {
91
+ readonly local: readonly TieredAstNodeDescription[];
92
+ readonly universal: readonly TieredAstNodeDescription[];
93
+ }
94
+
90
95
  /**
91
96
  * Public contract for the per-language scope-extension service. Extends
92
97
  * the {@link ScopeExtensionRegistry} (the imperative `register` surface
93
- * contributions use) with the per-tier query methods called cross-class
94
- * from `HydraniumScopeProvider`'s `layerLocalExtension` /
95
- * `layerUniversalExtension` helpers, which layer scope-extension
96
- * contributions around the cached project chain.
97
- *
98
- * Adopter overrides go through {@link DefaultScopeExtensionService}; the
99
- * interface keeps the public API stable while internals
100
- * (`collectExtensionDescriptions` walk) stay `protected` on the default
101
- * class.
98
+ * contributions use) with the query `HydraniumScopeProvider` builds its
99
+ * extension tiers from.
102
100
  */
103
101
  export interface ScopeExtensionService extends ScopeExtensionRegistry {
104
102
  /**
105
- * Layer this language's `local`-tier scope-extension descriptions on
106
- * top of `outerScope` for the given reference type. Returns
107
- * `outerScope` unchanged when no extension contributes a `local`-tier
108
- * description for the type (zero-cost common path).
103
+ * Run every extension registered for `referenceType` once against
104
+ * `context`, and answer its descriptions by tier. A caller that needs
105
+ * both tiers takes them from one call: a call per tier runs every
106
+ * extension twice.
109
107
  */
110
- getLocalExtensionScope(referenceType: string, context: AstNode, outerScope: Scope): Scope;
111
- /**
112
- * Layer this language's `universal`-tier scope-extension descriptions
113
- * BELOW `outerScope` for the given reference type. Returns `outerScope`
114
- * unchanged when no extension contributes a `universal`-tier
115
- * description for the type.
116
- */
117
- getUniversalExtensionScope(referenceType: string, context: AstNode, outerScope: Scope): Scope;
108
+ getDescriptions(referenceType: string, context: AstNode): ScopeExtensionDescriptions;
118
109
  }
119
110
 
120
111
  export type ScopeExtensionServiceOptions = LogNameOptions;
121
112
 
113
+ const NO_DESCRIPTIONS: ScopeExtensionDescriptions = { local: [], universal: [] };
114
+
122
115
  /**
123
116
  * Default {@link ScopeExtensionService} implementation. Per-language
124
117
  * registry for dynamic scope contributions: extra resolvable descriptions
@@ -128,13 +121,19 @@ export type ScopeExtensionServiceOptions = LogNameOptions;
128
121
  * separate from the query-time scope-resolution concern (which only reads).
129
122
  *
130
123
  * Lives in the per-language `references` group alongside `ScopeProvider`,
131
- * which consumes the per-tier query methods ({@link getLocalExtensionScope}
132
- * / {@link getUniversalExtensionScope}). Designed as a base class; adopters
124
+ * which consumes {@link getDescriptions}. Designed as a base class; adopters
133
125
  * extend it and call `register` in their constructor.
126
+ *
127
+ * At `debug`, each extension's calls and self-time go into one profile
128
+ * session, reported when a build starts and when it ends. A report thus
129
+ * covers one build, validation included, or the calls made between two
130
+ * builds, such as a reference picker's.
134
131
  */
135
132
  export class DefaultScopeExtensionService implements ScopeExtensionService {
136
133
  protected readonly scopeExtensions = new Registry<ScopeExtension>();
137
134
  protected readonly tracer: Tracer;
135
+ /** Open from the first profiled call until {@link reportProfile}. */
136
+ protected profileSession?: ProfileSession;
138
137
 
139
138
  constructor(
140
139
  protected readonly services: ServerLanguageServices,
@@ -150,6 +149,10 @@ export class DefaultScopeExtensionService implements ScopeExtensionService {
150
149
  for (const contribution of Object.values(contributions)) {
151
150
  contribution.registerScopeExtensions(this);
152
151
  }
152
+ // Optional for the same reason: a stub tree may carry no builder.
153
+ const builder = services.shared.workspace.DocumentBuilder;
154
+ builder?.onUpdate(() => this.reportProfile());
155
+ builder?.onBuildEnded(() => this.reportProfile());
153
156
  }
154
157
 
155
158
  /**
@@ -162,46 +165,58 @@ export class DefaultScopeExtensionService implements ScopeExtensionService {
162
165
  return this.scopeExtensions.register(extension);
163
166
  }
164
167
 
165
- getLocalExtensionScope(referenceType: string, context: AstNode, outerScope: Scope): Scope {
166
- const local = this.collectExtensionDescriptions(referenceType, context, 'local');
167
- if (local.length === 0) {
168
- return outerScope;
169
- }
170
- return new StreamScope(stream(local), outerScope);
171
- }
172
-
173
- getUniversalExtensionScope(referenceType: string, context: AstNode, outerScope: Scope): Scope {
174
- const universal = this.collectExtensionDescriptions(referenceType, context, 'universal');
175
- if (universal.length === 0) {
176
- return outerScope;
177
- }
178
- return new StreamScope(outerScope.getAllElements(), new MapScope(universal));
179
- }
180
-
181
168
  /**
182
- * Walk this language's registered scope extensions matching
183
- * `referenceType` and collect descriptions of the requested `tier`.
184
- * `push`-ed descriptions of other tiers are silently dropped here —
185
- * the per-tier query methods only return descriptions matching their
186
- * tier. (`local` and `universal` are the only acceptor entry points;
187
- * `push` is the escape hatch for pre-built descriptions and is the
188
- * only path that could carry other tiers.)
169
+ * Holds the profile wrap, which an override would have to copy; to change
170
+ * which extensions run, override {@link extensionsFor}.
189
171
  */
190
- protected collectExtensionDescriptions(
191
- referenceType: string,
192
- context: AstNode,
193
- tier: 'local' | 'universal'
194
- ): TieredAstNodeDescription[] {
195
- const extensionsForType = this.scopeExtensions.all().filter(extension => extension.referenceTypes.includes(referenceType));
172
+ getDescriptions(referenceType: string, context: AstNode): ScopeExtensionDescriptions {
173
+ const extensionsForType = this.extensionsFor(referenceType, context);
196
174
  if (extensionsForType.length === 0) {
197
- return [];
175
+ return NO_DESCRIPTIONS;
198
176
  }
199
177
  const document = AstUtils.getDocument(context);
200
178
  const collected: TieredAstNodeDescription[] = [];
201
179
  const accept = createScopeDescriptionAcceptor(collected, this.services.workspace.AstNodeDescriptionProvider);
180
+ const session = Logger.isLevelEnabled('debug') ? (this.profileSession ??= this.tracer.profile('scope-extension')) : undefined;
202
181
  for (const extension of extensionsForType) {
203
- extension.addDescriptions(context, referenceType, document, accept);
182
+ if (session) {
183
+ session.scope(extension.id, () => extension.addDescriptions(context, referenceType, document, accept));
184
+ } else {
185
+ extension.addDescriptions(context, referenceType, document, accept);
186
+ }
187
+ }
188
+ return {
189
+ local: collected.filter(description => description.tier === 'local'),
190
+ universal: collected.filter(description => description.tier === 'universal')
191
+ };
192
+ }
193
+
194
+ /** The extensions that run for one reference: by default, those registered for `referenceType`. */
195
+ protected extensionsFor(referenceType: string, _context: AstNode): ScopeExtension[] {
196
+ return this.scopeExtensions.all().filter(extension => extension.referenceTypes.includes(referenceType));
197
+ }
198
+
199
+ /**
200
+ * Report the profile session, if one is open, and start the next one fresh:
201
+ * one line per extension, its calls and self-time. Not a share of the
202
+ * session's time, which runs from the first call to the report and so takes
203
+ * in whatever happened between the calls.
204
+ */
205
+ protected reportProfile(): void {
206
+ const session = this.profileSession;
207
+ this.profileSession = undefined;
208
+ for (const record of session?.records() ?? []) {
209
+ this.tracer.debug(this.formatProfileRecord(record));
204
210
  }
205
- return collected.filter(description => description.tier === tier);
211
+ }
212
+
213
+ /**
214
+ * The log line of one extension's {@link reportProfile} entry. It names the
215
+ * language: each has its own session, and an extension id is unique only
216
+ * within its language.
217
+ */
218
+ protected formatProfileRecord(record: ProfileRecord): string {
219
+ const language = this.services.LanguageMetaData.languageId;
220
+ return `[profile scope-extension ${language}] ${record.id} ×${record.count} ${Format.elapsed(record.selfMs)}`;
206
221
  }
207
222
  }
@@ -23,6 +23,7 @@ import type { ProjectManager } from './project/project-manager.js';
23
23
  import type { ExtendedServiceRegistry } from './service-registry.js';
24
24
  import type { DocumentUriPolicy } from './workspace/document-uri-policy.js';
25
25
  import type { HydraniumWorkspaceManager } from './workspace/hydranium-workspace-manager.js';
26
+ import type { HydraniumDocumentBuilder } from './document-builder/document-builder.js';
26
27
  import type { AdditionalDocumentContribution } from './workspace/additional-document-contribution.js';
27
28
 
28
29
  /**
@@ -94,6 +95,10 @@ export interface ServerSharedServicesMinimal<TProject extends Project = Project>
94
95
  // only through this narrowing — a scope provider querying a URI before
95
96
  // the file exists is the caller that needs it.
96
97
  /* override */ LangiumDocuments: HydraniumDocumentRegistry;
98
+ // Same, for the builder: only the framework's has `onBuildEnded`, which a
99
+ // per-build report needs because Langium's phases stay silent for a
100
+ // build that validates nothing.
101
+ /* override */ DocumentBuilder: HydraniumDocumentBuilder;
97
102
  ProjectManager: ProjectManager<TProject>;
98
103
  SelfSaveRegistry: SelfSaveRegistry;
99
104
  BuildPipelineIntegration: BuildPipelineIntegration;
@@ -7,6 +7,7 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
+ import { type Clock, SystemClock } from '@hydranium/protocol';
10
11
  import * as fsp from 'node:fs/promises';
11
12
 
12
13
  /**
@@ -37,6 +38,7 @@ export const RENAME_BUDGET_MS = 10_000;
37
38
  export interface RenameOverOpenReadersOptions {
38
39
  readonly rename?: (from: string, to: string) => Promise<void>;
39
40
  readonly budgetMs?: number;
41
+ readonly clock?: Clock;
40
42
  }
41
43
 
42
44
  /**
@@ -60,18 +62,21 @@ export async function renameOverOpenReaders(
60
62
  options: RenameOverOpenReadersOptions = {}
61
63
  ): Promise<void> {
62
64
  const rename = options.rename ?? fsp.rename;
63
- const deadline = Date.now() + (options.budgetMs ?? RENAME_BUDGET_MS);
65
+ const clock = options.clock ?? new SystemClock();
66
+ const deadline = clock.now() + (options.budgetMs ?? RENAME_BUDGET_MS);
64
67
  for (let attempt = 0; ; attempt++) {
65
68
  try {
66
69
  await rename(staging, destination);
67
70
  return;
68
71
  } catch (err: unknown) {
69
72
  const code = err instanceof Error && 'code' in err ? String((err as NodeJS.ErrnoException).code) : undefined;
70
- if (code === undefined || !RENAME_CONTENTION_CODES.has(code) || Date.now() >= deadline) {
73
+ if (code === undefined || !RENAME_CONTENTION_CODES.has(code) || clock.now() >= deadline) {
71
74
  throw err;
72
75
  }
73
76
  // Backs off to keep a long contention window from spinning, capped so
74
77
  // a late attempt still lands promptly once the handle is released.
78
+ // Not `clock.setTimer`: it unrefs its timer, so a process with only
79
+ // this write pending could exit mid-backoff.
75
80
  await new Promise(resolve => setTimeout(resolve, Math.min(2 ** attempt, 50)));
76
81
  }
77
82
  }
@@ -83,6 +83,10 @@ export interface CaptureServerLogOptions {
83
83
  *
84
84
  * The server's own threshold is left alone: it governs what the client sees,
85
85
  * not the file, and the setting replaces it within moments of startup.
86
+ *
87
+ * The server runs every path guarded on a level this admits, its debug
88
+ * profiling included, so a timing run captured at a level that admits debug
89
+ * measures that work too.
86
90
  */
87
91
  level?: LogThreshold;
88
92
  }
@@ -19,7 +19,7 @@ import {
19
19
  type WorkspaceLock
20
20
  } from '@hydranium/langium';
21
21
  import { type CancellationToken, Disposable } from 'vscode-languageserver';
22
- import { HydraniumDocumentBuilder } from '../langium/document-builder/document-builder.js';
22
+ import { type BuildEndedEvent, HydraniumDocumentBuilder } from '../langium/document-builder/document-builder.js';
23
23
 
24
24
  /** Recorded call to a stubbed {@link DocumentBuilder} method. */
25
25
  export interface RecordedBuilderCall<TArgs extends unknown[]> {
@@ -77,7 +77,7 @@ export interface StubWaitUntilGate {
77
77
  export interface StubDocumentBuilder
78
78
  extends
79
79
  Pick<DocumentBuilder, 'update' | 'onDocumentPhase' | 'onUpdate' | 'build' | 'onBuildPhase' | 'resetToState' | 'updateBuildOptions'>,
80
- Pick<HydraniumDocumentBuilder, 'onDocumentPhaseDelivered' | 'scheduleUpdate' | 'finalBuildState'> {
80
+ Pick<HydraniumDocumentBuilder, 'onDocumentPhaseDelivered' | 'scheduleUpdate' | 'finalBuildState' | 'onBuildEnded'> {
81
81
  /**
82
82
  * Single-overload stub of {@link DocumentBuilder.waitUntil}. Real has
83
83
  * two overloads (`(state, cancelToken?): Promise<void>` and
@@ -107,6 +107,13 @@ export interface StubDocumentBuilder
107
107
  * looking wired.
108
108
  */
109
109
  fireBuildPhase(state: DocumentState, built: LangiumDocument[], cancelToken?: CancellationToken): void;
110
+ /**
111
+ * Synchronously fire every {@link HydraniumDocumentBuilder.onBuildEnded}
112
+ * listener with `event`. As in the real builder, one that throws does not
113
+ * stop the others; the real builder logs the error, and this stub, having
114
+ * no logger, rethrows the first one once all have run.
115
+ */
116
+ fireBuildEnded(event: BuildEndedEvent): void;
110
117
  /**
111
118
  * Hold the next {@link waitUntil} call. The returned handle releases it;
112
119
  * the call's return value resolves on the next tick after `resolve` runs.
@@ -152,6 +159,7 @@ export function makeStubDocumentBuilder(workspaceLock?: WorkspaceLock): StubDocu
152
159
  const deliveredListeners = new Map<DocumentState, Array<(document: LangiumDocument, version: number) => void>>();
153
160
  const buildPhaseListeners = new Map<DocumentState, DocumentBuildListener[]>();
154
161
  const onUpdateListeners: DocumentUpdateListener[] = [];
162
+ const buildEndedListeners: Array<(event: BuildEndedEvent) => void> = [];
155
163
  const gates: Array<{ take(release: () => void): void }> = [];
156
164
  const updateCalls: RecordedBuilderCall<[URI[], URI[]]>[] = [];
157
165
  const waitUntilCalls: RecordedBuilderCall<[DocumentState, URI | undefined]>[] = [];
@@ -258,6 +266,28 @@ export function makeStubDocumentBuilder(workspaceLock?: WorkspaceLock): StubDocu
258
266
  reraise(listener(changed, deleted));
259
267
  }
260
268
  },
269
+ onBuildEnded(listener: (event: BuildEndedEvent) => void) {
270
+ buildEndedListeners.push(listener);
271
+ return Disposable.create(() => {
272
+ const idx = buildEndedListeners.indexOf(listener);
273
+ if (idx >= 0) {
274
+ buildEndedListeners.splice(idx, 1);
275
+ }
276
+ });
277
+ },
278
+ fireBuildEnded(event: BuildEndedEvent) {
279
+ const failures: unknown[] = [];
280
+ for (const listener of buildEndedListeners.slice()) {
281
+ try {
282
+ listener(event);
283
+ } catch (error: unknown) {
284
+ failures.push(error);
285
+ }
286
+ }
287
+ if (failures.length > 0) {
288
+ throw failures[0];
289
+ }
290
+ },
261
291
  onBuildPhase(state: DocumentState, listener: DocumentBuildListener) {
262
292
  const list = buildPhaseListeners.get(state) ?? [];
263
293
  list.push(listener);
@@ -317,6 +347,7 @@ export function makeStubDocumentBuilder(workspaceLock?: WorkspaceLock): StubDocu
317
347
  phaseListeners.clear();
318
348
  deliveredListeners.clear();
319
349
  onUpdateListeners.length = 0;
350
+ buildEndedListeners.length = 0;
320
351
  gates.length = 0;
321
352
  updateCalls.length = 0;
322
353
  waitUntilCalls.length = 0;