@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.
Files changed (32) hide show
  1. package/AGENTS.md +19 -13
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +13 -14
  4. package/package.json +1 -1
  5. package/src/abideLsp.ts +46 -8
  6. package/src/abideResolverPlugin.ts +3 -5
  7. package/src/lib/server/runtime/devClientFingerprint.ts +2 -1
  8. package/src/lib/shared/escapeRegex.ts +9 -0
  9. package/src/lib/shared/fileName.ts +9 -0
  10. package/src/lib/shared/fileStem.ts +3 -1
  11. package/src/lib/shared/programNameForPackage.ts +3 -1
  12. package/src/lib/shared/stripImport.ts +3 -1
  13. package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +45 -0
  14. package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
  15. package/src/lib/ui/compile/compileShadow.ts +42 -10
  16. package/src/lib/ui/compile/createShadowLanguageService.ts +48 -0
  17. package/src/lib/ui/compile/encodeSemanticTokens.ts +37 -0
  18. package/src/lib/ui/compile/generateBuild.ts +2 -2
  19. package/src/lib/ui/compile/generateSSR.ts +6 -6
  20. package/src/lib/ui/compile/makeVarNamer.ts +10 -0
  21. package/src/lib/ui/compile/offsetToPosition.ts +9 -0
  22. package/src/lib/ui/compile/parseTemplate.ts +524 -104
  23. package/src/lib/ui/compile/structuralBlockTokens.ts +101 -0
  24. package/src/lib/ui/compile/types/SemanticToken.ts +11 -0
  25. package/src/lib/ui/compile/types/TemplateNode.ts +9 -0
  26. package/src/lib/ui/dom/appendText.ts +1 -5
  27. package/src/lib/ui/dom/applyResolved.ts +1 -5
  28. package/src/lib/ui/dom/isComment.ts +6 -0
  29. package/src/lib/ui/runtime/createDoc.ts +3 -3
  30. package/src/lib/ui/runtime/pathSegments.ts +10 -0
  31. package/template/.zed/settings.json +4 -0
  32. package/template/src/ui/pages/page.abide +4 -4
@@ -0,0 +1,101 @@
1
+ import type { SemanticToken } from './types/SemanticToken.ts'
2
+
3
+ /*
4
+ Lexical highlighting for `{#…}`/`{:…}`/`{/…}` control-flow framing — the part the
5
+ HTML grammar sees only as text and the shadow program lowers away. A pure scan of
6
+ raw source (independent of a successful parse, so it survives mid-edit), it emits
7
+ an `operator` token for the `{`+sigil opener and a `keyword` token for the block
8
+ word, plus the `of`/`by` connectors inside a `{#for …}` head. Expression interiors
9
+ (and the `{@const}`/`{@html}` tags and bare `{expr}` interpolations) are NOT touched
10
+ here — those are the shadow's job. A keyword allowlist after the sigil prevents
11
+ matching arbitrary `{:foo}` text. Longest phrases first so `for await` beats `for`
12
+ and `else if` beats `else`.
13
+ */
14
+ const BLOCK_KEYWORDS = [
15
+ 'for await',
16
+ 'else if',
17
+ 'if',
18
+ 'for',
19
+ 'await',
20
+ 'switch',
21
+ 'case',
22
+ 'default',
23
+ 'try',
24
+ 'catch',
25
+ 'finally',
26
+ 'then',
27
+ 'else',
28
+ ]
29
+
30
+ const BLOCK_HEAD = new RegExp(`\\{([#:/])\\s*(${BLOCK_KEYWORDS.join('|')})\\b`, 'g')
31
+
32
+ export function structuralBlockTokens(source: string): SemanticToken[] {
33
+ const tokens: SemanticToken[] = []
34
+ for (const match of source.matchAll(BLOCK_HEAD)) {
35
+ const braceStart = match.index
36
+ const sigil = match[1]
37
+ const keyword = match[2]
38
+ if (keyword === undefined) {
39
+ continue
40
+ }
41
+ const keywordStart = braceStart + match[0].length - keyword.length
42
+ tokens.push({ start: braceStart, length: 2, type: 'operator', modifiers: [] })
43
+ tokens.push({ start: keywordStart, length: keyword.length, type: 'keyword', modifiers: [] })
44
+ /* A `{#for …}` head carries abide-only connectors `of`/`by` that the shadow
45
+ lowers away (`of`) or never emits (`by`), so color them here. */
46
+ if (sigil === '#' && (keyword === 'for' || keyword === 'for await')) {
47
+ tokens.push(...forHeadConnectors(source, keywordStart + keyword.length))
48
+ }
49
+ }
50
+ return tokens
51
+ }
52
+
53
+ /*
54
+ Scans a `{#for …}` head from `from` to its closing `}`, emitting `keyword` tokens
55
+ for the `of`/`by` connectors at brace depth 0 — skipping any `of`/`by` nested in a
56
+ destructure, call, or string so an identifier or object key is never miscolored.
57
+ */
58
+ function forHeadConnectors(source: string, from: number): SemanticToken[] {
59
+ const tokens: SemanticToken[] = []
60
+ let depth = 0
61
+ let cursor = from
62
+ while (cursor < source.length) {
63
+ const char = source.charAt(cursor)
64
+ if (char === '"' || char === "'" || char === '`') {
65
+ cursor += 1
66
+ while (cursor < source.length && source.charAt(cursor) !== char) {
67
+ if (source.charAt(cursor) === '\\') {
68
+ cursor += 1
69
+ }
70
+ cursor += 1
71
+ }
72
+ } else if (char === '{' || char === '(' || char === '[') {
73
+ depth += 1
74
+ } else if (char === ')' || char === ']') {
75
+ depth -= 1
76
+ } else if (char === '}') {
77
+ if (depth === 0) {
78
+ break
79
+ }
80
+ depth -= 1
81
+ } else if (depth === 0 && (char === 'o' || char === 'b')) {
82
+ const word = source.startsWith('of', cursor)
83
+ ? 'of'
84
+ : source.startsWith('by', cursor)
85
+ ? 'by'
86
+ : undefined
87
+ const isWordBoundary = (offset: number): boolean => /\s/.test(source.charAt(offset))
88
+ if (
89
+ word !== undefined &&
90
+ isWordBoundary(cursor - 1) &&
91
+ isWordBoundary(cursor + word.length)
92
+ ) {
93
+ tokens.push({ start: cursor, length: word.length, type: 'keyword', modifiers: [] })
94
+ cursor += word.length
95
+ continue
96
+ }
97
+ }
98
+ cursor += 1
99
+ }
100
+ return tokens
101
+ }
@@ -0,0 +1,11 @@
1
+ /*
2
+ A semantic-highlighting token in original `.abide` source coordinates. `type` and
3
+ `modifiers` are legend names (see ABIDE_SEMANTIC_TOKENS_LEGEND), resolved to wire
4
+ indices only at encode time.
5
+ */
6
+ export type SemanticToken = {
7
+ start: number
8
+ length: number
9
+ type: string
10
+ modifiers: string[]
11
+ }
@@ -40,6 +40,11 @@ export type TemplateNode =
40
40
  async: boolean
