@abide/abide 0.41.1 → 0.42.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/AGENTS.md +19 -13
- package/CHANGELOG.md +48 -0
- package/README.md +13 -14
- package/package.json +1 -1
- package/src/abideLsp.ts +46 -8
- package/src/abideResolverPlugin.ts +3 -5
- package/src/lib/server/runtime/devClientFingerprint.ts +2 -1
- package/src/lib/shared/escapeRegex.ts +9 -0
- package/src/lib/shared/fileName.ts +9 -0
- package/src/lib/shared/fileStem.ts +3 -1
- package/src/lib/shared/programNameForPackage.ts +3 -1
- package/src/lib/shared/stripImport.ts +3 -1
- package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +45 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/compileShadow.ts +42 -10
- package/src/lib/ui/compile/createShadowLanguageService.ts +48 -0
- package/src/lib/ui/compile/encodeSemanticTokens.ts +37 -0
- package/src/lib/ui/compile/generateBuild.ts +2 -2
- package/src/lib/ui/compile/generateSSR.ts +6 -6
- package/src/lib/ui/compile/makeVarNamer.ts +10 -0
- package/src/lib/ui/compile/offsetToPosition.ts +9 -0
- package/src/lib/ui/compile/parseTemplate.ts +524 -104
- package/src/lib/ui/compile/structuralBlockTokens.ts +101 -0
- package/src/lib/ui/compile/types/SemanticToken.ts +11 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +9 -0
- package/src/lib/ui/dom/appendText.ts +1 -5
- package/src/lib/ui/dom/applyResolved.ts +1 -5
- package/src/lib/ui/dom/isComment.ts +6 -0
- package/src/lib/ui/runtime/createDoc.ts +3 -3
- package/src/lib/ui/runtime/pathSegments.ts +10 -0
- package/template/.zed/settings.json +4 -0
- package/template/src/ui/pages/page.abide +4 -4
package/AGENTS.md
CHANGED
|
@@ -143,23 +143,29 @@ import: `scope`, `props` / `prop`, `effect`, `html`, `snippet`.
|
|
|
143
143
|
| `{...expr}` | Spread keys as attributes (elements) or props (components). |
|
|
144
144
|
| `attach={fn}` | Build-time element attachment with optional teardown. |
|
|
145
145
|
|
|
146
|
-
**Control flow** —
|
|
147
|
-
|
|
148
|
-
| Form
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
|
|
|
146
|
+
**Control flow** — `{#…}` blocks (each head reads as the JS clause it lowers to):
|
|
147
|
+
|
|
148
|
+
| Form | Meaning |
|
|
149
|
+
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
150
|
+
| `{#if c}` / `{:else if c}` / `{:else}` / `{/if}` | Conditional, source-order, re-evaluated reactively. |
|
|
151
|
+
| `{#for x of list}` / `{#for x, i of list by x.id}` / `{/for}` | Keyed list (`for…of`); `, i` index reactive; `by` key reconciles rows in place; key defaults to item. |
|
|
152
|
+
| `{#for await x of asyncIter by x.id}` … `{:catch e}` / `{/for}` | Async keyed list (`for await…of`); rows append as the iterator yields. |
|
|
153
|
+
| `{#await p}` / `{:then v}` / `{:catch e}` / `{:finally}` / `{/await}` | Promise branches; pending content (before `{:then}`) streams. |
|
|
154
|
+
| `{#await p then v}` / `{/await}` | Blocking: no pending, resolved inline (SSR settles before the first flush). |
|
|
155
|
+
| `{#switch s}` / `{:case v}` / `{:default}` / `{/switch}` | First strict (`===`) match wins. |
|
|
156
|
+
| `{#try}` / `{:catch e}` / `{:finally}` / `{/try}` | Synchronous render error boundary. |
|
|
157
|
+
|
|
158
|
+
**Reusable markup** — `<template>`:
|
|
159
|
+
|
|
160
|
+
| Form | Meaning |
|
|
161
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
162
|
+
| `<template name="row" args={item}>` … `{row(item)}` | Named template (snippet) definition + call. A `<template>` with a control-flow attribute (`if`/`each`/…) is a compile error — use the `{#…}` block. |
|
|
157
163
|
|
|
158
164
|
**Components & slots** — a capitalised tag (`<Card title={x}>`) mounts a child;
|
|
159
165
|
attributes become props, children fill the child's `<slot>` (with fallback when
|
|
160
166
|
empty). `<style>` is scoped to the component and its children. Component files
|
|
161
|
-
end in `.abide`. `then
|
|
162
|
-
component state inside the block.
|
|
167
|
+
end in `.abide`. A block's binding names (`{:then v}`, `{#for x of …}`) lexically
|
|
168
|
+
shadow same-named component state inside the block.
|
|
163
169
|
|
|
164
170
|
## Server surface — abide/server/\* (server-only)
|
|
165
171
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
+
## 0.42.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - reject stray {:…}/{/…} tokens outside a block ([`1b5d54e`](https://github.com/briancray/abide/commit/1b5d54e7798acfa7c9b15ce6b65b0c2b6a8c2c07))
|
|
8
|
+
|
|
9
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - parse {#switch}/{:case}/{:default} blocks to the switch AST ([`448da38`](https://github.com/briancray/abide/commit/448da3840c120ac3b15d239c090f123c25527fec))
|
|
10
|
+
|
|
11
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - parse {#for … of … by …} blocks to the each AST ([`7bb2ac8`](https://github.com/briancray/abide/commit/7bb2ac8f3012e3d40cfcbe8ddf277d6a8732ee93))
|
|
12
|
+
|
|
13
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - LSP semantic-token encoder + extract offsetToPosition ([`7daeb92`](https://github.com/briancray/abide/commit/7daeb926f045058aa452b5f539ddd75b0d8efa67))
|
|
14
|
+
|
|
15
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - semantic-token legend + TS-classification mapping for abide lsp ([`892f97b`](https://github.com/briancray/abide/commit/892f97bbc6dfd8af9301297276995f7060fe3f9a))
|
|
16
|
+
|
|
17
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - type-aware semanticClassifications on the shadow language service ([`9377dd2`](https://github.com/briancray/abide/commit/9377dd24d855cde790d960fceef00255904ee926))
|
|
18
|
+
|
|
19
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - block control-flow grammar formatting + shared helpers ([`b39c0a7`](https://github.com/briancray/abide/commit/b39c0a70696b031b63ef157c67360c2da0fd60b0))
|
|
20
|
+
|
|
21
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - zed onboarding — auto-install extension + enable semantic tokens ([`b47acc7`](https://github.com/briancray/abide/commit/b47acc7cc106956f115c70b110c29ce017a71cd0))
|
|
22
|
+
|
|
23
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - parse {#if}/{:else if}/{:else} blocks to the if AST ([`b662397`](https://github.com/briancray/abide/commit/b66239765bae2ec7a24d70bc7b2edbcbe400614c))
|
|
24
|
+
|
|
25
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - parse {#try}/{:catch}/{:finally} blocks to the try AST ([`b82c3dd`](https://github.com/briancray/abide/commit/b82c3ddaaec3db3941639df952e568f017d6c767))
|
|
26
|
+
|
|
27
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - Breaking: remove <template> control-flow directives in favor of {#…} blocks ([`bf5cf89`](https://github.com/briancray/abide/commit/bf5cf89813a8dcbc967e1303bd2509e18e701761))
|
|
28
|
+
|
|
29
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - color of/by connectors in {#for} block heads ([`c958614`](https://github.com/briancray/abide/commit/c9586147f8196e9e6301b14cdb08986b36da2751))
|
|
30
|
+
|
|
31
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - parse {#await} streaming + blocking blocks to the await AST ([`cfd7ff0`](https://github.com/briancray/abide/commit/cfd7ff0102b2be2ac241a6c0b1965949aa6621bf))
|
|
32
|
+
|
|
33
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - serve textDocument/semanticTokens/full for .abide ([`e152663`](https://github.com/briancray/abide/commit/e152663dae9e60e13d16f0edf4b0a140a99b1bfb))
|
|
34
|
+
|
|
35
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - structural {#…} block framing tokenizer ([`e1e85e8`](https://github.com/briancray/abide/commit/e1e85e847e9442f8c6140df5346b1d37f8cfad54))
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - point block-construct loc at its primary expression (shadow source-map fidelity) ([`0fb0efa`](https://github.com/briancray/abide/commit/0fb0efab43639331c856969fdb6a7b83bd7ab604))
|
|
40
|
+
|
|
41
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - unify block/branch child-scan loops behind a shared helper ([`4683098`](https://github.com/briancray/abide/commit/4683098df96edd0321dc66d200372e0fbedce2e2))
|
|
42
|
+
|
|
43
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - document {#…} control-flow blocks in the AGENTS surface map ([`68fe96f`](https://github.com/briancray/abide/commit/68fe96f3294a228b91eef5cde37c0300673eb88a))
|
|
44
|
+
|
|
45
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - map {#for}/{:then}/{:catch} bindings + by-key to source so hover & semantic tokens land on them ([`e8447e8`](https://github.com/briancray/abide/commit/e8447e8d54934737b80c2f49d4f4d1b134786531))
|
|
46
|
+
|
|
47
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - map <template args> snippet param + await/try {:catch} bindings (+ index/catch/snippet tests) ([`f724d10`](https://github.com/briancray/abide/commit/f724d101f5baed79de2d39a1ab9598eb9d1eb818))
|
|
48
|
+
|
|
49
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - satisfy noUncheckedIndexedAccess in block-token + shadow paths ([`fbb6e44`](https://github.com/briancray/abide/commit/fbb6e4443932e92277fb332b639c746663c7759b))
|
|
50
|
+
|
|
3
51
|
## 0.41.1
|
|
4
52
|
|
|
5
53
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -140,29 +140,28 @@ async function send() {
|
|
|
140
140
|
<button disabled={!text}>send</button>
|
|
141
141
|
</form>
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
{#if latest}
|
|
144
144
|
<p>latest from {latest.from}</p>
|
|
145
|
-
|
|
145
|
+
{:else}
|
|
146
146
|
<p>no messages yet</p>
|
|
147
|
-
|
|
147
|
+
{/if}
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<
|
|
152
|
-
|
|
149
|
+
{#switch view}
|
|
150
|
+
{:case 'mine'}<p>showing your messages</p>
|
|
151
|
+
{:default}<p>showing every message</p>
|
|
152
|
+
{/switch}
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
{#await history}
|
|
155
155
|
<p>loading…</p>
|
|
156
|
-
|
|
156
|
+
{:then data}
|
|
157
157
|
<ul>
|
|
158
|
-
|
|
158
|
+
{#for message, i of data by message.id}
|
|
159
159
|
{i}. {line(message)}
|
|
160
|
-
|
|
160
|
+
{/for}
|
|
161
161
|
</ul>
|
|
162
|
-
|
|
162
|
+
{:catch reason}
|
|
163
163
|
<p>failed: {reason.message}</p>
|
|
164
|
-
|
|
165
|
-
</template>
|
|
164
|
+
{/await}
|
|
166
165
|
|
|
167
166
|
<style>
|
|
168
167
|
form { display: flex; gap: 0.5rem; }
|
package/package.json
CHANGED
package/src/abideLsp.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
2
2
|
import ts from 'typescript'
|
|
3
|
+
import { ABIDE_SEMANTIC_TOKENS_LEGEND } from './lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts'
|
|
3
4
|
import type {
|
|
4
5
|
ShadowLanguageService,
|
|
5
6
|
ShadowQuickInfo,
|
|
6
7
|
} from './lib/ui/compile/createShadowLanguageService.ts'
|
|
7
8
|
import { createShadowLanguageService } from './lib/ui/compile/createShadowLanguageService.ts'
|
|
9
|
+
import { encodeSemanticTokens } from './lib/ui/compile/encodeSemanticTokens.ts'
|
|
8
10
|
import { nearestProjectRoot } from './lib/ui/compile/nearestProjectRoot.ts'
|
|
11
|
+
import { offsetToPosition } from './lib/ui/compile/offsetToPosition.ts'
|
|
12
|
+
import { structuralBlockTokens } from './lib/ui/compile/structuralBlockTokens.ts'
|
|
9
13
|
import type { AbideDiagnostic } from './lib/ui/compile/types/AbideDiagnostic.ts'
|
|
10
14
|
|
|
11
15
|
/*
|
|
@@ -19,6 +23,28 @@ unsaved text as overlays. Each document routes to a shadow service for its
|
|
|
19
23
|
nearest tsconfig, so files in a monorepo opened at its root are checked against
|
|
20
24
|
their own project — matching `abide check` run from that package.
|
|
21
25
|
*/
|
|
26
|
+
/*
|
|
27
|
+
The semantic-tokens `data` array for one component: the structural `{#…}` framing
|
|
28
|
+
merged with the shadow's type-aware expression tokens, encoded to the LSP wire
|
|
29
|
+
format. Never throws — on any internal failure it yields an empty stream so the
|
|
30
|
+
editor falls back to tree-sitter highlighting.
|
|
31
|
+
*/
|
|
32
|
+
export function componentSemanticTokens(
|
|
33
|
+
service: ShadowLanguageService,
|
|
34
|
+
abidePath: string,
|
|
35
|
+
text: string,
|
|
36
|
+
): number[] {
|
|
37
|
+
try {
|
|
38
|
+
const tokens = [
|
|
39
|
+
...structuralBlockTokens(text),
|
|
40
|
+
...service.semanticClassifications(abidePath),
|
|
41
|
+
]
|
|
42
|
+
return encodeSemanticTokens(text, tokens)
|
|
43
|
+
} catch {
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
|
|
23
49
|
const documentText = new Map<string, string>()
|
|
24
50
|
|
|
@@ -64,7 +90,14 @@ export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
|
|
|
64
90
|
jsonrpc: '2.0',
|
|
65
91
|
id: message.id,
|
|
66
92
|
result: {
|
|
67
|
-
capabilities: {
|
|
93
|
+
capabilities: {
|
|
94
|
+
textDocumentSync: 1,
|
|
95
|
+
hoverProvider: true,
|
|
96
|
+
semanticTokensProvider: {
|
|
97
|
+
legend: ABIDE_SEMANTIC_TOKENS_LEGEND,
|
|
98
|
+
full: true,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
68
101
|
serverInfo: { name: 'abide-lsp' },
|
|
69
102
|
},
|
|
70
103
|
})
|
|
@@ -115,6 +148,18 @@ export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
|
|
|
115
148
|
})
|
|
116
149
|
return
|
|
117
150
|
}
|
|
151
|
+
case 'textDocument/semanticTokens/full': {
|
|
152
|
+
const { uri } = message.params.textDocument
|
|
153
|
+
const data = isAbide(uri)
|
|
154
|
+
? componentSemanticTokens(
|
|
155
|
+
serviceFor(fileURLToPath(uri)),
|
|
156
|
+
fileURLToPath(uri),
|
|
157
|
+
documentText.get(fileURLToPath(uri)) ?? '',
|
|
158
|
+
)
|
|
159
|
+
: []
|
|
160
|
+
send({ jsonrpc: '2.0', id: message.id, result: { data } })
|
|
161
|
+
return
|
|
162
|
+
}
|
|
118
163
|
case 'textDocument/didClose': {
|
|
119
164
|
const { uri } = message.params.textDocument
|
|
120
165
|
if (isAbide(uri)) {
|
|
@@ -194,13 +239,6 @@ function toLspHover(text: string, info: ShadowQuickInfo): object {
|
|
|
194
239
|
}
|
|
195
240
|
}
|
|
196
241
|
|
|
197
|
-
/* An absolute offset → LSP `{ line, character }` (0-based, UTF-16 code units). */
|
|
198
|
-
function offsetToPosition(text: string, offset: number): { line: number; character: number } {
|
|
199
|
-
const before = text.slice(0, offset)
|
|
200
|
-
const line = before.split('\n').length - 1
|
|
201
|
-
return { line, character: offset - (before.lastIndexOf('\n') + 1) }
|
|
202
|
-
}
|
|
203
|
-
|
|
204
242
|
/* An LSP `{ line, character }` (0-based) → absolute offset in `text`. */
|
|
205
243
|
function positionToOffset(text: string, position: { line: number; character: number }): number {
|
|
206
244
|
const lineStart = text
|
|
@@ -4,6 +4,8 @@ import type { BunPlugin } from 'bun'
|
|
|
4
4
|
import { Glob } from 'bun'
|
|
5
5
|
import { abideImportName } from './lib/shared/abideImportName.ts'
|
|
6
6
|
import { abideLog } from './lib/shared/abideLog.ts'
|
|
7
|
+
import { escapeRegex } from './lib/shared/escapeRegex.ts'
|
|
8
|
+
import { fileName } from './lib/shared/fileName.ts'
|
|
7
9
|
import { fileStem } from './lib/shared/fileStem.ts'
|
|
8
10
|
import { jsonSchemaForPromptArguments } from './lib/shared/jsonSchemaForPromptArguments.ts'
|
|
9
11
|
import { manifestModule } from './lib/shared/manifestModule.ts'
|
|
@@ -65,10 +67,6 @@ function resolveExtensionUncached(path: string): string {
|
|
|
65
67
|
|
|
66
68
|
const NS = 'abide-virtual'
|
|
67
69
|
|
|
68
|
-
function escapeRegex(value: string): string {
|
|
69
|
-
return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
|
|
70
|
-
}
|
|
71
|
-
|
|
72
70
|
/* Memoises a zero-arg async producer so repeat calls reuse the first in-flight promise. */
|
|
73
71
|
function once<T>(produce: () => Promise<T>): () => Promise<T> {
|
|
74
72
|
let promise: Promise<T> | undefined
|
|
@@ -791,7 +789,7 @@ async function scanPages(pagesDir: string): Promise<PagesScan> {
|
|
|
791
789
|
return { pageFiles: [], layoutFiles: [] }
|
|
792
790
|
}
|
|
793
791
|
const allFiles = await Array.fromAsync(new Glob('**/*.abide').scan({ cwd: pagesDir }))
|
|
794
|
-
const leafIs = (name: string) => (file: string) => (file
|
|
792
|
+
const leafIs = (name: string) => (file: string) => fileName(file) === name
|
|
795
793
|
return {
|
|
796
794
|
pageFiles: allFiles.filter(leafIs('page.abide')),
|
|
797
795
|
layoutFiles: allFiles.filter(leafIs('layout.abide')),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { relative } from 'node:path'
|
|
2
|
+
import { fileName } from '../../shared/fileName.ts'
|
|
2
3
|
import { analyzeComponent } from '../../ui/compile/analyzeComponent.ts'
|
|
3
4
|
import { compileComponent } from '../../ui/compile/compileComponent.ts'
|
|
4
5
|
import { nearestProjectRoot } from '../../ui/compile/nearestProjectRoot.ts'
|
|
@@ -19,7 +20,7 @@ const GENERATED = /(^|\/)\.abide\//
|
|
|
19
20
|
// page.abide / layout.abide are router-mounted, not `mountChild`-tracked, so they
|
|
20
21
|
// can't hot-swap — they fold into `structure` (a reload) instead.
|
|
21
22
|
function isPageOrLayout(moduleId: string): boolean {
|
|
22
|
-
const file = moduleId
|
|
23
|
+
const file = fileName(moduleId)
|
|
23
24
|
return file === 'page.abide' || file === 'layout.abide'
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Escapes the regex metacharacters in `value` so it can be embedded literally
|
|
3
|
+
inside a `new RegExp(...)` pattern. Shared by the $rpc/$sockets import stripper,
|
|
4
|
+
the resolver plugin's virtual-namespace matcher, and the SSR snippet-call
|
|
5
|
+
rewriter so the same escaping is applied one way everywhere.
|
|
6
|
+
*/
|
|
7
|
+
export function escapeRegex(value: string): string {
|
|
8
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The bare leaf filename of a path — directory stripped, extension kept —
|
|
3
|
+
e.g. `users/list.ts` → `list.ts`. The extension-stripping `fileStem` builds on
|
|
4
|
+
this; call sites that match a full filename (`layout.abide`, `page.abide`) use
|
|
5
|
+
it directly so the leaf-grab is written one way.
|
|
6
|
+
*/
|
|
7
|
+
export function fileName(path: string): string {
|
|
8
|
+
return path.split('/').pop() ?? ''
|
|
9
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { fileName } from './fileName.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
The bare filename of a path, with directory and trailing extension stripped —
|
|
3
5
|
e.g. `users/list.ts` → `list`, `/_virtual/mcp-resources.ts` → `mcp-resources`.
|
|
@@ -5,5 +7,5 @@ Used to derive a virtual-module name from its path and to check an $rpc /
|
|
|
5
7
|
$sockets module's single export name against its file stem.
|
|
6
8
|
*/
|
|
7
9
|
export function fileStem(path: string): string {
|
|
8
|
-
return (path
|
|
10
|
+
return fileName(path).replace(/\.[^.]+$/, '')
|
|
9
11
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { fileName } from './fileName.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
Derives the CLI program/binary name from a package.json `name` field.
|
|
3
5
|
Scoped names (`@scope/tool`) keep only the final segment so the value is
|
|
@@ -10,5 +12,5 @@ export function programNameForPackage(name: string | undefined): string {
|
|
|
10
12
|
if (name === undefined || name === '') {
|
|
11
13
|
return 'app'
|
|
12
14
|
}
|
|
13
|
-
return name
|
|
15
|
+
return fileName(name) || 'app'
|
|
14
16
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeRegex } from './escapeRegex.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
Strips the user's `import { … } from '<moduleName>'` declaration from a
|
|
3
5
|
module source. Used by the $rpc / $sockets rewriters to remove the
|
|
@@ -18,7 +20,7 @@ includes newlines, so multi-line braced imports like
|
|
|
18
20
|
still match — the body just can't contain another `}` to bound it.
|
|
19
21
|
*/
|
|
20
22
|
export function stripImport(source: string, moduleName: string): string {
|
|
21
|
-
const escaped = moduleName
|
|
23
|
+
const escaped = escapeRegex(moduleName)
|
|
22
24
|
const pattern = new RegExp(
|
|
23
25
|
`^\\s*import\\s*\\{[^}]*\\}\\s*from\\s*['"]${escaped}['"]\\s*;?\\s*$`,
|
|
24
26
|
'gm',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The LSP semantic-tokens legend abide lsp advertises, and the decoder from
|
|
3
|
+
TypeScript's encoded classifications to legend names. TypeScript encodes a
|
|
4
|
+
classification as `((tokenType + 1) << 8) + modifierBitset`; its TokenType and
|
|
5
|
+
TokenModifier enums fix the orders below. `keyword`/`operator` carry the `{#…}`
|
|
6
|
+
block framing the structural tokenizer emits.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* TS TokenType order (class=0 … member=11) → LSP token-type name. */
|
|
10
|
+
const TS_TYPE_TO_LSP = [
|
|
11
|
+
'class',
|
|
12
|
+
'enum',
|
|
13
|
+
'interface',
|
|
14
|
+
'namespace',
|
|
15
|
+
'typeParameter',
|
|
16
|
+
'type',
|
|
17
|
+
'parameter',
|
|
18
|
+
'variable',
|
|
19
|
+
'enumMember',
|
|
20
|
+
'property',
|
|
21
|
+
'function',
|
|
22
|
+
'method',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
/* TS TokenModifier order (declaration=bit 0 … local=bit 5). */
|
|
26
|
+
const TS_MODIFIERS = ['declaration', 'static', 'async', 'readonly', 'defaultLibrary', 'local']
|
|
27
|
+
|
|
28
|
+
export const ABIDE_SEMANTIC_TOKENS_LEGEND = {
|
|
29
|
+
tokenTypes: [...TS_TYPE_TO_LSP, 'keyword', 'operator'],
|
|
30
|
+
tokenModifiers: TS_MODIFIERS,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Decodes one TypeScript encoded classification into legend names. */
|
|
34
|
+
export function mapTsClassification(
|
|
35
|
+
classification: number,
|
|
36
|
+
): { type: string; modifiers: string[] } | undefined {
|
|
37
|
+
const tokenType = (classification >> 8) - 1
|
|
38
|
+
const type = TS_TYPE_TO_LSP[tokenType]
|
|
39
|
+
if (type === undefined) {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
const modifierSet = classification & 255
|
|
43
|
+
const modifiers = TS_MODIFIERS.filter((_, bit) => (modifierSet & (1 << bit)) !== 0)
|
|
44
|
+
return { type, modifiers }
|
|
45
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { relative } from 'node:path'
|
|
2
2
|
import type { BunPlugin } from 'bun'
|
|
3
|
+
import { fileName } from '../../shared/fileName.ts'
|
|
3
4
|
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
4
5
|
import { AbideCompileError } from './AbideCompileError.ts'
|
|
5
6
|
import { analyzeComponent } from './analyzeComponent.ts'
|
|
@@ -42,7 +43,7 @@ export const abideUiPlugin: BunPlugin = {
|
|
|
42
43
|
build.onLoad({ filter: /\.abide$/ }, async (args) => {
|
|
43
44
|
const source = await Bun.file(args.path).text()
|
|
44
45
|
const moduleId = relative(nearestProjectRoot(args.path, process.cwd()), args.path)
|
|
45
|
-
const isLayout = (args.path
|
|
46
|
+
const isLayout = fileName(args.path) === 'layout.abide'
|
|
46
47
|
/* Bun frames a plugin throw at `<file>:0` regardless of the real spot, so
|
|
47
48
|
carry the component path + resolved line:col in the message — otherwise a
|
|
48
49
|
control-flow / compile error reads as `:0` and (in deep imports) can look
|
|
@@ -91,6 +91,10 @@ export function compileShadow(source: string): CompiledShadow {
|
|
|
91
91
|
pre-assembled scope line carrying its own embedded segments. */
|
|
92
92
|
type Builder = {
|
|
93
93
|
raw: (text: string) => void
|
|
94
|
+
/* Appends `text` verbatim and maps it back to source, WITHOUT the `expr`
|
|
95
|
+
parens — for binding names and other spans that must stay bare TS syntax
|
|
96
|
+
(`for (const NAME of …)`). `text` must equal the source at `sourceLoc`. */
|
|
97
|
+
mapped: (text: string, sourceLoc: number | undefined) => void
|
|
94
98
|
expr: (code: string, sourceLoc: number | undefined) => void
|
|
95
99
|
stmt: (code: string, sourceLoc: number | undefined) => void
|
|
96
100
|
flush: (line: ScopeLine) => void
|
|
@@ -115,6 +119,16 @@ function createBuilder(): Builder {
|
|
|
115
119
|
raw(text) {
|
|
116
120
|
code += text
|
|
117
121
|
},
|
|
122
|
+
mapped(text, sourceLoc) {
|
|
123
|
+
if (sourceLoc !== undefined) {
|
|
124
|
+
mappings.push({
|
|
125
|
+
shadowStart: code.length,
|
|
126
|
+
sourceStart: sourceLoc,
|
|
127
|
+
length: text.length,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
code += text
|
|
131
|
+
},
|
|
118
132
|
expr(exprCode, sourceLoc) {
|
|
119
133
|
code += '('
|
|
120
134
|
if (sourceLoc !== undefined) {
|
|
@@ -408,19 +422,24 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
408
422
|
}
|
|
409
423
|
case 'each':
|
|
410
424
|
/* `for await` over an async each's AsyncIterable, plain `for…of` otherwise —
|
|
411
|
-
so the item binds to the element type under either iteration protocol.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
)
|
|
425
|
+
so the item binds to the element type under either iteration protocol. The
|
|
426
|
+
binding name is `mapped` (not `raw`) so hover/highlighting land on it. */
|
|
427
|
+
builder.raw(node.async ? 'for await (const ' : 'for (const ')
|
|
428
|
+
builder.mapped(node.as, node.asLoc)
|
|
429
|
+
builder.raw(' of ')
|
|
415
430
|
builder.expr(node.items, node.loc)
|
|
416
431
|
builder.raw(') {\n')
|
|
417
432
|
if (node.key !== undefined) {
|
|
418
|
-
builder.raw(
|
|
433
|
+
builder.raw('void ')
|
|
434
|
+
builder.expr(node.key, node.keyLoc)
|
|
435
|
+
builder.raw(';\n')
|
|
419
436
|
}
|
|
420
437
|
/* `index="i"` binds the row's position — always a number (the row ordinal,
|
|
421
438
|
or an async stream's arrival count). Declare it so body references check. */
|
|
422
439
|
if (node.index !== undefined) {
|
|
423
|
-
builder.raw(
|
|
440
|
+
builder.raw('const ')
|
|
441
|
+
builder.mapped(node.index, node.indexLoc)
|
|
442
|
+
builder.raw(': number = 0;\n')
|
|
424
443
|
}
|
|
425
444
|
emitNodes(node.children, builder)
|
|
426
445
|
builder.raw('}\n')
|
|
@@ -439,7 +458,9 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
439
458
|
const pending = node.children.filter((child) => child.kind !== 'branch')
|
|
440
459
|
const branches = node.children.filter((child) => child.kind === 'branch')
|
|
441
460
|
if (node.blocking && node.as !== undefined) {
|
|
442
|
-
builder.raw(
|
|
461
|
+
builder.raw('{\nconst ')
|
|
462
|
+
builder.mapped(node.as, node.asLoc)
|
|
463
|
+
builder.raw(` = ${resolved};\n`)
|
|
443
464
|
emitNodes(pending, builder)
|
|
444
465
|
builder.raw('}\n')
|
|
445
466
|
} else {
|
|
@@ -451,9 +472,13 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
451
472
|
}
|
|
452
473
|
builder.raw('{\n')
|
|
453
474
|
if (branch.branch === 'then' && branch.as !== undefined) {
|
|
454
|
-
builder.raw(
|
|
475
|
+
builder.raw('const ')
|
|
476
|
+
builder.mapped(branch.as, branch.asLoc)
|
|
477
|
+
builder.raw(` = ${resolved};\n`)
|
|
455
478
|
} else if (branch.branch === 'catch' && branch.as !== undefined) {
|
|
456
|
-
builder.raw(
|
|
479
|
+
builder.raw('const ')
|
|
480
|
+
builder.mapped(branch.as, branch.asLoc)
|
|
481
|
+
builder.raw(' = undefined as any;\n')
|
|
457
482
|
}
|
|
458
483
|
emitNodes(branch.children, builder)
|
|
459
484
|
builder.raw('}\n')
|
|
@@ -508,7 +533,14 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
508
533
|
builder.raw('}\n')
|
|
509
534
|
return
|
|
510
535
|
case 'snippet':
|
|
511
|
-
|
|
536
|
+
/* `args={…}` is the parameter list; `mapped` (not `raw`) so hover/highlighting
|
|
537
|
+
land on the binding. `loc` is the `args` expression's offset (see
|
|
538
|
+
toSnippetOrTemplate). The name is a static attribute with no tracked offset. */
|
|
539
|
+
builder.raw(`const ${node.name} = (`)
|
|
540
|
+
if (node.params !== undefined) {
|
|
541
|
+
builder.mapped(node.params, node.loc)
|
|
542
|
+
}
|
|
543
|
+
builder.raw(') => {\n')
|
|
512
544
|
emitNodes(node.children, builder)
|
|
513
545
|
builder.raw('};\n')
|
|
514
546
|
return
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import ts from 'typescript'
|
|
3
3
|
import { messageFromError } from '../../shared/messageFromError.ts'
|
|
4
|
+
import { mapTsClassification } from './ABIDE_SEMANTIC_TOKENS_LEGEND.ts'
|
|
4
5
|
import { assetModulesFile } from './assetModulesFile.ts'
|
|
5
6
|
import { compileShadow } from './compileShadow.ts'
|
|
6
7
|
import { loadShadowTsConfig } from './loadShadowTsConfig.ts'
|
|
@@ -10,6 +11,7 @@ import { shadowNaming } from './shadowNaming.ts'
|
|
|
10
11
|
import { sourceToShadowOffset } from './sourceToShadowOffset.ts'
|
|
11
12
|
import type { AbideDiagnostic } from './types/AbideDiagnostic.ts'
|
|
12
13
|
import type { CompiledShadow } from './types/CompiledShadow.ts'
|
|
14
|
+
import type { SemanticToken } from './types/SemanticToken.ts'
|
|
13
15
|
|
|
14
16
|
const { suffixed, isShadow, sourceOf } = shadowNaming
|
|
15
17
|
|
|
@@ -27,6 +29,8 @@ export type ShadowLanguageService = {
|
|
|
27
29
|
/* Hover info at a source offset, or undefined if the offset isn't a checked
|
|
28
30
|
expression (markup, whitespace) or TypeScript has nothing to report. */
|
|
29
31
|
quickInfo: (abidePath: string, sourceOffset: number) => ShadowQuickInfo | undefined
|
|
32
|
+
/* Type-aware semantic tokens for every checked expression, in source coords. */
|
|
33
|
+
semanticClassifications: (abidePath: string) => SemanticToken[]
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/*
|
|
@@ -194,5 +198,49 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
|
|
|
194
198
|
length: span?.length ?? 1,
|
|
195
199
|
}
|
|
196
200
|
},
|
|
201
|
+
semanticClassifications(abidePath) {
|
|
202
|
+
const fileName = suffixed(abidePath)
|
|
203
|
+
/* Compile first so the mappings cache is current. */
|
|
204
|
+
shadowText(abidePath)
|
|
205
|
+
const shadow = shadows.get(abidePath)
|
|
206
|
+
if (shadow === undefined) {
|
|
207
|
+
return []
|
|
208
|
+
}
|
|
209
|
+
const { spans } = service.getEncodedSemanticClassifications(
|
|
210
|
+
fileName,
|
|
211
|
+
{ start: 0, length: shadow.code.length },
|
|
212
|
+
ts.SemanticClassificationFormat.TwentyTwenty,
|
|
213
|
+
)
|
|
214
|
+
/* `spans` is flat triples [start, length, classification, …] in shadow
|
|
215
|
+
coords; keep only those overlapping a mapped expression segment. */
|
|
216
|
+
const tokens: SemanticToken[] = []
|
|
217
|
+
for (let index = 0; index + 2 < spans.length; index += 3) {
|
|
218
|
+
const spanStart = spans[index]
|
|
219
|
+
const spanLength = spans[index + 1]
|
|
220
|
+
const classification = spans[index + 2]
|
|
221
|
+
if (
|
|
222
|
+
spanStart === undefined ||
|
|
223
|
+
spanLength === undefined ||
|
|
224
|
+
classification === undefined
|
|
225
|
+
) {
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
const mapped = mapTsClassification(classification)
|
|
229
|
+
if (mapped === undefined) {
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
const located = remapShadowDiagnostic(shadow.mappings, spanStart, spanLength)
|
|
233
|
+
if (located === undefined) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
tokens.push({
|
|
237
|
+
start: located.start,
|
|
238
|
+
length: located.length,
|
|
239
|
+
type: mapped.type,
|
|
240
|
+
modifiers: mapped.modifiers,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
return tokens
|
|
244
|
+
},
|
|
197
245
|
}
|
|
198
246
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ABIDE_SEMANTIC_TOKENS_LEGEND } from './ABIDE_SEMANTIC_TOKENS_LEGEND.ts'
|
|
2
|
+
import { offsetToPosition } from './offsetToPosition.ts'
|
|
3
|
+
import type { SemanticToken } from './types/SemanticToken.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Encodes source-coordinate tokens into the LSP semantic-tokens `data` array: five
|
|
7
|
+
integers per token (deltaLine, deltaStartChar, length, tokenTypeIndex,
|
|
8
|
+
modifierBitset), each position relative to the previous token. Tokens are sorted
|
|
9
|
+
by start; tokens with an unknown legend type, and any token overlapping the one
|
|
10
|
+
before it, are dropped — the protocol requires a strictly non-overlapping,
|
|
11
|
+
positionally-ordered stream.
|
|
12
|
+
*/
|
|
13
|
+
export function encodeSemanticTokens(text: string, tokens: SemanticToken[]): number[] {
|
|
14
|
+
const sorted = [...tokens].sort((a, b) => a.start - b.start || a.length - b.length)
|
|
15
|
+
const data: number[] = []
|
|
16
|
+
let previousLine = 0
|
|
17
|
+
let previousCharacter = 0
|
|
18
|
+
let previousEnd = -1
|
|
19
|
+
for (const token of sorted) {
|
|
20
|
+
const typeIndex = ABIDE_SEMANTIC_TOKENS_LEGEND.tokenTypes.indexOf(token.type)
|
|
21
|
+
if (typeIndex < 0 || token.start < previousEnd) {
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
const modifierBitset = token.modifiers.reduce((bits, name) => {
|
|
25
|
+
const bit = ABIDE_SEMANTIC_TOKENS_LEGEND.tokenModifiers.indexOf(name)
|
|
26
|
+
return bit < 0 ? bits : bits | (1 << bit)
|
|
27
|
+
}, 0)
|
|
28
|
+
const { line, character } = offsetToPosition(text, token.start)
|
|
29
|
+
const deltaLine = line - previousLine
|
|
30
|
+
const deltaCharacter = deltaLine === 0 ? character - previousCharacter : character
|
|
31
|
+
data.push(deltaLine, deltaCharacter, token.length, typeIndex, modifierBitset)
|
|
32
|
+
previousLine = line
|
|
33
|
+
previousCharacter = character
|
|
34
|
+
previousEnd = token.start + token.length
|
|
35
|
+
}
|
|
36
|
+
return data
|
|
37
|
+
}
|