@abide/abide 0.105.0 → 0.106.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.
- package/CHANGELOG.md +24 -0
- package/bin/abide.ts +4 -1
- package/docs/REGISTRY.md +156 -44
- package/docs/RULEBOOK.md +375 -87
- package/docs/abide.md +7243 -10256
- package/package.json +4 -1
- package/scaffold/AGENTS.md +2 -0
- package/scaffold/src/getGreeting.ts +6 -0
- package/scaffold/src/index.abide +3 -1
- package/scaffold/src/tests/getGreeting.test.ts +6 -0
- package/scaffold/src/tests/index.test.ts +18 -0
- package/src/compiler/DOCS_SITE.ts +2 -0
- package/src/compiler/Lowering.ts +234 -14
- package/src/compiler/PositionTable.ts +14 -7
- package/src/compiler/PositionedRefusal.ts +13 -0
- package/src/compiler/Refusals.ts +18 -11
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowMap.ts +37 -0
- package/src/compiler/TEMPLATE_BLOCKS.ts +12 -0
- package/src/compiler/TypeSession.ts +410 -31
- package/src/compiler/appLayout.ts +18 -2
- package/src/compiler/argsSchemaOf.ts +55 -9
- package/src/compiler/checkApp.ts +33 -0
- package/src/compiler/checkedShadow.ts +36 -22
- package/src/compiler/citedClauses.ts +14 -0
- package/src/compiler/classifyRead.ts +6 -0
- package/src/compiler/classifyTemplate.ts +2 -1
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +27 -6
- package/src/compiler/emitClient.ts +304 -103
- package/src/compiler/handlersUnder.ts +4 -0
- package/src/compiler/invocationProps.ts +1 -1
- package/src/compiler/isIgnored.ts +27 -0
- package/src/compiler/parseAbide.ts +176 -76
- package/src/compiler/projectShadow.ts +30 -6
- package/src/compiler/readSources.ts +1 -1
- package/src/compiler/readsIn.ts +100 -1
- package/src/compiler/renderMarkup.ts +36 -5
- package/src/compiler/scaffold.ts +19 -6
- package/src/compiler/serveLanguage.ts +156 -21
- package/src/compiler/templateContext.ts +150 -0
- package/src/compiler/tokenize.ts +6 -0
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/Inbox.ts +8 -0
- package/src/server/MCP_PROTOCOL_VERSION.ts +1 -1
- package/src/server/MOUNTABLE.ts +6 -0
- package/src/server/abideModules.ts +8 -1
- package/src/server/answerRequest.ts +3 -1
- package/src/server/answerRpc.ts +26 -12
- package/src/server/answerSocket.ts +26 -1
- package/src/server/argsFor.ts +4 -3
- package/src/server/assistant.ts +19 -13
- package/src/server/bridgeRelay.ts +108 -20
- package/src/server/claudeApi.ts +7 -7
- package/src/server/claudeCode.ts +11 -11
- package/src/server/commands.ts +73 -9
- package/src/server/createApp.ts +41 -7
- package/src/server/devWorker.ts +3 -3
- package/src/server/generatedSurfaces.ts +37 -4
- package/src/server/muxUpgrade.ts +47 -0
- package/src/server/preload.ts +13 -2
- package/src/server/render.ts +4 -1
- package/src/server/runCompiled.ts +1 -0
- package/src/server/seedBuffer.ts +31 -1
- package/src/server/serveDev.ts +83 -72
- package/src/server/surfaceOf.ts +3 -0
- package/src/server/testMount.ts +53 -0
- package/src/shared/ADDRESSES.ts +1 -1
- package/src/shared/ERROR_META.ts +3 -0
- package/src/shared/Schema.ts +1 -1
- package/src/shared/WORK.ts +14 -12
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/boundValue.ts +81 -0
- package/src/shared/index.ts +7 -2
- package/src/shared/jsonSchema.ts +25 -2
- package/src/shared/log.ts +11 -1
- package/src/shared/reactive/Keyed.ts +30 -6
- package/src/shared/reactive/PATCHES.ts +1 -1
- package/src/shared/reactive/Reactive.ts +118 -60
- package/src/shared/reactive/canonicalKey.ts +1 -1
- package/src/shared/reactive/index.ts +13 -5
- package/src/shared/reactive/recordWrites.ts +1 -1
- package/src/shared/reactive/watch.ts +26 -4
- package/src/shared/rpc.ts +6 -1
- package/src/shared/url.ts +7 -3
- package/src/shared/validateJson.ts +1 -1
- package/src/ui/ArmRegion.ts +2 -2
- package/src/ui/List.ts +9 -9
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +194 -36
- package/src/ui/Scope.ts +29 -0
- package/src/ui/adoptWalk.ts +3 -3
- package/src/ui/applyHead.ts +25 -3
- package/src/ui/bindProperty.ts +70 -22
- package/src/ui/index.ts +1 -1
- package/src/ui/leave.ts +5 -0
- package/src/ui/listenForRoutes.ts +14 -3
- package/src/ui/reconcile.ts +48 -28
- package/src/ui/regions.ts +36 -4
- package/src/ui/rows.ts +108 -28
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { type Dirent, readdirSync, realpathSync, statSync } from 'node:fs' // Bun exposes no sync directory listing or realpath of its own
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path' // Bun exposes no path helpers of its own
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getTouchingPropertyName,
|
|
5
|
+
type Node,
|
|
6
|
+
NodeFlags,
|
|
7
|
+
SyntaxKind,
|
|
8
|
+
} from 'typescript/unstable/ast'
|
|
4
9
|
import {
|
|
5
10
|
API,
|
|
6
11
|
type Symbol as CheckedSymbol,
|
|
7
12
|
type Checker,
|
|
13
|
+
type CompletionEntry,
|
|
8
14
|
type CompletionInfo,
|
|
15
|
+
CompletionItemKind,
|
|
9
16
|
NodeBuilderFlags,
|
|
10
17
|
type Project,
|
|
11
18
|
SignatureKind,
|
|
19
|
+
type Snapshot,
|
|
12
20
|
SymbolFlags,
|
|
21
|
+
type Type,
|
|
13
22
|
} from 'typescript/unstable/async'
|
|
14
23
|
import { placeOf } from '#server/placement.ts'
|
|
15
24
|
import { aliasedSymbol } from './aliasedSymbol.ts'
|
|
@@ -24,15 +33,17 @@ import {
|
|
|
24
33
|
moduleConfigShadow,
|
|
25
34
|
} from './checkedShadow.ts'
|
|
26
35
|
import type { Finding } from './Finding.ts'
|
|
36
|
+
import { isIgnored } from './isIgnored.ts'
|
|
27
37
|
import { CHECKED_VALUES } from './Lowering.ts'
|
|
28
38
|
import { diagnosisAt, lineOf } from './lineOf.ts'
|
|
29
39
|
import type { AbideFile } from './parseAbide.ts'
|
|
30
|
-
import { projectShadow } from './projectShadow.ts'
|
|
40
|
+
import { projectShadow, type Span } from './projectShadow.ts'
|
|
31
41
|
import { abideIn } from './reactiveTypes.ts'
|
|
32
42
|
import type { ParsedSource, ReadSource } from './readSources.ts'
|
|
33
43
|
import { readsIn, type ShadowSpans } from './readsIn.ts'
|
|
34
44
|
import { CHECKED_SUFFIX, SHADOW_SUFFIX, ShadowMap } from './ShadowMap.ts'
|
|
35
45
|
import { SOURCE_SUFFIX } from './splitImports.ts'
|
|
46
|
+
import { type TemplateContext, templateContext } from './templateContext.ts'
|
|
36
47
|
import { templateWarnings } from './templateWarnings.ts'
|
|
37
48
|
|
|
38
49
|
// Where 31.3 gets its answer. A `.abide` file is projected into a SHADOW `.ts` held in a virtual
|
|
@@ -250,6 +261,52 @@ export class TypeSession {
|
|
|
250
261
|
* rather than taking a second one.
|
|
251
262
|
*/
|
|
252
263
|
private latest: Project | undefined = undefined
|
|
264
|
+
private latestSnapshot: Snapshot | undefined = undefined
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Snapshots a later one replaced, released once no call is in flight (`hold`).
|
|
268
|
+
*
|
|
269
|
+
* THE CHECKER PROCESS KEEPS EVERY SNAPSHOT UNTIL IT IS RELEASED — `updateSnapshot` releases
|
|
270
|
+
* nothing — and each holds a whole program and checker. Dropped unreleased, a kept session grew
|
|
271
|
+
* without bound: an `abide lsp` checker reached 59 GB in two hours of watcher-driven snapshots.
|
|
272
|
+
* Released the moment it is replaced, a question still being answered over it throws, so the
|
|
273
|
+
* release waits for the last call out.
|
|
274
|
+
*/
|
|
275
|
+
private retired: Snapshot[] = []
|
|
276
|
+
private inFlight = 0
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Marks a call in flight until the returned value is disposed (`using`); the last one out
|
|
280
|
+
* releases what was retired meanwhile. A caller holding a `Project` across calls (`projectWith`)
|
|
281
|
+
* holds the session for as long.
|
|
282
|
+
*/
|
|
283
|
+
hold(): Disposable {
|
|
284
|
+
this.inFlight += 1
|
|
285
|
+
return {
|
|
286
|
+
[Symbol.dispose]: () => {
|
|
287
|
+
this.inFlight -= 1
|
|
288
|
+
if (this.inFlight === 0) this.releaseRetired()
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private retire(snapshot: Snapshot): void {
|
|
294
|
+
this.retired.push(snapshot)
|
|
295
|
+
if (this.inFlight === 0) this.releaseRetired()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private releaseRetired(): void {
|
|
299
|
+
for (const snapshot of this.retired) {
|
|
300
|
+
// A process that has stopped answering rejects the release, and dies with what it held.
|
|
301
|
+
const release = snapshot.dispose().catch(() => {})
|
|
302
|
+
this.releasing.add(release)
|
|
303
|
+
void release.finally(() => this.releasing.delete(release))
|
|
304
|
+
}
|
|
305
|
+
this.retired = []
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Releases sent and not yet answered, which `close` lets finish before ending the connection. */
|
|
309
|
+
private releasing = new Set<Promise<void>>()
|
|
253
310
|
|
|
254
311
|
/**
|
|
255
312
|
* Paths edited or deleted on disk since the last snapshot, or `everything`.
|
|
@@ -397,10 +454,12 @@ export class TypeSession {
|
|
|
397
454
|
// after it: a dropped cache re-reads what the project already listed, and a shadow written
|
|
398
455
|
// for a page in a directory it never listed is not among them.
|
|
399
456
|
if (everything)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
457
|
+
this.retire(
|
|
458
|
+
await this.api.updateSnapshot({
|
|
459
|
+
openProjects: [this.opened],
|
|
460
|
+
fileChanges: { invalidateAll: true },
|
|
461
|
+
}),
|
|
462
|
+
)
|
|
404
463
|
const snapshot = await this.api.updateSnapshot({
|
|
405
464
|
openProjects: [this.opened],
|
|
406
465
|
fileChanges: {
|
|
@@ -409,6 +468,8 @@ export class TypeSession {
|
|
|
409
468
|
deleted: edits.deleted,
|
|
410
469
|
},
|
|
411
470
|
})
|
|
471
|
+
if (this.latestSnapshot !== undefined) this.retire(this.latestSnapshot)
|
|
472
|
+
this.latestSnapshot = snapshot
|
|
412
473
|
this.latest = snapshot.getProjects()[0]
|
|
413
474
|
return this.latest
|
|
414
475
|
}
|
|
@@ -457,6 +518,7 @@ export class TypeSession {
|
|
|
457
518
|
async projectWith(
|
|
458
519
|
modules: readonly string[],
|
|
459
520
|
): Promise<Project | undefined> {
|
|
521
|
+
using _held = this.hold()
|
|
460
522
|
const changed: string[] = []
|
|
461
523
|
for (const path of modules) {
|
|
462
524
|
const canonical = this.canonical(path)
|
|
@@ -475,7 +537,7 @@ export class TypeSession {
|
|
|
475
537
|
if (found[at] === undefined) missing.push(modules[at] as string)
|
|
476
538
|
return missing.length === 0
|
|
477
539
|
? project
|
|
478
|
-
: this.currentProject(missing, [...missing])
|
|
540
|
+
: await this.currentProject(missing, [...missing])
|
|
479
541
|
}
|
|
480
542
|
|
|
481
543
|
/**
|
|
@@ -562,6 +624,7 @@ export class TypeSession {
|
|
|
562
624
|
text?: string
|
|
563
625
|
})[],
|
|
564
626
|
): Promise<{ answers: CheckerAnswer[]; warnings: Diagnosis[] }> {
|
|
627
|
+
using _held = this.hold()
|
|
565
628
|
const answered = await this.classify(sources)
|
|
566
629
|
for (let at = 0; at < sources.length; at += 1) {
|
|
567
630
|
const source = sources[at] as (typeof sources)[number]
|
|
@@ -639,6 +702,7 @@ export class TypeSession {
|
|
|
639
702
|
servers: projection.servers,
|
|
640
703
|
scripts: projection.scripts,
|
|
641
704
|
positions: projection.positions,
|
|
705
|
+
items: new Set(projection.items.values()),
|
|
642
706
|
},
|
|
643
707
|
})
|
|
644
708
|
}
|
|
@@ -665,7 +729,7 @@ export class TypeSession {
|
|
|
665
729
|
// a wrong page, with nothing about it saying so.
|
|
666
730
|
if (sourceFile === undefined)
|
|
667
731
|
throw new Error(
|
|
668
|
-
`abide: ${source.path} is not inside the project ${this.tsconfig}, so no name in it can be typed (31.3). Give the app a tsconfig whose include reaches it.`,
|
|
732
|
+
`abide: ${source.path} is not inside the project ${this.tsconfig}, so no name in it can be typed (RULEBOOK 31.3). Give the app a tsconfig whose include reaches it.`,
|
|
669
733
|
)
|
|
670
734
|
const read = await readsIn(
|
|
671
735
|
checker,
|
|
@@ -676,19 +740,25 @@ export class TypeSession {
|
|
|
676
740
|
source.file,
|
|
677
741
|
)
|
|
678
742
|
answers[at] = read.answer
|
|
679
|
-
for (const warning of read.warnings)
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
),
|
|
688
|
-
'warning',
|
|
689
|
-
warning.message,
|
|
690
|
-
),
|
|
743
|
+
for (const warning of read.warnings) {
|
|
744
|
+
const offset = query.map.toSource(
|
|
745
|
+
query.map.shadowPath,
|
|
746
|
+
warning.at,
|
|
747
|
+
)
|
|
748
|
+
if (
|
|
749
|
+
source.text === undefined ||
|
|
750
|
+
!isIgnored(source.text, offset, warning.message)
|
|
691
751
|
)
|
|
752
|
+
warnings.push(
|
|
753
|
+
diagnosisAt(
|
|
754
|
+
source.path,
|
|
755
|
+
source.text,
|
|
756
|
+
offset,
|
|
757
|
+
'warning',
|
|
758
|
+
warning.message,
|
|
759
|
+
),
|
|
760
|
+
)
|
|
761
|
+
}
|
|
692
762
|
}),
|
|
693
763
|
)
|
|
694
764
|
return { answers, warnings }
|
|
@@ -707,6 +777,7 @@ export class TypeSession {
|
|
|
707
777
|
within: string,
|
|
708
778
|
sources: readonly Omit<ParsedSource, 'quiet'>[],
|
|
709
779
|
): Promise<{ ambient: Diagnosis[]; unkeyed: Diagnosis[] }> {
|
|
780
|
+
using _held = this.hold()
|
|
710
781
|
const ambient: Diagnosis[] = []
|
|
711
782
|
const unkeyed: Diagnosis[] = []
|
|
712
783
|
const project = await this.currentProject()
|
|
@@ -813,6 +884,7 @@ export class TypeSession {
|
|
|
813
884
|
app: Pick<AppLayout, 'root' | 'addresses' | 'handlers'>,
|
|
814
885
|
scripts = true,
|
|
815
886
|
): Promise<{ diagnoses: Diagnosis[]; refused: Set<string> }> {
|
|
887
|
+
using _held = this.hold()
|
|
816
888
|
const within = app.root
|
|
817
889
|
const created: string[] = []
|
|
818
890
|
const changed: string[] = []
|
|
@@ -942,8 +1014,18 @@ export class TypeSession {
|
|
|
942
1014
|
),
|
|
943
1015
|
),
|
|
944
1016
|
)
|
|
945
|
-
const
|
|
946
|
-
|
|
1017
|
+
const reported = [...refused, ...(await Promise.all(reports)).flat()]
|
|
1018
|
+
reported.sort(byPlace)
|
|
1019
|
+
// A component's props type is checked twice — in its `props` call and in what a tag is held
|
|
1020
|
+
// to (`checkedShadow`) — and both copies place a finding in it at the one place written.
|
|
1021
|
+
const found: Diagnosis[] = []
|
|
1022
|
+
const placed = new Set<string>()
|
|
1023
|
+
for (const diagnosis of reported) {
|
|
1024
|
+
const key = `${diagnosis.path}:${diagnosis.line}:${diagnosis.column}:${diagnosis.code}:${diagnosis.message}`
|
|
1025
|
+
if (placed.has(key)) continue
|
|
1026
|
+
placed.add(key)
|
|
1027
|
+
found.push(diagnosis)
|
|
1028
|
+
}
|
|
947
1029
|
return { diagnoses: found, refused: refusing }
|
|
948
1030
|
}
|
|
949
1031
|
|
|
@@ -976,9 +1058,9 @@ export class TypeSession {
|
|
|
976
1058
|
* The members that may be written at `offset` in a `.abide`, or `undefined` where none can be.
|
|
977
1059
|
*
|
|
978
1060
|
* 38.98 — ASKED WHERE A HOVER IS (`askedAt`), so `config().` offers the fields 27.13 declared and
|
|
979
|
-
* a name read by name offers its value's members (31.1). MEMBERS ALONE
|
|
980
|
-
* API rejects ("completion list needs auto imports"), which
|
|
981
|
-
* nothing offered.
|
|
1061
|
+
* a name read by name offers its value's members (31.1). MEMBERS ALONE in a script or an
|
|
1062
|
+
* expression: at an identifier the API rejects ("completion list needs auto imports"), which
|
|
1063
|
+
* the language server answers as nothing offered. Markup is answered apart (38.117).
|
|
982
1064
|
*/
|
|
983
1065
|
async complete(
|
|
984
1066
|
path: string,
|
|
@@ -986,15 +1068,80 @@ export class TypeSession {
|
|
|
986
1068
|
offset: number,
|
|
987
1069
|
triggerCharacter?: string,
|
|
988
1070
|
): Promise<CompletionInfo | undefined> {
|
|
1071
|
+
using _held = this.hold()
|
|
1072
|
+
const template = templateContext(text, offset)
|
|
1073
|
+
if (template !== undefined)
|
|
1074
|
+
return await this.completeTemplate(path, template)
|
|
989
1075
|
const at = this.askedAt(path, text, offset)
|
|
990
1076
|
if (at === undefined) return undefined
|
|
991
|
-
return at.project.checker.getCompletionsAtPosition(
|
|
1077
|
+
return await at.project.checker.getCompletionsAtPosition(
|
|
992
1078
|
at.asked.path,
|
|
993
1079
|
at.asked.position,
|
|
994
1080
|
{ triggerCharacter },
|
|
995
1081
|
)
|
|
996
1082
|
}
|
|
997
1083
|
|
|
1084
|
+
/**
|
|
1085
|
+
* 38.117 — what markup offers: a block keyword, a component in scope, or a component's props.
|
|
1086
|
+
*
|
|
1087
|
+
* A PROP IS ASKED OF THE LAST LOWERING, by the component's name: the tag being typed is text the
|
|
1088
|
+
* parser refuses, and the import the name resolves through is the same in the lowering before it.
|
|
1089
|
+
*/
|
|
1090
|
+
private async completeTemplate(
|
|
1091
|
+
path: string,
|
|
1092
|
+
template: TemplateContext,
|
|
1093
|
+
): Promise<CompletionInfo> {
|
|
1094
|
+
const entries: CompletionEntry[] = []
|
|
1095
|
+
if (template.kind !== 'attribute') {
|
|
1096
|
+
const kind =
|
|
1097
|
+
template.kind === 'block'
|
|
1098
|
+
? CompletionItemKind.Keyword
|
|
1099
|
+
: CompletionItemKind.Class
|
|
1100
|
+
for (const name of template.offered) entries.push({ name, kind })
|
|
1101
|
+
return { isIncomplete: false, entries }
|
|
1102
|
+
}
|
|
1103
|
+
const lowered = this.loweredForInspect.get(this.keyOf(path))
|
|
1104
|
+
const project = this.latest
|
|
1105
|
+
if (lowered === undefined || project === undefined)
|
|
1106
|
+
return { isIncomplete: false, entries }
|
|
1107
|
+
const checker = project.checker
|
|
1108
|
+
const found = await checker.resolveName(
|
|
1109
|
+
template.tag,
|
|
1110
|
+
SymbolFlags.Value,
|
|
1111
|
+
{ document: lowered.map.shadowPath, position: 0 },
|
|
1112
|
+
)
|
|
1113
|
+
const record =
|
|
1114
|
+
found === undefined
|
|
1115
|
+
? undefined
|
|
1116
|
+
: await propsRecordOf(
|
|
1117
|
+
checker,
|
|
1118
|
+
await aliasedSymbol(checker, found),
|
|
1119
|
+
)
|
|
1120
|
+
if (record === undefined) return { isIncomplete: false, entries }
|
|
1121
|
+
const unwritten = (await checker.getPropertiesOfType(record)).filter(
|
|
1122
|
+
(prop) => !template.written.includes(prop.name),
|
|
1123
|
+
)
|
|
1124
|
+
for (const entry of await Promise.all(
|
|
1125
|
+
unwritten.map(async (prop) => {
|
|
1126
|
+
const type = await checker.getTypeOfSymbol(prop)
|
|
1127
|
+
return {
|
|
1128
|
+
name: prop.name,
|
|
1129
|
+
kind: CompletionItemKind.Property,
|
|
1130
|
+
detail:
|
|
1131
|
+
type === undefined
|
|
1132
|
+
? undefined
|
|
1133
|
+
: await checker.typeToString(
|
|
1134
|
+
type,
|
|
1135
|
+
undefined,
|
|
1136
|
+
NodeBuilderFlags.NoTruncation,
|
|
1137
|
+
),
|
|
1138
|
+
}
|
|
1139
|
+
}),
|
|
1140
|
+
))
|
|
1141
|
+
entries.push(entry)
|
|
1142
|
+
return { isIncomplete: false, entries }
|
|
1143
|
+
}
|
|
1144
|
+
|
|
998
1145
|
/**
|
|
999
1146
|
* The hover signature, documentation and definitions of the name at `offset` in a `.abide`.
|
|
1000
1147
|
*
|
|
@@ -1013,6 +1160,7 @@ export class TypeSession {
|
|
|
1013
1160
|
text: string,
|
|
1014
1161
|
offset: number,
|
|
1015
1162
|
): Promise<Inspection | undefined> {
|
|
1163
|
+
using _held = this.hold()
|
|
1016
1164
|
const at = this.askedAt(path, text, offset)
|
|
1017
1165
|
if (at === undefined) return undefined
|
|
1018
1166
|
const { project, asked } = at
|
|
@@ -1061,6 +1209,181 @@ export class TypeSession {
|
|
|
1061
1209
|
}
|
|
1062
1210
|
}
|
|
1063
1211
|
|
|
1212
|
+
/**
|
|
1213
|
+
* Every place the name at `offset` in a `.abide` is written, across the files the checker holds,
|
|
1214
|
+
* or `undefined` where nothing is named there.
|
|
1215
|
+
*
|
|
1216
|
+
* 38.116 — ASKED OF EVERY PLACE THE CHECKER READS THE OFFSET AT (`ShadowMap.placesFrom`), because
|
|
1217
|
+
* the checked module rewrites a name read by name into another symbol's read: the projection is
|
|
1218
|
+
* where a reactive's markup uses are the declaration's references. A place is kept only where
|
|
1219
|
+
* both layers copied it from the author's text and that text is the name, so scaffolding a
|
|
1220
|
+
* lowering wrote is never a place to rename.
|
|
1221
|
+
*
|
|
1222
|
+
* AND ASKED AGAIN WHERE A REFERENCE IS TEXT THE CHECKED MODULE COPIES TWICE. A component's props
|
|
1223
|
+
* type is a second copy of its `props` call (`checkedShadow`), and a tag's keys are typed by
|
|
1224
|
+
* that copy: the checker answers them as another symbol's references, so a reference landing
|
|
1225
|
+
* on one copy is asked at every place the author's text is read.
|
|
1226
|
+
*/
|
|
1227
|
+
async references(
|
|
1228
|
+
path: string,
|
|
1229
|
+
text: string,
|
|
1230
|
+
offset: number,
|
|
1231
|
+
): Promise<References | undefined> {
|
|
1232
|
+
using _held = this.hold()
|
|
1233
|
+
const lowered = this.loweredForInspect.get(this.keyOf(path))
|
|
1234
|
+
const project = this.latest
|
|
1235
|
+
if (
|
|
1236
|
+
lowered === undefined ||
|
|
1237
|
+
lowered.text !== text ||
|
|
1238
|
+
project === undefined
|
|
1239
|
+
)
|
|
1240
|
+
return undefined
|
|
1241
|
+
const asked = lowered.map.placesFrom(offset)
|
|
1242
|
+
if (asked.length === 0) return undefined
|
|
1243
|
+
const askedAt = new Set<string>()
|
|
1244
|
+
for (const place of asked)
|
|
1245
|
+
askedAt.add(`${place.path}:${place.position}`)
|
|
1246
|
+
let name: Place | undefined
|
|
1247
|
+
let spelled = ''
|
|
1248
|
+
let renamable = true
|
|
1249
|
+
const seen = new Set<string>()
|
|
1250
|
+
const occurrences: Occurrence[] = []
|
|
1251
|
+
for (let index = 0; index < asked.length; index += 1) {
|
|
1252
|
+
const { path: file, position } = asked[index] as {
|
|
1253
|
+
path: string
|
|
1254
|
+
position: number
|
|
1255
|
+
}
|
|
1256
|
+
const sourceFile = await project.program.getSourceFile(file)
|
|
1257
|
+
if (sourceFile === undefined) continue
|
|
1258
|
+
const node = getTouchingPropertyName(sourceFile, position)
|
|
1259
|
+
if (node.kind !== SyntaxKind.Identifier) continue
|
|
1260
|
+
const written = node.getText(sourceFile)
|
|
1261
|
+
if (name === undefined) {
|
|
1262
|
+
const copied = this.copiedSpan(node, written)
|
|
1263
|
+
name = copied === undefined ? undefined : placeOfSpan(copied)
|
|
1264
|
+
spelled = written
|
|
1265
|
+
}
|
|
1266
|
+
const entries = await project.checker.getReferencedSymbolsForNode(
|
|
1267
|
+
node,
|
|
1268
|
+
position,
|
|
1269
|
+
)
|
|
1270
|
+
for (const entry of entries) {
|
|
1271
|
+
const definition = await entry.definition.resolve(project)
|
|
1272
|
+
if (definition === undefined) continue
|
|
1273
|
+
const declaredIn = definition.getSourceFile().fileName
|
|
1274
|
+
if (!this.authored(declaredIn)) renamable = false
|
|
1275
|
+
const declared = (definition as { name?: Node }).name
|
|
1276
|
+
const references = await Promise.all(
|
|
1277
|
+
entry.references.map((handle) => handle.resolve(project)),
|
|
1278
|
+
)
|
|
1279
|
+
for (const reference of references) {
|
|
1280
|
+
if (reference === undefined) continue
|
|
1281
|
+
const copied = this.copiedSpan(reference, written)
|
|
1282
|
+
if (copied === undefined) continue
|
|
1283
|
+
const declaration =
|
|
1284
|
+
declared !== undefined &&
|
|
1285
|
+
declared.pos === reference.pos &&
|
|
1286
|
+
declaredIn === reference.getSourceFile().fileName
|
|
1287
|
+
// Text the checker reads at more than one place is asked at every one.
|
|
1288
|
+
const copies = copied.map?.placesFrom(copied.start) ?? []
|
|
1289
|
+
if (copies.length > 2)
|
|
1290
|
+
for (const place of copies) {
|
|
1291
|
+
const at = `${place.path}:${place.position}`
|
|
1292
|
+
if (askedAt.has(at)) continue
|
|
1293
|
+
askedAt.add(at)
|
|
1294
|
+
asked.push(place)
|
|
1295
|
+
}
|
|
1296
|
+
const placed = placeOfSpan(copied)
|
|
1297
|
+
const key = `${placed.path}:${placed.start.line}:${placed.start.column}`
|
|
1298
|
+
if (seen.has(key)) continue
|
|
1299
|
+
seen.add(key)
|
|
1300
|
+
const holder = reference.parent
|
|
1301
|
+
const declaresHolder =
|
|
1302
|
+
definition.pos === holder.pos &&
|
|
1303
|
+
declaredIn === reference.getSourceFile().fileName
|
|
1304
|
+
const shorthand =
|
|
1305
|
+
holder.kind ===
|
|
1306
|
+
SyntaxKind.ShorthandPropertyAssignment ||
|
|
1307
|
+
(holder.kind === SyntaxKind.BindingElement &&
|
|
1308
|
+
(holder as { propertyName?: Node }).propertyName ===
|
|
1309
|
+
undefined)
|
|
1310
|
+
occurrences.push({
|
|
1311
|
+
...placed,
|
|
1312
|
+
declaration,
|
|
1313
|
+
// A shorthand names a property and a binding at once, and renaming one
|
|
1314
|
+
// writes the other out beside it: the binding element declares the local
|
|
1315
|
+
// and the shorthand assignment the property.
|
|
1316
|
+
shorthand: !shorthand
|
|
1317
|
+
? undefined
|
|
1318
|
+
: declaresHolder ===
|
|
1319
|
+
(holder.kind === SyntaxKind.BindingElement)
|
|
1320
|
+
? 'local'
|
|
1321
|
+
: 'property',
|
|
1322
|
+
})
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (name === undefined) return undefined
|
|
1327
|
+
return { name, written: spelled, renamable, occurrences }
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Whether a file the checker read is one an author wrote and can edit: neither a library's
|
|
1332
|
+
* declaration, nor a dependency's, nor scaffolding this session serves that stands for no `.abide`.
|
|
1333
|
+
*/
|
|
1334
|
+
private authored(fileName: string): boolean {
|
|
1335
|
+
if (fileName.endsWith('.d.ts') || fileName.includes('/node_modules/'))
|
|
1336
|
+
return false
|
|
1337
|
+
return (
|
|
1338
|
+
ShadowMap.fileFor(this.canonical(fileName)) !== undefined ||
|
|
1339
|
+
!this.shadows.has(this.keyOf(fileName))
|
|
1340
|
+
)
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Where a node the checker read stands in the file the author wrote, with that file's map where
|
|
1345
|
+
* it is a `.abide`, or `undefined` where the author did not write `name` there: in scaffolding,
|
|
1346
|
+
* or in a file this session serves.
|
|
1347
|
+
*/
|
|
1348
|
+
private copiedSpan(node: Node, name: string): CopiedSpan | undefined {
|
|
1349
|
+
const file = node.getSourceFile()
|
|
1350
|
+
let start = node.getStart(file)
|
|
1351
|
+
let end = node.end
|
|
1352
|
+
// A string literal names a property by its text, inside the quotes.
|
|
1353
|
+
if (node.kind === SyntaxKind.StringLiteral) {
|
|
1354
|
+
start += 1
|
|
1355
|
+
end -= 1
|
|
1356
|
+
}
|
|
1357
|
+
const canonical = this.canonical(file.fileName)
|
|
1358
|
+
const abidePath = ShadowMap.fileFor(canonical)
|
|
1359
|
+
if (abidePath === undefined) {
|
|
1360
|
+
if (!this.authored(file.fileName)) return undefined
|
|
1361
|
+
if (file.text.slice(start, end) !== name) return undefined
|
|
1362
|
+
return {
|
|
1363
|
+
path: file.fileName,
|
|
1364
|
+
text: file.text,
|
|
1365
|
+
start,
|
|
1366
|
+
end,
|
|
1367
|
+
map: undefined,
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
const lowered = this.loweredForInspect.get(this.keyOf(abidePath))
|
|
1371
|
+
const span = lowered?.map.copiedToSource(canonical, start, end)
|
|
1372
|
+
if (
|
|
1373
|
+
lowered === undefined ||
|
|
1374
|
+
span === undefined ||
|
|
1375
|
+
lowered.text.slice(span.start, span.end) !== name
|
|
1376
|
+
)
|
|
1377
|
+
return undefined
|
|
1378
|
+
return {
|
|
1379
|
+
path: abidePath,
|
|
1380
|
+
text: lowered.text,
|
|
1381
|
+
start: span.start,
|
|
1382
|
+
end: span.end,
|
|
1383
|
+
map: lowered.map,
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1064
1387
|
/**
|
|
1065
1388
|
* A span the checker read, placed in the file the author wrote.
|
|
1066
1389
|
*
|
|
@@ -1120,6 +1443,13 @@ export class TypeSession {
|
|
|
1120
1443
|
}
|
|
1121
1444
|
).client
|
|
1122
1445
|
const child = client.process
|
|
1446
|
+
// A release still being written when the connection ends is a write after end, thrown
|
|
1447
|
+
// where nothing catches it. Bounded, for the same process that has stopped answering.
|
|
1448
|
+
if (this.releasing.size > 0)
|
|
1449
|
+
await Promise.race([
|
|
1450
|
+
Promise.allSettled(this.releasing),
|
|
1451
|
+
Bun.sleep(RELEASE_GRACE_MS),
|
|
1452
|
+
])
|
|
1123
1453
|
await client.close()
|
|
1124
1454
|
child?.kill()
|
|
1125
1455
|
}
|
|
@@ -1127,6 +1457,9 @@ export class TypeSession {
|
|
|
1127
1457
|
|
|
1128
1458
|
const AMBIENT = 'abide-check.d.ts'
|
|
1129
1459
|
|
|
1460
|
+
// How long `close` waits on releases in flight before ending the connection under them.
|
|
1461
|
+
const RELEASE_GRACE_MS = 1000
|
|
1462
|
+
|
|
1130
1463
|
// What the checker asks for when it resolves an import of `x.abide` (`standIn`).
|
|
1131
1464
|
const DECLARATION_SUFFIX = '.abide.d.ts'
|
|
1132
1465
|
// Beside the app's tsconfig, so `extends` and every path in it resolve as they would from there.
|
|
@@ -1189,6 +1522,45 @@ export type Inspection = {
|
|
|
1189
1522
|
definitions: Place[]
|
|
1190
1523
|
}
|
|
1191
1524
|
|
|
1525
|
+
/**
|
|
1526
|
+
* The name under a cursor and every place it is written.
|
|
1527
|
+
*
|
|
1528
|
+
* 38.116 — `name` is where the cursor's own name stands; `renamable` is false where any symbol it
|
|
1529
|
+
* names is declared somewhere an author does not write.
|
|
1530
|
+
*/
|
|
1531
|
+
export type References = {
|
|
1532
|
+
name: Place
|
|
1533
|
+
/** The name as it is written. */
|
|
1534
|
+
written: string
|
|
1535
|
+
renamable: boolean
|
|
1536
|
+
occurrences: Occurrence[]
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* One place a name is written: whether it is where the name is declared, and where it is a
|
|
1541
|
+
* shorthand, which side of it a rename renames — the `local` binding or the `property`.
|
|
1542
|
+
*/
|
|
1543
|
+
export type Occurrence = Place & {
|
|
1544
|
+
declaration: boolean
|
|
1545
|
+
shorthand: 'local' | 'property' | undefined
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/** A span of a file the author wrote, and the map of the `.abide` it is in, where it is in one. */
|
|
1549
|
+
type CopiedSpan = Span & {
|
|
1550
|
+
path: string
|
|
1551
|
+
text: string
|
|
1552
|
+
map: ShadowMap | undefined
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/** A `CopiedSpan` as lines and columns. */
|
|
1556
|
+
function placeOfSpan(span: CopiedSpan): Place {
|
|
1557
|
+
return {
|
|
1558
|
+
path: span.path,
|
|
1559
|
+
start: lineOf(span.text, span.start),
|
|
1560
|
+
end: lineOf(span.text, span.end),
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1192
1564
|
/**
|
|
1193
1565
|
* The props type a component's tag is held to, printed.
|
|
1194
1566
|
*
|
|
@@ -1198,16 +1570,23 @@ async function propsTypeOf(
|
|
|
1198
1570
|
checker: Checker,
|
|
1199
1571
|
component: CheckedSymbol,
|
|
1200
1572
|
): Promise<string> {
|
|
1573
|
+
const record = await propsRecordOf(checker, component)
|
|
1574
|
+
return record === undefined
|
|
1575
|
+
? 'any'
|
|
1576
|
+
: checker.typeToString(record, undefined, NodeBuilderFlags.NoTruncation)
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/** The record a component is called with — the props its tag is held to (33.4). */
|
|
1580
|
+
async function propsRecordOf(
|
|
1581
|
+
checker: Checker,
|
|
1582
|
+
component: CheckedSymbol,
|
|
1583
|
+
): Promise<Type | undefined> {
|
|
1201
1584
|
const type = await checker.getTypeOfSymbol(component)
|
|
1202
1585
|
const call =
|
|
1203
1586
|
type === undefined
|
|
1204
1587
|
? undefined
|
|
1205
1588
|
: (await checker.getSignaturesOfType(type, SignatureKind.Call))[0]
|
|
1206
|
-
|
|
1207
|
-
call === undefined ? undefined : await checker.getParameterType(call, 0)
|
|
1208
|
-
return record === undefined
|
|
1209
|
-
? 'any'
|
|
1210
|
-
: checker.typeToString(record, undefined, NodeBuilderFlags.NoTruncation)
|
|
1589
|
+
return call === undefined ? undefined : checker.getParameterType(call, 0)
|
|
1211
1590
|
}
|
|
1212
1591
|
|
|
1213
1592
|
/**
|
|
@@ -82,6 +82,11 @@ export type AppLayout = {
|
|
|
82
82
|
addresses: string[]
|
|
83
83
|
/** What the tree refuses, each at its file; a build stops naming every one. */
|
|
84
84
|
refused: Diagnosis[]
|
|
85
|
+
/**
|
|
86
|
+
* What the parser refuses of each `.abide` as the disk holds it, each at its file. Apart from
|
|
87
|
+
* `refused` because it is the TEXT's: an editor's check parses the text it holds instead (38.34).
|
|
88
|
+
*/
|
|
89
|
+
unparsed: Diagnosis[]
|
|
85
90
|
/** The resolver every import was followed with, whose answers it keeps. */
|
|
86
91
|
resolve: (specifier: string, from: string) => string | undefined
|
|
87
92
|
}
|
|
@@ -99,11 +104,19 @@ export async function appLayout(root: string): Promise<AppLayout> {
|
|
|
99
104
|
const { abide, handlers, modules, published } = appFiles(root, refusals)
|
|
100
105
|
await refuseModuleHooks(root, modules, refusals)
|
|
101
106
|
const resolve = resolverIn()
|
|
107
|
+
const unparsed = new Refusals(root, true)
|
|
102
108
|
const own = parseSources(
|
|
103
109
|
await textsAt(abide.map((path) => `${root}/${path}`)),
|
|
110
|
+
unparsed,
|
|
111
|
+
)
|
|
112
|
+
const found = await withDependencies(
|
|
113
|
+
root,
|
|
114
|
+
own,
|
|
115
|
+
handlers,
|
|
116
|
+
resolve,
|
|
104
117
|
refusals,
|
|
118
|
+
unparsed,
|
|
105
119
|
)
|
|
106
|
-
const found = await withDependencies(root, own, handlers, resolve, refusals)
|
|
107
120
|
const invoked = componentGraph(found.sources, resolve).invoked
|
|
108
121
|
const sources: PlacedSource[] = []
|
|
109
122
|
for (const source of found.sources) {
|
|
@@ -150,6 +163,7 @@ export async function appLayout(root: string): Promise<AppLayout> {
|
|
|
150
163
|
mounted: found.mounted,
|
|
151
164
|
addresses: [...addresses].sort(),
|
|
152
165
|
refused: refusals.found,
|
|
166
|
+
unparsed: unparsed.found,
|
|
153
167
|
resolve,
|
|
154
168
|
}
|
|
155
169
|
}
|
|
@@ -182,6 +196,8 @@ export async function withDependencies(
|
|
|
182
196
|
own: readonly string[],
|
|
183
197
|
resolve: (specifier: string, from: string) => string | undefined,
|
|
184
198
|
refusals: Refusals,
|
|
199
|
+
/** Where what the parser refuses of a file reached here goes, where apart from `refusals`. */
|
|
200
|
+
unparsed: Refusals = refusals,
|
|
185
201
|
): Promise<{
|
|
186
202
|
sources: ParsedSource[]
|
|
187
203
|
handlers: HandlerFile[]
|
|
@@ -215,7 +231,7 @@ export async function withDependencies(
|
|
|
215
231
|
if ((thrown as { code?: unknown }).code === 'ENOENT') return
|
|
216
232
|
throw thrown
|
|
217
233
|
}
|
|
218
|
-
for (const one of parseSources([{ path, text }],
|
|
234
|
+
for (const one of parseSources([{ path, text }], unparsed)) {
|
|
219
235
|
byPath.set(one.path, one)
|
|
220
236
|
all.push(one)
|
|
221
237
|
}
|