41
41
  children: TemplateNode[]
42
42
  loc?: number
43
+ /* Source offsets of the binding name, `by` key, and index — so the shadow
44
+ maps hover/highlighting onto them (absent for synthesised/missing parts). */
45
+ asLoc?: number
46
+ keyLoc?: number
47
+ indexLoc?: number
43
48
  }
44
49
  | { kind: 'if'; condition: string; children: TemplateNode[]; loc?: number }
45
50
  | {
@@ -52,6 +57,8 @@ export type TemplateNode =
52
57
  as: string | undefined
53
58
  children: TemplateNode[]
54
59
  loc?: number
60
+ /* Source offset of an inline blocking `then` binding (`{#await p then v}`). */
61
+ asLoc?: number
55
62
  }
56
63
  | { kind: 'try'; children: TemplateNode[] }
57
64
  | {
@@ -59,6 +66,8 @@ export type TemplateNode =
59
66
  branch: 'then' | 'catch' | 'finally'
60
67
  as: string | undefined
61
68
  children: TemplateNode[]
69
+ /* Source offset of the `then`/`catch` binding, so the shadow maps it. */
70
+ asLoc?: number
62
71
  }
63
72
  | {
64
73
  kind: 'component'
@@ -4,6 +4,7 @@ import { effect } from '../effect.ts'
4
4
  import { claimChild } from '../runtime/claimChild.ts'
5
5
  import { RENDER } from '../runtime/RENDER.ts'
6
6
  import { appendSnippet } from './appendSnippet.ts'
7
+ import { isComment } from './isComment.ts'
7
8
  import { parseRawNodes } from './parseRawNodes.ts'
8
9
 
9
10
  const CLOSE = '/abide:html'
@@ -106,8 +107,3 @@ function appendRawHtml(parent: Node, read: () => unknown): void {
106
107
  set(markup())
107
108
  })
108
109
  }
109
-
110
- /* A comment node carrying exactly `data`. */
111
- function isComment(node: Node, data: string): boolean {
112
- return (node as { data?: string }).data === data && node.childNodes.length === 0
113
- }
@@ -1,4 +1,5 @@
1
1
  import { seedResolved } from '../seedResolved.ts'
2
+ import { isComment } from './isComment.ts'
2
3
 
3
4
  /*
4
5
  Bundle-side consumer of an SSR stream chunk, the counterpart of the doc stream's inline
@@ -71,11 +72,6 @@ export function applyResolved(root: Element, frame: string): void {
71
72
  }
72
73
  }
73
74
 
74
- /* A comment node carrying exactly `data`. */
75
- function isComment(node: Node, data: string): boolean {
76
- return (node as { data?: string }).data === data && node.childNodes.length === 0
77
- }
78
-
79
75
  /* Depth-first search for the parent + open-marker comment of a boundary. */
