@abide/abide 0.123.3 → 0.124.1
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/ABIDE.md +8 -5
- package/CHANGELOG.md +38 -0
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +29 -16
- package/docs/abide.md +999 -459
- package/package.json +1 -1
- package/src/compiler/ARRAY_MUTATORS.ts +20 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +46 -4
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowHost.ts +670 -0
- package/src/compiler/ShadowMap.ts +1 -1
- package/src/compiler/TypeSession.ts +71 -1715
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +44 -17
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/formatAbide.ts +5 -6
- package/src/compiler/handlersUnder.ts +16 -35
- package/src/compiler/projectShadow.ts +1 -0
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readsIn.ts +180 -81
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +26 -20
- package/src/compiler/splitImports.ts +2 -2
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/DesktopSession.ts +213 -0
- package/src/server/Followers.ts +399 -0
- package/src/server/MODULE_QUERIES.ts +6 -2
- package/src/server/PageRender.ts +201 -0
- package/src/server/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +6 -70
- package/src/server/answerRpc.ts +48 -260
- package/src/server/answerSocket.ts +41 -463
- package/src/server/appKit.ts +1 -1
- package/src/server/argsContract.ts +1 -1
- package/src/server/createApp.ts +8 -2
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +1 -1
- package/src/server/index.ts +1 -1
- package/src/server/installServer.ts +5 -9
- package/src/server/invokeHandler.ts +230 -0
- package/src/server/mountHandlers.ts +184 -0
- package/src/server/openDesktopWindow.ts +308 -0
- package/src/server/preload.ts +18 -0
- package/src/server/principal.ts +1 -1
- package/src/server/remote.ts +1 -1
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runDesktop.ts +19 -447
- package/src/server/seedBuffer.ts +11 -11
- package/src/server/servedApp.ts +1 -1
- package/src/server/shell.ts +1 -1
- package/src/server/surfaceOf.ts +1 -1
- package/src/server/versionOf.ts +1 -1
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- package/src/shared/index.ts +1 -1
- package/src/shared/{note.ts → issueReport.ts} +22 -0
- package/src/shared/loadCall.ts +66 -0
- package/src/shared/reactive/Keyed.ts +23 -15
- package/src/shared/reactive/Reactive.ts +46 -1002
- package/src/shared/reactive/byOf.ts +2 -5
- package/src/shared/reactive/callLoad.ts +8 -14
- package/src/shared/reactive/cellShape.ts +734 -0
- package/src/shared/reactive/index.ts +12 -24
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +8 -5
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- package/src/ui/Router.ts +53 -69
- package/src/ui/outboxQueue.ts +2 -8
- package/src/ui/outcomeOf.ts +120 -0
- package/src/ui/socketWire.ts +3 -3
- package/src/ui/wire.ts +9 -10
- package/src/shared/LOAD_OFFLINE.ts +0 -15
- package/src/shared/LOAD_SIGNAL.ts +0 -10
- package/src/shared/issuesOf.ts +0 -18
- package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
package/package.json
CHANGED
|
@@ -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
|
+
}
|