@abide/abide 0.123.3 → 0.124.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 (89) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +31 -0
  3. package/docs/REGISTRY.md +45 -12
  4. package/docs/RULEBOOK.md +24 -14
  5. package/docs/abide.md +999 -459
  6. package/package.json +1 -1
  7. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  8. package/src/compiler/LoweredFile.ts +609 -0
  9. package/src/compiler/Lowering.ts +17 -0
  10. package/src/compiler/SHARED_NAMES.ts +2 -0
  11. package/src/compiler/ShadowHost.ts +670 -0
  12. package/src/compiler/ShadowMap.ts +1 -1
  13. package/src/compiler/TypeSession.ts +71 -1715
  14. package/src/compiler/appLints.ts +109 -0
  15. package/src/compiler/checkedShadow.ts +1 -1
  16. package/src/compiler/classifyRead.ts +16 -1
  17. package/src/compiler/compileApp.ts +2 -2
  18. package/src/compiler/declaredExports.ts +44 -17
  19. package/src/compiler/diagnoseSources.ts +232 -0
  20. package/src/compiler/formatAbide.ts +5 -6
  21. package/src/compiler/handlersUnder.ts +16 -35
  22. package/src/compiler/reactiveReads.ts +235 -0
  23. package/src/compiler/reactiveTypes.ts +13 -0
  24. package/src/compiler/readsIn.ts +180 -81
  25. package/src/compiler/routesDeclaration.ts +46 -0
  26. package/src/compiler/scaffold.ts +7 -2
  27. package/src/compiler/serveLanguage.ts +26 -20
  28. package/src/compiler/splitImports.ts +2 -2
  29. package/src/server/DesktopSession.ts +213 -0
  30. package/src/server/Followers.ts +399 -0
  31. package/src/server/MODULE_QUERIES.ts +6 -2
  32. package/src/server/PageRender.ts +201 -0
  33. package/src/server/abideModules.ts +7 -7
  34. package/src/server/answerFailure.ts +3 -2
  35. package/src/server/answerRequest.ts +6 -70
  36. package/src/server/answerRpc.ts +48 -260
  37. package/src/server/answerSocket.ts +41 -463
  38. package/src/server/appKit.ts +1 -1
  39. package/src/server/argsContract.ts +1 -1
  40. package/src/server/createApp.ts +8 -2
  41. package/src/server/csp.ts +1 -1
  42. package/src/server/deferredBuilds.ts +3 -16
  43. package/src/server/faceOf.ts +1 -1
  44. package/src/server/generatedSurfaces.ts +2 -1
  45. package/src/server/handlerFileMount.ts +1 -1
  46. package/src/server/index.ts +1 -1
  47. package/src/server/installServer.ts +5 -9
  48. package/src/server/invokeHandler.ts +230 -0
  49. package/src/server/mountHandlers.ts +184 -0
  50. package/src/server/openDesktopWindow.ts +308 -0
  51. package/src/server/preload.ts +18 -0
  52. package/src/server/principal.ts +1 -1
  53. package/src/server/remote.ts +1 -1
  54. package/src/server/render.ts +31 -59
  55. package/src/server/requestScope.ts +5 -36
  56. package/src/server/runDesktop.ts +19 -447
  57. package/src/server/seedBuffer.ts +11 -11
  58. package/src/server/servedApp.ts +1 -1
  59. package/src/server/shell.ts +1 -1
  60. package/src/server/surfaceOf.ts +1 -1
  61. package/src/server/versionOf.ts +1 -1
  62. package/src/shared/RPC_SERVER.ts +27 -31
  63. package/src/shared/Schema.ts +1 -2
  64. package/src/shared/index.ts +1 -1
  65. package/src/shared/{note.ts → issueReport.ts} +22 -0
  66. package/src/shared/loadCall.ts +66 -0
  67. package/src/shared/reactive/Keyed.ts +23 -15
  68. package/src/shared/reactive/Reactive.ts +34 -1001
  69. package/src/shared/reactive/byOf.ts +2 -5
  70. package/src/shared/reactive/callLoad.ts +8 -14
  71. package/src/shared/reactive/cellShape.ts +734 -0
  72. package/src/shared/reactive/index.ts +12 -24
  73. package/src/shared/reactive/pairReactive.ts +5 -3
  74. package/src/shared/reactive/producer.ts +121 -0
  75. package/src/shared/reactive/scoped.ts +3 -2
  76. package/src/shared/reactive/selections.ts +209 -0
  77. package/src/shared/reactive/watch.ts +3 -4
  78. package/src/shared/rpc.ts +8 -5
  79. package/src/shared/validateJson.ts +1 -2
  80. package/src/shared/writePath.ts +29 -0
  81. package/src/ui/Router.ts +53 -69
  82. package/src/ui/outboxQueue.ts +2 -8
  83. package/src/ui/outcomeOf.ts +120 -0
  84. package/src/ui/socketWire.ts +3 -3
  85. package/src/ui/wire.ts +9 -10
  86. package/src/shared/LOAD_OFFLINE.ts +0 -15
  87. package/src/shared/LOAD_SIGNAL.ts +0 -10
  88. package/src/shared/issuesOf.ts +0 -18
  89. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.123.3",