80
76
  function findBoundary(node: Node, open: string): { parent: Node; start: Node } | undefined {
81
77
  for (const child of [...node.childNodes]) {
@@ -0,0 +1,6 @@
1
+ /* A comment node carrying exactly `data`. Empty-child check distinguishes the
2
+ `<!--data-->` marker from an element that happens to expose a `data` property.
3
+ Shared by the marker-range scanners in appendText/applyResolved. */
4
+ export function isComment(node: Node, data: string): boolean {
5
+ return (node as { data?: string }).data === data && node.childNodes.length === 0
6
+ }
@@ -3,13 +3,13 @@ import { batch } from './batch.ts'
3
3
  import { createComputedNode } from './createComputedNode.ts'
4
4
  import { createSignalNode } from './createSignalNode.ts'
5
5
  import { PATCH_BUS } from './PATCH_BUS.ts'
6
+ import { pathSegments } from './pathSegments.ts'
6
7
  import { readNode } from './readNode.ts'
7
8
  import { trigger } from './trigger.ts'
8
9
  import type { Cell } from './types/Cell.ts'
9
10
  import type { Doc } from './types/Doc.ts'
10
11
  import type { Patch } from './types/Patch.ts'
11
12
  import type { ReactiveNode } from './types/ReactiveNode.ts'
12
- import { unescapeKey } from './unescapeKey.ts'
13
13
  import { walkPath } from './walkPath.ts'
14
14
  import { writeNode } from './writeNode.ts'
15
15
 
@@ -132,7 +132,7 @@ export function createDoc(initial: unknown): Doc {
132
132
  function apply(patch: Patch): void {
133
133
  /* Segments index the tree, so they carry the REAL keys (unescaped); the path
134
134
  strings (parentPath, node-map keys) stay escaped, re-walked through walkPath. */
135
- const segments = patch.path === '' ? [] : patch.path.split('/').map(unescapeKey)
135
+ const segments = patch.path === '' ? [] : pathSegments(patch.path)
136
136
  /* Capture the pre-image only when a consumer is listening (the inverse's only
137
137
  cost): a replace/remove inverts to the value it overwrote, an add to a
138
138
  remove (computed post-apply, below, to resolve an array append's index). */
@@ -220,7 +220,7 @@ export function createDoc(initial: unknown): Doc {
220
220
  */
221
221
  function cell<T>(path: string): Cell<T> {
222
222
  const node = nodeFor(path)
223
- const segments = path.split('/').map(unescapeKey)
223
+ const segments = pathSegments(path)
224
224
  const leafKey = segments[segments.length - 1] as string
225
225
  /* Auto-vivify missing ancestor objects so binding a nested path on a doc
226
226
  booted shallow (e.g. `state({})`) doesn't crash, and a later `set` writes
@@ -0,0 +1,10 @@
1
+ import { unescapeKey } from './unescapeKey.ts'
2
+
3
+ /*
4
+ Splits an escaped JSON-Pointer-style doc path into its REAL key segments
5
+ (each segment unescaped). The path strings (parentPath, node-map keys) stay
6
+ escaped and re-walked through `walkPath`; segments index the live tree.
7
+ */
8
+ export function pathSegments(path: string): string[] {
9
+ return path.split('/').map(unescapeKey)
10
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "auto_install_extensions": { "abide": true },
3
+ "semantic_tokens": "combined"
4
+ }
@@ -3,17 +3,17 @@
3
3
  Root page — served at GET /. Every folder under src/ui/pages/ that contains a
4
4
  page.abide mounts at that folder's URL; the root layout.abide wraps it.
5
5
 
6
- The blocking await-block below (the `then` attribute sits ON the <template await>)
6
+ The blocking await-block below (the `then` clause sits in the `{#await}` head)
7
7
  resolves on the server during SSR and renders inline — no pending placeholder. The
8
8
  decoded body is captured into the per-request cache, serialized into the HTML, and
9
- replayed on the client during hydration with no second fetch. A `then` *child*
9
+ replayed on the client during hydration with no second fetch. A `{:then}` *branch*
10
10
  instead would stream the resolution in out of order.
11
11
  */
12
12
  import { cache } from '@abide/abide/shared/cache'
13
13
  import { getHello } from '$server/rpc/getHello.ts'
14
14
  </script>
15
15
 
16
- <template await={cache(getHello)()} then="hello">
16
+ {#await cache(getHello)() then hello}
17
17
  <h1>{hello.message}</h1>
18
- </template>
18
+ {/await}
19
19
  <p>Edit <code>src/ui/pages/page.abide</code> and the page hot-reloads.</p>