3
+ "version": "0.124.0",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The methods an array declares that change it in place.
3
+ *
4
+ * A constants leaf, with no imports of its own.
5
+ *
6
+ * 31.9 — a call of one on a name bound to a `Reactive` is lifted to a write through the receiver's
7
+ * path only where the receiver's TYPE is an array (`liftedMutators` in `readsIn`): the name says
8
+ * which method, the type says whose.
9
+ */
10
+ export const ARRAY_MUTATORS: ReadonlySet<string> = new Set([
11
+ 'copyWithin',
12
+ 'fill',
13
+ 'pop',
14
+ 'push',
15
+ 'reverse',
16
+ 'shift',
17
+ 'sort',
18
+ 'splice',
19
+ 'unshift',
20
+ ])
@@ -0,0 +1,609 @@
1
+ import {
2
+ getTouchingPropertyName,
3
+ type Node,
4
+ NodeFlags,
5
+ SyntaxKind,
6
+ } from 'typescript/unstable/ast'
7
+ import {
8
+ type Checker,
9
+ type CompletionEntry,
10
+ type CompletionInfo,
11
+ CompletionItemKind,
12
+ NodeBuilderFlags,
13
+ type Project,
14
+ SignatureKind,
15
+ type Symbol as CheckedSymbol,
16
+ SymbolFlags,
17
+ type Type,
18
+ } from 'typescript/unstable/async'
19
+
20
+ import { aliasedSymbol } from './aliasedSymbol.ts'
21
+ import { COMPONENT_STAND_IN } from './checkedShadow.ts'
22
+ import { lineOf } from './lineOf.ts'
23
+ import { CHECKED_VALUES } from './Lowering.ts'
24
+ import type { Span } from './projectShadow.ts'
25
+ import type { ShadowHost } from './ShadowHost.ts'
26
+ import { CHECKED_SUFFIX, ShadowMap } from './ShadowMap.ts'
27
+ import { templateContext } from './templateContext.ts'
28
+
29
+ /**
30
+ * One `.abide` as a diagnosis lowered it into the checker: the text it lowered and the map from that
31
+ * text to what the checker reads — what an editor's questions about the file are asked through.
32
+ *
33
+ * 38.39 — `text` IS WHAT THE LOWERING WAS MADE OF, and an offset the editor asks at maps through a
34
+ * lowering of the same text or not at all: a file the parser refuses keeps its last lowering, whose
35
+ * offsets have moved under every edit since. Every question is asked of the host's last snapshot,
36
+ * which holds this lowering, and a span in another `.abide` maps back through that file's own.
37
+ */
38
+ export class LoweredFile {
39
+ readonly text: string
40
+ readonly map: ShadowMap
41
+ private readonly host: ShadowHost
42
+
43
+ constructor(host: ShadowHost, text: string, map: ShadowMap) {
44
+ this.host = host
45
+ this.text = text
46
+ this.map = map
47
+ }
48
+
49
+ /**
50
+ * Where the author's `offset` in `text` stands in what the checker reads, or `undefined` where
51
+ * `text` is not what was lowered.
52
+ */
53
+ private askedAt(
54
+ text: string,
55
+ offset: number,
56
+ ): { path: string; position: number } | undefined {
57
+ if (this.text !== text) return undefined
58
+ const asked = this.map.fromSource(offset)
59
+ return asked === undefined || !this.host.holds(asked.path)
60
+ ? undefined
61
+ : asked
62
+ }
63
+
64
+ /**
65
+ * The members that may be written at `offset` in a script or an expression, or `undefined`
66
+ * where none can be (`completionAt`).
67
+ */
68
+ async complete(
69
+ text: string,
70
+ offset: number,
71
+ triggerCharacter?: string,
72
+ ): Promise<CompletionInfo | undefined> {
73
+ const asked = this.askedAt(text, offset)
74
+ if (asked === undefined) return undefined
75
+ return this.host.ask(
76
+ (project) =>
77
+ project.checker.getCompletionsAtPosition(
78
+ asked.path,
79
+ asked.position,
80
+ { triggerCharacter },
81
+ ),
82
+ { latest: true },
83
+ )
84
+ }
85
+
86
+ /**
87
+ * The props of the component `tag` names that `written` does not, as completions.
88
+ *
89
+ * A PROP IS ASKED OF THIS LOWERING, by the component's name: the tag being typed is text the
90
+ * parser refuses, and the import the name resolves through is the same in the lowering before it.
91
+ */
92
+ async propsUnwritten(
93
+ tag: string,
94
+ written: readonly string[],
95
+ ): Promise<CompletionEntry[]> {
96
+ const entries = await this.host.ask(
97
+ async (project) => {
98
+ const checker = project.checker
99
+ const found = await checker.resolveName(
100
+ tag,
101
+ SymbolFlags.Value,
102
+ { document: this.map.shadowPath, position: 0 },
103
+ )
104
+ const record =
105
+ found === undefined
106
+ ? undefined
107
+ : await propsRecordOf(
108
+ checker,
109
+ await aliasedSymbol(checker, found),
110
+ )
111
+ if (record === undefined) return []
112
+ const unwritten = (
113
+ await checker.getPropertiesOfType(record)
114
+ ).filter((prop) => !written.includes(prop.name))
115
+ return Promise.all(
116
+ unwritten.map(async (prop) => {
117
+ const type = await checker.getTypeOfSymbol(prop)
118
+ return {
119
+ name: prop.name,
120
+ kind: CompletionItemKind.Property,
121
+ detail:
122
+ type === undefined
123
+ ? undefined
124
+ : await checker.typeToString(
125
+ type,
126
+ undefined,
127
+ NodeBuilderFlags.NoTruncation,
128
+ ),
129
+ }
130
+ }),
131
+ )
132
+ },
133
+ { latest: true },
134
+ )
135
+ return entries ?? []
136
+ }
137
+
138
+ /**
139
+ * The hover signature, documentation and definitions of the name at `offset`.
140
+ *
141
+ * 38.39 — WHAT THE NAME AT `offset` IS, asked of the program this lowering is in: its type and
142
+ * documentation for a hover, and where it is declared for a jump. The checked module is asked
143
+ * where the position was copied into it, which is what a member hangs off — `regions.join` is
144
+ * `Array`'s — and the projection where the lowering rewrote it, so a name read by name is the
145
+ * `Reactive` it was declared as.
146
+ */
147
+ async inspect(
148
+ text: string,
149
+ offset: number,
150
+ ): Promise<Inspection | undefined> {
151
+ const asked = this.askedAt(text, offset)
152
+ if (asked === undefined) return undefined
153
+ const inspection = await this.host.ask(
154
+ async (project) => {
155
+ const checker = project.checker
156
+ const found = await checker.getSymbolAtPosition(
157
+ asked.path,
158
+ asked.position,
159
+ )
160
+ if (found === undefined) return undefined
161
+ const symbol = await aliasedSymbol(checker, found)
162
+ const declarations: Node[] = []
163
+ for (const handle of symbol.declarations) {
164
+ const node = await handle.resolve(project)
165
+ if (node !== undefined) declarations.push(node)
166
+ }
167
+ // A `.abide` import resolves to its checked module's stand-in, which is the
168
+ // component.
169
+ const component =
170
+ symbol.name === COMPONENT_STAND_IN &&
171
+ declarations[0]
172
+ ?.getSourceFile()
173
+ .fileName.endsWith(CHECKED_SUFFIX)
174
+ const [signature, documentation] = component
175
+ ? [
176
+ `(component) ${found.name}(props: ${await propsTypeOf(checker, symbol)})`,
177
+ '',
178
+ ]
179
+ : await Promise.all([
180
+ describe(project, symbol, declarations[0]),
181
+ symbol.getDocumentationComment(checker),
182
+ ])
183
+ const definitions: Place[] = []
184
+ for (const node of declarations) {
185
+ const named = (node as { name?: Node }).name ?? node
186
+ const file = node.getSourceFile()
187
+ definitions.push(
188
+ this.spanInSource(
189
+ file.fileName,
190
+ file.text,
191
+ named.getStart(file),
192
+ named.end,
193
+ ),
194
+ )
195
+ }
196
+ return {
197
+ signature: signature.replaceAll(`${CHECKED_VALUES}.`, ''),
198
+ documentation,
199
+ definitions,
200
+ }
201
+ },
202
+ { latest: true },
203
+ )
204
+ return inspection
205
+ }
206
+
207
+ /**
208
+ * Every place the name at `offset` is written, across the files the checker holds, or
209
+ * `undefined` where nothing is named there.
210
+ *
211
+ * 38.116 — ASKED OF EVERY PLACE THE CHECKER READS THE OFFSET AT (`ShadowMap.placesFrom`), because
212
+ * the checked module rewrites a name read by name into another symbol's read: the projection is
213
+ * where a reactive's markup uses are the declaration's references. A place is kept only where
214
+ * both layers copied it from the author's text and that text is the name, so scaffolding a
215
+ * lowering wrote is never a place to rename.
216
+ *
217
+ * AND ASKED AGAIN WHERE A REFERENCE IS TEXT THE CHECKED MODULE COPIES TWICE. A component's props
218
+ * type is a second copy of its `props` call (`checkedShadow`), and a tag's keys are typed by
219
+ * that copy: the checker answers them as another symbol's references, so a reference landing
220
+ * on one copy is asked at every place the author's text is read.
221
+ */
222
+ async references(
223
+ text: string,
224
+ offset: number,
225
+ ): Promise<References | undefined> {
226
+ if (this.text !== text) return undefined
227
+ const asked = this.map.placesFrom(offset)
228
+ if (asked.length === 0) return undefined
229
+ return this.host.ask((project) => this.referencesIn(project, asked), {
230
+ latest: true,
231
+ })
232
+ }
233
+
234
+ private async referencesIn(
235
+ project: Project,
236
+ asked: { path: string; position: number }[],
237
+ ): Promise<References | undefined> {
238
+ const askedAt = new Set<string>()
239
+ for (const place of asked)
240
+ askedAt.add(`${place.path}:${place.position}`)
241
+ let name: Place | undefined
242
+ let spelled = ''
243
+ let renamable = true
244
+ const seen = new Set<string>()
245
+ const occurrences: Occurrence[] = []
246
+ for (let index = 0; index < asked.length; index += 1) {
247
+ const { path: file, position } = asked[index] as {
248
+ path: string
249
+ position: number
250
+ }
251
+ const sourceFile = await project.program.getSourceFile(file)
252
+ if (sourceFile === undefined) continue
253
+ const node = getTouchingPropertyName(sourceFile, position)
254
+ if (node.kind !== SyntaxKind.Identifier) continue
255
+ const written = node.getText(sourceFile)
256
+ if (name === undefined) {
257
+ const copied = this.copiedSpan(node, written)
258
+ name = copied === undefined ? undefined : placeOfSpan(copied)
259
+ spelled = written
260
+ }
261
+ const entries = await project.checker.getReferencedSymbolsForNode(
262
+ node,
263
+ position,
264
+ )
265
+ for (const entry of entries) {
266
+ const definition = await entry.definition.resolve(project)
267
+ if (definition === undefined) continue
268
+ const declaredIn = definition.getSourceFile().fileName
269
+ if (!this.authored(declaredIn)) renamable = false
270
+ const declared = (definition as { name?: Node }).name
271
+ const references = await Promise.all(
272
+ entry.references.map((handle) => handle.resolve(project)),
273
+ )
274
+ for (const reference of references) {
275
+ if (reference === undefined) continue
276
+ const copied = this.copiedSpan(reference, written)
277
+ if (copied === undefined) continue
278
+ const declaration =
279
+ declared !== undefined &&
280
+ declared.pos === reference.pos &&
281
+ declaredIn === reference.getSourceFile().fileName
282
+ // Text the checker reads at more than one place is asked at every one.
283
+ const copies = copied.map?.placesFrom(copied.start) ?? []
284
+ if (copies.length > 2)
285
+ for (const place of copies) {
286
+ const at = `${place.path}:${place.position}`
287
+ if (askedAt.has(at)) continue
288
+ askedAt.add(at)
289
+ asked.push(place)
290
+ }
291
+ const placed = placeOfSpan(copied)
292
+ const key = `${placed.path}:${placed.start.line}:${placed.start.column}`
293
+ if (seen.has(key)) continue
294
+ seen.add(key)
295
+ const holder = reference.parent
296
+ const declaresHolder =
297
+ definition.pos === holder.pos &&
298
+ declaredIn === reference.getSourceFile().fileName
299
+ const shorthand =
300
+ holder.kind ===
301
+ SyntaxKind.ShorthandPropertyAssignment ||
302
+ (holder.kind === SyntaxKind.BindingElement &&
303
+ (holder as { propertyName?: Node }).propertyName ===
304
+ undefined)
305
+ occurrences.push({
306
+ ...placed,
307
+ declaration,
308
+ // A shorthand names a property and a binding at once, and renaming one
309
+ // writes the other out beside it: the binding element declares the local
310
+ // and the shorthand assignment the property.
311
+ shorthand: !shorthand
312
+ ? undefined
313
+ : declaresHolder ===
314
+ (holder.kind === SyntaxKind.BindingElement)
315
+ ? 'local'
316
+ : 'property',
317
+ })
318
+ }
319
+ }
320
+ }
321
+ if (name === undefined) return undefined
322
+ return { name, written: spelled, renamable, occurrences }
323
+ }
324
+
325
+ /**
326
+ * Whether a file the checker read is one an author wrote and can edit: neither a library's
327
+ * declaration, nor a dependency's, nor scaffolding the host serves that stands for no `.abide`.
328
+ */
329
+ private authored(fileName: string): boolean {
330
+ if (fileName.endsWith('.d.ts') || fileName.includes('/node_modules/'))
331
+ return false
332
+ return (
333
+ ShadowMap.fileFor(this.host.canonical(fileName)) !== undefined ||
334
+ !this.host.holds(fileName)
335
+ )
336
+ }
337
+
338
+ /**
339
+ * Where a node the checker read stands in the file the author wrote, with that file's map where
340
+ * it is a `.abide`, or `undefined` where the author did not write `name` there: in scaffolding,
341
+ * or in a file the host serves.
342
+ */
343
+ private copiedSpan(node: Node, name: string): CopiedSpan | undefined {
344
+ const file = node.getSourceFile()
345
+ let start = node.getStart(file)
346
+ let end = node.end
347
+ // A string literal names a property by its text, inside the quotes.
348
+ if (node.kind === SyntaxKind.StringLiteral) {
349
+ start += 1
350
+ end -= 1
351
+ }
352
+ const canonical = this.host.canonical(file.fileName)
353
+ const abidePath = ShadowMap.fileFor(canonical)
354
+ if (abidePath === undefined) {
355
+ if (!this.authored(file.fileName)) return undefined
356
+ if (file.text.slice(start, end) !== name) return undefined
357
+ return {
358
+ path: file.fileName,
359
+ text: file.text,
360
+ start,
361
+ end,
362
+ map: undefined,
363
+ }
364
+ }
365
+ const lowered = this.host.lowered(abidePath)
366
+ const span = lowered?.map.copiedToSource(canonical, start, end)
367
+ if (
368
+ lowered === undefined ||
369
+ span === undefined ||
370
+ lowered.text.slice(span.start, span.end) !== name
371
+ )
372
+ return undefined
373
+ return {
374
+ path: abidePath,
375
+ text: lowered.text,
376
+ start: span.start,
377
+ end: span.end,
378
+ map: lowered.map,
379
+ }
380
+ }
381
+
382
+ /**
383
+ * A span the checker read, placed in the file the author wrote: a lowered `.abide` mapped back
384
+ * through both of its maps, and one the host has not lowered — or a span of the scaffolding a
385
+ * lowering adds, which the author never wrote — placed at its top.
386
+ */
387
+ private spanInSource(
388
+ fileName: string,
389
+ text: string,
390
+ start: number,
391
+ end: number,
392
+ ): Place {
393
+ const canonical = this.host.canonical(fileName)
394
+ const abidePath = ShadowMap.fileFor(canonical)
395
+ if (abidePath === undefined)
396
+ return {
397
+ path: fileName,
398
+ start: lineOf(text, start),
399
+ end: lineOf(text, end),
400
+ }
401
+ const lowered = this.host.lowered(abidePath)
402
+ const placed = lowered?.map.spanToSource(canonical, start, end)
403
+ if (lowered === undefined || placed === undefined) {
404
+ const top = { line: 1, column: 1 }
405
+ return { path: abidePath, start: top, end: top }
406
+ }
407
+ return {
408
+ path: abidePath,
409
+ start: lineOf(lowered.text, placed.start),
410
+ end: lineOf(lowered.text, placed.end),
411
+ }
412
+ }
413
+ }
414
+
415
+ /**
416
+ * The completions at `offset` in a `.abide`'s `text`, asked through `lowered` — its file's last
417
+ * lowering, where it has one — or `undefined` where none can be written.
418
+ *
419
+ * 38.98 — ASKED WHERE A HOVER IS, so `config().` offers the fields 27.13 declared and a name read by
420
+ * name offers its value's members (31.1). MEMBERS ALONE in a script or an expression: at an
421
+ * identifier the API rejects ("completion list needs auto imports"), which the language server
422
+ * answers as nothing offered. 38.117 — MARKUP offers a block keyword, a component in scope, or a
423
+ * component's props, the last of which is the one a lowering is needed for.
424
+ */
425
+ export async function completionAt(
426
+ lowered: LoweredFile | undefined,
427
+ text: string,
428
+ offset: number,
429
+ triggerCharacter?: string,
430
+ ): Promise<CompletionInfo | undefined> {
431
+ const template = templateContext(text, offset)
432
+ if (template === undefined)
433
+ return lowered?.complete(text, offset, triggerCharacter)
434
+ if (template.kind === 'attribute')
435
+ return {
436
+ isIncomplete: false,
437
+ entries:
438
+ lowered === undefined
439
+ ? []
440
+ : await lowered.propsUnwritten(
441
+ template.tag,
442
+ template.written,
443
+ ),
444
+ }
445
+ const kind =
446
+ template.kind === 'block'
447
+ ? CompletionItemKind.Keyword
448
+ : CompletionItemKind.Class
449
+ const entries: CompletionEntry[] = []
450
+ for (const name of template.offered) entries.push({ name, kind })
451
+ return { isIncomplete: false, entries }
452
+ }
453
+
454
+ /**
455
+ * Where something is in a file the author wrote, line and column from 1.
456
+ *
457
+ * 38.39.
458
+ */
459
+ export type Place = {
460
+ path: string
461
+ start: { line: number; column: number }
462
+ end: { line: number; column: number }
463
+ }
464
+
465
+ /**
466
+ * The name under a cursor: how it is declared, what its doc comment says, and where.
467
+ *
468
+ * 38.39.
469
+ */
470
+ export type Inspection = {
471
+ signature: string
472
+ documentation: string
473
+ definitions: Place[]
474
+ }
475
+
476
+ /**
477
+ * The name under a cursor and every place it is written.
478
+ *
479
+ * 38.116 — `name` is where the cursor's own name stands; `renamable` is false where any symbol it
480
+ * names is declared somewhere an author does not write.
481
+ */
482
+ export type References = {
483
+ name: Place
484
+ /** The name as it is written. */
485
+ written: string
486
+ renamable: boolean
487
+ occurrences: Occurrence[]
488
+ }
489
+
490
+ /**
491
+ * One place a name is written: whether it is where the name is declared, and where it is a
492
+ * shorthand, which side of it a rename renames — the `local` binding or the `property`.
493
+ */
494
+ type Occurrence = Place & {
495
+ declaration: boolean
496
+ shorthand: 'local' | 'property' | undefined
497
+ }
498
+
499
+ /** A span of a file the author wrote, and the map of the `.abide` it is in, where it is in one. */
500
+ type CopiedSpan = Span & {
501
+ path: string
502
+ text: string
503
+ map: ShadowMap | undefined
504
+ }
505
+
506
+ /** A `CopiedSpan` as lines and columns. */
507
+ function placeOfSpan(span: CopiedSpan): Place {
508
+ return {
509
+ path: span.path,
510
+ start: lineOf(span.text, span.start),
511
+ end: lineOf(span.text, span.end),
512
+ }
513
+ }
514
+
515
+ /**
516
+ * The props type a component's tag is held to, printed.
517
+ *
518
+ * 33.4 — the record its stand-in is called with.
519
+ */
520
+ async function propsTypeOf(
521
+ checker: Checker,
522
+ component: CheckedSymbol,
523
+ ): Promise<string> {
524
+ const record = await propsRecordOf(checker, component)
525
+ return record === undefined
526
+ ? 'any'
527
+ : checker.typeToString(record, undefined, NodeBuilderFlags.NoTruncation)
528
+ }
529
+
530
+ /** The record a component is called with — the props its tag is held to (33.4). */
531
+ async function propsRecordOf(
532
+ checker: Checker,
533
+ component: CheckedSymbol,
534
+ ): Promise<Type | undefined> {
535
+ const type = await checker.getTypeOfSymbol(component)
536
+ const call =
537
+ type === undefined
538
+ ? undefined
539
+ : (await checker.getSignaturesOfType(type, SignatureKind.Call))[0]
540
+ return call === undefined ? undefined : checker.getParameterType(call, 0)
541
+ }
542
+
543
+ /**
544
+ * How a hover spells a symbol, as tsserver does: the keyword it was declared with, its name, and its
545
+ * type — a function's first signature, with how many more it has.
546
+ */
547
+ async function describe(
548
+ project: Project,
549
+ symbol: CheckedSymbol,
550
+ declaration: Node | undefined,
551
+ ): Promise<string> {
552
+ const checker = project.checker
553
+ const flags = symbol.flags
554
+ const name = symbol.name
555
+ if (flags & SymbolFlags.Interface) return `interface ${name}`
556
+ if (flags & SymbolFlags.Class) return `class ${name}`
557
+ if (flags & SymbolFlags.TypeAlias)
558
+ return `type ${name} = ${await checker.typeToString(
559
+ await checker.getDeclaredTypeOfSymbol(symbol),
560
+ undefined,
561
+ NodeBuilderFlags.NoTruncation,
562
+ )}`
563
+ const type = await checker.getTypeOfSymbol(symbol)
564
+ if (type === undefined) return name
565
+ if (flags & (SymbolFlags.Function | SymbolFlags.Method)) {
566
+ const signatures = await checker.getSignaturesOfType(
567
+ type,
568
+ SignatureKind.Call,
569
+ )
570
+ const first = signatures[0]
571
+ const declared =
572
+ first === undefined
573
+ ? undefined
574
+ : await checker.signatureToSignatureDeclaration(
575
+ first,
576
+ SyntaxKind.MethodSignature,
577
+ undefined,
578
+ NodeBuilderFlags.NoTruncation,
579
+ )
580
+ if (declared !== undefined) {
581
+ const printed = (await project.emitter.printNode(declared))
582
+ .trim()
583
+ .replace(/;$/, '')
584
+ const more = signatures.length - 1
585
+ return `${flags & SymbolFlags.Function ? 'function' : '(method)'} ${name}${printed}${
586
+ more > 0 ? ` (+${more} overload${more === 1 ? '' : 's'})` : ''
587
+ }`
588
+ }
589
+ }
590
+ const spelled = await checker.typeToString(
591
+ type,
592
+ undefined,
593
+ NodeBuilderFlags.NoTruncation,
594
+ )
595
+ let keyword = '(property)'
596
+ if (flags & SymbolFlags.Function) keyword = 'function'
597
+ else if (flags & SymbolFlags.Method) keyword = '(method)'
598
+ else if (declaration?.kind === SyntaxKind.Parameter) keyword = '(parameter)'
599
+ else if (flags & SymbolFlags.Variable) {
600
+ const list = declaration?.parent.flags ?? 0
601
+ keyword =
602
+ list & NodeFlags.Const
603
+ ? 'const'
604
+ : list & NodeFlags.Let
605
+ ? 'let'
606
+ : 'var'
607
+ }
608
+ return `${keyword} ${name}: ${spelled}`
609
+ }
@@ -817,6 +817,21 @@ export class Lowering {
817
817
  return this.checking && this.importsRaw
818
818
  ? undefined
819
819
  : this.use('raw')
820
+ // 31.9 — an array mutator called through a path: run on a copy the name then holds.
821
+ // The checker reads it as the call on the value it is, so its result is typed as one.
822
+ case 'mutate': {
823
+ const call = this.lower(read.value as Span).trim()
824
+ if (this.checking)
825
+ return `${this.valueOf(spelled, read, name)}${this.lower({ start: read.end, end: (read.value as Span).start })}${call}`
826
+ const segments: string[] = []
827
+ for (const segment of read.path)
828
+ segments.push(
829
+ segment.name === undefined
830
+ ? this.lower(segment)
831
+ : JSON.stringify(segment.name),
832
+ )
833
+ return `${this.use('mutatePath')}(${name}, [${segments.join(', ')}], (${MUTATED}) => ${MUTATED}${call})`
834
+ }
820
835
  // 27.13 — the call bound to a name, for the checker to read the defaults' type off.
821
836
  case 'config':
822
837
  return this.checking
@@ -999,3 +1014,5 @@ function writeParts(
999
1014
  // `__` for the reason every framework name in an emitted module is (`importClause`).
1000
1015
  const STORED = '__stored'
1001
1016
  const PREVIOUS = '__previous'
1017
+ // 31.9 — the copy a lifted mutator is called on.
1018
+ const MUTATED = '__mutated'
@@ -17,6 +17,8 @@ export const SHARED_NAMES: ReadonlySet<string> = new Set([
17
17
  'markupRun',
18
18
  'RAW_WRITES',
19
19
  'writePath',
20
+ // 31.9 — an array mutator lifted to a write through its receiver's path.
21
+ 'mutatePath',
20
22
  'listOf',
21
23
  // 32.23's abort, which only the server lane can need: a client binding is its own `watch`
22
24
  // and gets one from 2.12. Emitted on both sides because `renderMarkup` is both sides.