@miliastry/quasar 1.0.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 (172) hide show
  1. package/LICENSE +119 -0
  2. package/README.md +45 -0
  3. package/dist/Visuals/lyne.css +1027 -0
  4. package/dist/Visuals/osu.css +257 -0
  5. package/dist/index.d.mts +4563 -0
  6. package/dist/index.d.ts +4563 -0
  7. package/dist/index.js +11291 -0
  8. package/dist/index.mjs +11197 -0
  9. package/package.json +51 -0
  10. package/src/Analysis/Contracts/AnalysisReport.ts +26 -0
  11. package/src/Analysis/Contracts/Contribution.ts +76 -0
  12. package/src/Analysis/Contracts/Pass.ts +76 -0
  13. package/src/Analysis/Contracts/PipelineContext.ts +48 -0
  14. package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +292 -0
  15. package/src/Analysis/Passes/Analysis/MergeableColorAnalyzer.ts +112 -0
  16. package/src/Analysis/Passes/Analysis/RainbowAnalyzer.ts +233 -0
  17. package/src/Analysis/Passes/Analysis/WaveAnalyzer.ts +211 -0
  18. package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +135 -0
  19. package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +84 -0
  20. package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +99 -0
  21. package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +119 -0
  22. package/src/Analysis/Passes/Decision/DefaultDecision.ts +139 -0
  23. package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +179 -0
  24. package/src/Analysis/Passes/Transform/CollapseGradientTransform.ts +176 -0
  25. package/src/Analysis/Passes/Transform/MergeColorsTransform.ts +126 -0
  26. package/src/Analysis/Passes/Transform/RainbowCollapseTransform.ts +83 -0
  27. package/src/Analysis/Passes/Transform/WaveCollapseTransform.ts +88 -0
  28. package/src/Analysis/Passes/Utility/CharacterCountAnalyzer.ts +45 -0
  29. package/src/Analysis/Pipeline/Pipeline.ts +133 -0
  30. package/src/Analysis/Pipeline/PipelineBuilder.ts +55 -0
  31. package/src/Analysis/Pipeline/PipelineStage.ts +19 -0
  32. package/src/Analysis/Utils/color-utils.ts +132 -0
  33. package/src/Analysis/__tests__/Integration.test.ts +162 -0
  34. package/src/Analysis/__tests__/Pipeline.test.ts +133 -0
  35. package/src/Analysis/index.ts +52 -0
  36. package/src/BBCode/BBCodeDocumentModel.ts +175 -0
  37. package/src/BBCode/BBCodeToGreenNode.ts +755 -0
  38. package/src/BBCode/Parser.ts +384 -0
  39. package/src/BBCode/index.ts +12 -0
  40. package/src/Collab/positions.ts +91 -0
  41. package/src/Commands/Command.ts +44 -0
  42. package/src/Commands/CommandRegistry.ts +78 -0
  43. package/src/Commands/DeleteNode.ts +20 -0
  44. package/src/Commands/InsertText.ts +21 -0
  45. package/src/Commands/SplitMerge.ts +28 -0
  46. package/src/Commands/WrapInTag.ts +21 -0
  47. package/src/Commands/index.ts +6 -0
  48. package/src/Diff/TreeDiffer.ts +264 -0
  49. package/src/Diff/__tests__/TreeDiffer.test.ts +65 -0
  50. package/src/Diff/index.ts +2 -0
  51. package/src/Events/EventBus.ts +160 -0
  52. package/src/Events/index.ts +2 -0
  53. package/src/Formatter/Formatter.ts +54 -0
  54. package/src/Formatter/index.ts +2 -0
  55. package/src/HTML/HTMLDocumentModel.ts +35 -0
  56. package/src/HTML/HTMLToGreenNode.ts +290 -0
  57. package/src/Incremental/ChangeTracker.ts +105 -0
  58. package/src/Incremental/IncrementalParser.ts +591 -0
  59. package/src/Incremental/__tests__/IncrementalParser.test.ts +164 -0
  60. package/src/Incremental/index.ts +4 -0
  61. package/src/Lexer/BBCodeLexer.ts +382 -0
  62. package/src/Lexer/Lexer.ts +181 -0
  63. package/src/Lexer/index.ts +10 -0
  64. package/src/Linter/Linter.ts +193 -0
  65. package/src/Linter/index.ts +2 -0
  66. package/src/Markdown/MarkdownAST.ts +112 -0
  67. package/src/Markdown/MarkdownDocumentModel.ts +55 -0
  68. package/src/Markdown/MarkdownLexer.ts +203 -0
  69. package/src/Markdown/MarkdownParser.ts +455 -0
  70. package/src/Markdown/MarkdownToGreenNode.ts +153 -0
  71. package/src/Model/DocumentModel.ts +694 -0
  72. package/src/Model/NodeFactory.ts +117 -0
  73. package/src/Model/TagRegistry.ts +495 -0
  74. package/src/Model/index.ts +5 -0
  75. package/src/Plugins/PluginAPI.ts +119 -0
  76. package/src/Plugins/PluginRegistry.ts +132 -0
  77. package/src/Plugins/index.ts +3 -0
  78. package/src/Queries/QueryEngine.ts +152 -0
  79. package/src/Queries/index.ts +1 -0
  80. package/src/RenderPipeline/RenderPipeline.ts +125 -0
  81. package/src/RenderPipeline/RenderTree.ts +134 -0
  82. package/src/RenderPipeline/index.ts +4 -0
  83. package/src/Semantic/SemanticAnalyzer.ts +506 -0
  84. package/src/Semantic/index.ts +2 -0
  85. package/src/Symbols/SymbolTable.ts +124 -0
  86. package/src/Symbols/index.ts +1 -0
  87. package/src/Syntax/GreenNode.ts +324 -0
  88. package/src/Syntax/GreenNodePool.ts +269 -0
  89. package/src/Syntax/NodeMatcher.ts +370 -0
  90. package/src/Syntax/RedNode.ts +569 -0
  91. package/src/Syntax/RedNodeStore.ts +184 -0
  92. package/src/Syntax/TreeBuilder.ts +214 -0
  93. package/src/Syntax/__tests__/GreenNode.test.ts +33 -0
  94. package/src/Syntax/__tests__/RedNode.test.ts +81 -0
  95. package/src/Syntax/__tests__/RedNodeStore.test.ts +104 -0
  96. package/src/Syntax/greenEdit.ts +110 -0
  97. package/src/Syntax/hash.ts +30 -0
  98. package/src/Syntax/index.ts +12 -0
  99. package/src/Syntax/partition.ts +161 -0
  100. package/src/Syntax/preserveNodeIds.ts +201 -0
  101. package/src/Tests/ASTOptimizerIdempotence.test.ts +77 -0
  102. package/src/Tests/BlockPatcher.test.ts +437 -0
  103. package/src/Tests/BlockPatcherWindowed.test.ts +364 -0
  104. package/src/Tests/BoxDrawer.test.ts +217 -0
  105. package/src/Tests/BoxRichTitle.test.ts +105 -0
  106. package/src/Tests/Chars500kBenchmark.test.ts +151 -0
  107. package/src/Tests/Chars500kEdits.test.ts +321 -0
  108. package/src/Tests/CollabPositions.test.ts +146 -0
  109. package/src/Tests/CompilerPathProfiling.test.ts +186 -0
  110. package/src/Tests/DOMMorpher.test.ts +142 -0
  111. package/src/Tests/DomPatchPerf.test.ts +60 -0
  112. package/src/Tests/EffectSegments.snapshot.json +616 -0
  113. package/src/Tests/EffectSegments.test.ts +68 -0
  114. package/src/Tests/FindNodeAtOffset.test.ts +65 -0
  115. package/src/Tests/Fuzzer.test.ts +166 -0
  116. package/src/Tests/GreenNodePool.test.ts +153 -0
  117. package/src/Tests/Lexer.test.ts +238 -0
  118. package/src/Tests/LyneMode.test.ts +187 -0
  119. package/src/Tests/ModelCoherence.test.ts +180 -0
  120. package/src/Tests/Partition.test.ts +238 -0
  121. package/src/Tests/PluginTags.test.ts +150 -0
  122. package/src/Tests/ProblematicSection.test.ts +46 -0
  123. package/src/Tests/ProblematicSectionHTML.test.ts +58 -0
  124. package/src/Tests/RedReuse.test.ts +134 -0
  125. package/src/Tests/ReproDelete20k.test.ts +62 -0
  126. package/src/Tests/SemanticValidators.test.ts +136 -0
  127. package/src/Tests/StableNodeIds.test.ts +210 -0
  128. package/src/Tests/StudioColorBloat.test.ts +25 -0
  129. package/src/Tests/StudioDebugText.test.ts +27 -0
  130. package/src/Tests/StudioTrailingChar.test.ts +25 -0
  131. package/src/Tests/StudioValidText.test.ts +25 -0
  132. package/src/Tests/UrlImgBug.test.ts +23 -0
  133. package/src/Tests/VisualBuilderFidelity.test.ts +105 -0
  134. package/src/Tests/referenceDocument.ts +119 -0
  135. package/src/Transactions/Transaction.ts +176 -0
  136. package/src/Transactions/UndoManager.ts +111 -0
  137. package/src/Transactions/index.ts +3 -0
  138. package/src/Transformers/ASTOptimizer.ts +315 -0
  139. package/src/Transformers/GradientTransformer.ts +143 -0
  140. package/src/Transformers/GrowTransformer.ts +115 -0
  141. package/src/Transformers/RainbowTransformer.ts +121 -0
  142. package/src/Transformers/SineWaveTransformer.ts +130 -0
  143. package/src/Transformers/Transformer.ts +22 -0
  144. package/src/Types/core.ts +270 -0
  145. package/src/Types/diagnostics.ts +156 -0
  146. package/src/Types/index.ts +23 -0
  147. package/src/Types/operations.ts +180 -0
  148. package/src/Types/queries.ts +121 -0
  149. package/src/Types/symbols.ts +69 -0
  150. package/src/Types/tokens.ts +186 -0
  151. package/src/Utils/BBCodeGenerator.ts +126 -0
  152. package/src/Utils/ColorMath.ts +276 -0
  153. package/src/Utils/color.ts +112 -0
  154. package/src/Utils/dom-to-svg.test.ts +86 -0
  155. package/src/Utils/dom-to-svg.ts +615 -0
  156. package/src/Utils/treeTransformers.ts +717 -0
  157. package/src/Visitors/BBBlocksExporter.ts +69 -0
  158. package/src/Visitors/BBCodeExporter.ts +318 -0
  159. package/src/Visitors/BlockPatcher.ts +963 -0
  160. package/src/Visitors/DOMMorpher.ts +134 -0
  161. package/src/Visitors/HTMLRenderer.ts +1077 -0
  162. package/src/Visitors/JSONExporter.ts +66 -0
  163. package/src/Visitors/MarkdownExporter.ts +99 -0
  164. package/src/Visitors/SVGRenderer.ts +35 -0
  165. package/src/Visitors/TiptapExporter.ts +145 -0
  166. package/src/Visitors/Visitor.ts +48 -0
  167. package/src/Visitors/index.ts +9 -0
  168. package/src/Visuals/BoxDrawer.ts +175 -0
  169. package/src/Visuals/index.ts +42 -0
  170. package/src/Visuals/lyne.css +1027 -0
  171. package/src/Visuals/osu.css +257 -0
  172. package/src/index.ts +197 -0
@@ -0,0 +1,569 @@
1
+ /**
2
+ * DocumentEngine — RedNode (Mutable Syntax Tree)
3
+ *
4
+ * The Red Tree wraps GreenNode and adds mutable state:
5
+ * - Parent references
6
+ * - Stable Node IDs
7
+ * - Version tracking
8
+ * - Diagnostic storage
9
+ *
10
+ * Multiple Red Trees can reference the same Green Tree
11
+ * (different views, different versions of edits).
12
+ *
13
+ * Inspired by Roslyn's Red Tree.
14
+ */
15
+
16
+ import { GreenNode } from './GreenNode'
17
+ import type { NodeId, DocumentNode, NodeKind, NodeAttributes, NodeMetadata } from '../Types/core'
18
+ import { createNodeId } from '../Types/core'
19
+ import type { Diagnostic } from '../Types/diagnostics'
20
+ import type { Range } from '../Types/tokens'
21
+
22
+ // ─── Red Node ──────────────────────────────────────────────────
23
+
24
+ export class RedNode {
25
+ /** The underlying immutable green node */
26
+ readonly green: GreenNode
27
+ /** Stable identity */
28
+ readonly id: NodeId
29
+ /** Parent reference (null for root) */
30
+ parent: RedNode | null
31
+ /** Children as red nodes */
32
+ children: RedNode[]
33
+ /** Version counter for change tracking */
34
+ version: number
35
+ /** Diagnostics for this node */
36
+ diagnostics: Diagnostic[]
37
+ /** The semantic kind */
38
+ kind: NodeKind
39
+ /** Additional metadata */
40
+ metadata: NodeMetadata
41
+
42
+ /**
43
+ * Cached position of this node inside `parent.children`.
44
+ *
45
+ * `declare` is load-bearing — see the note on `GreenNode._hashCache`: with
46
+ * `target: ES2022` a plain field declaration emits a `defineProperty` on every
47
+ * construction, and this class is instantiated once per node per parse.
48
+ *
49
+ * The cache is *self-validating*: `index` only trusts it when
50
+ * `parent.children[_idxCache] === this`. That single reference compare is what
51
+ * makes it safe against code that mutates the `children` array directly
52
+ * (`treeTransformers` does `cloned.children = []`) instead of going through the
53
+ * mutation methods — a stale entry is detected and recomputed rather than
54
+ * silently returning a wrong index. `-1` never validates, so it is a safe
55
+ * initial value.
56
+ */
57
+ private declare _idxCache: number
58
+
59
+ /**
60
+ * Absolute start offset in the source.
61
+ *
62
+ * This is the red tree's job now: a green node knows its width, not its
63
+ * place, so that identical structures can be the same object (see the header
64
+ * of `GreenNode.ts`). The offset is accumulated on the way down during
65
+ * construction — one addition per node — and stored, so `range` stays the
66
+ * O(1) property every consumer already assumes it is.
67
+ *
68
+ * Held as the `Range` object itself rather than a number plus a getter that
69
+ * builds one: `range` is read from 148 call sites, and allocating there would
70
+ * trade a parse-time win for a read-time cost on every consumer. This is the
71
+ * same shape green nodes used to hold, so nothing downstream changes.
72
+ *
73
+ * `declare` for the usual reason: no `defineProperty` per construction.
74
+ */
75
+ private declare _range: Range
76
+
77
+ /**
78
+ * Pending offset shift for this subtree, applied on first read.
79
+ *
80
+ * `setStart` defers the walk: it records how far the subtree moved instead of
81
+ * adding the delta to every node's `_range` on the spot. The shift is a single
82
+ * integer here, on the adopted subtree ROOT — the node `setStart` was called
83
+ * on. Any offset read (`range`, `innerStart`, `innerEnd`, `findNodeAtOffset`)
84
+ * materializes the nearest pending ancestor's subtree via {@link materialize}.
85
+ *
86
+ * Why this is sound: a mid-document edit displaces every adopted block after
87
+ * it by the same delta, so adoption alone (thousands of `setStart` calls) was
88
+ * walking thousands of nodes per keystroke — measured as the dominant phase
89
+ * of `buildRed` (3.5-5.8 ms on the 547 KB fixture). The BlockPatcher locates
90
+ * the edited window by reference identity and only ever reads the few changed
91
+ * blocks' ranges, so the displaced-but-unchanged subtrees stay pending
92
+ * indefinitely — the walk never happens.
93
+ *
94
+ * Contract for future readers: app-layer interaction code (hover, links,
95
+ * selection ranges) DOES read `range` on user interaction. The first such
96
+ * read after a mid-document edit fires the deferred walk for the displaced
97
+ * tail — a one-time cost off the keystroke path, which is exactly the trade
98
+ * the laziness makes. Do not "optimize" by reading ranges back into the
99
+ * per-keystroke pipeline; the patcher stays churn-based by design.
100
+ *
101
+ * Composition across reparses: a node re-adopted while its ancestor is still
102
+ * pending must end up at the SUM of both deltas. `setStart` recomputes the
103
+ * delta from the untouched base `_range`, which is exactly the pending
104
+ * ancestor's base too, so composing during `materialize` (add the ancestor's
105
+ * delta into a child's pending delta) stays consistent.
106
+ */
107
+ private declare _lazyShift: number
108
+
109
+ constructor(
110
+ green: GreenNode,
111
+ options?: {
112
+ id?: NodeId
113
+ parent?: RedNode | null
114
+ kind?: NodeKind
115
+ diagnostics?: Diagnostic[]
116
+ metadata?: NodeMetadata
117
+ /** Absolute start offset. Defaults to the parent's inner offset. */
118
+ start?: number
119
+ },
120
+ ) {
121
+ this.green = green
122
+ this.id = options?.id ?? createNodeId()
123
+ this.parent = options?.parent ?? null
124
+ this.children = []
125
+ this.version = 1
126
+ this.diagnostics = options?.diagnostics ?? []
127
+ this.metadata = options?.metadata ?? {}
128
+ this.kind = options?.kind ?? (green.kind as NodeKind)
129
+ this._idxCache = -1
130
+ this._lazyShift = 0
131
+ const start = options?.start ?? 0
132
+ this._range = { start, end: start + green.width }
133
+ }
134
+
135
+ /**
136
+ * Move this node (and its subtree) to a new absolute offset.
137
+ *
138
+ * Only for the tree-building paths, which create a node before they know
139
+ * where its parent will put it. Callers that mutate a live tree should
140
+ * rebuild instead — a red node's offset must always agree with its place.
141
+ *
142
+ * The move is deferred, not walked: the delta is recorded as a pending
143
+ * shift and applied by any offset read the first time the subtree is
144
+ * actually touched. Adoption calls this once per displaced block (thousands
145
+ * on a mid-document edit), and almost none of those subtrees are read on the
146
+ * same keystroke's hot path — the BlockPatcher locates the edit window by
147
+ * reference identity and only reads the few changed blocks.
148
+ *
149
+ * Re-adoption composes: a node whose subtree was shifted in a previous
150
+ * reparse and never read still carries a pending `_lazyShift`. The new
151
+ * target is absolute, so the delta is measured from the CURRENT effective
152
+ * start (`_range.start + _lazyShift`) and accumulated, not overwritten —
153
+ * otherwise the earlier shift would be applied twice.
154
+ */
155
+ setStart(start: number): void {
156
+ const delta = start - (this._range.start + this._lazyShift)
157
+ if (delta === 0) return
158
+ this._lazyShift += delta
159
+ }
160
+
161
+ /**
162
+ * Apply `delta` to `node`'s subtree, composing with any nested pending shift.
163
+ *
164
+ * A node with its own pending shift has a base `_range` the parent's delta is
165
+ * relative to as well (both were computed from the same pre-shift tree), so
166
+ * the parent's delta can be folded into the child's pending delta instead of
167
+ * into its `_range` — the child's later materialization applies the sum. This
168
+ * is what makes nested shifts across reparses compose without double counting.
169
+ */
170
+ private static applyShift(node: RedNode, delta: number): void {
171
+ if (delta === 0) return
172
+ if (node._lazyShift !== 0) {
173
+ node._lazyShift += delta
174
+ return
175
+ }
176
+ node._range.start += delta
177
+ node._range.end += delta
178
+ const titleNodes = node.metadata?.titleNodes as RedNode[] | undefined
179
+ if (titleNodes) {
180
+ for (let i = 0; i < titleNodes.length; i++) {
181
+ RedNode.applyShift(titleNodes[i], delta)
182
+ }
183
+ }
184
+ for (let i = 0; i < node.children.length; i++) {
185
+ RedNode.applyShift(node.children[i], delta)
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Materialize every pending shift on the path from here to the root.
191
+ *
192
+ * Walks ancestors top-down (root-most first), applying each pending delta to
193
+ * its subtree. Top-down order matters: an ancestor's delta must land in a
194
+ * child's pending delta BEFORE the child's own materialization runs, or the
195
+ * two shifts would be applied to different bases. After the ancestors are
196
+ * settled, this node itself is materialized if it still carries a shift.
197
+ *
198
+ * The common case — nothing pending anywhere on the path — is one upward
199
+ * pointer walk that allocates nothing: `_range` is read from 148 call sites,
200
+ * and a per-read allocation there would show up in every phase. The chain
201
+ * array is only built after a pending shift is actually found.
202
+ */
203
+ private materialize(): void {
204
+ // Fast path: no pending shift on this node or any ancestor.
205
+ let node: RedNode | null = this
206
+ while (node !== null && node._lazyShift === 0) {
207
+ node = node.parent
208
+ }
209
+ if (node === null) return
210
+
211
+ // Slow path: collect the ancestor chain, root-most last (so we pop
212
+ // root-first), and materialize each pending shift top-down.
213
+ const chain: RedNode[] = []
214
+ let n: RedNode | null = this
215
+ while (n !== null) {
216
+ chain.push(n)
217
+ n = n.parent
218
+ }
219
+ for (let i = chain.length - 1; i >= 0; i--) {
220
+ const ancestor = chain[i]
221
+ const delta = ancestor._lazyShift
222
+ if (delta === 0) continue
223
+ ancestor._lazyShift = 0
224
+ ancestor._range.start += delta
225
+ ancestor._range.end += delta
226
+ for (let c = 0; c < ancestor.children.length; c++) {
227
+ RedNode.applyShift(ancestor.children[c], delta)
228
+ }
229
+ }
230
+ }
231
+
232
+ // ─── Mutation boundary ───────────────────────────────────
233
+ //
234
+ // What this is, stated plainly, because the documentation used to claim more:
235
+ // a **convention enforcer**, not an isolation mechanism and not a concurrency
236
+ // guarantee.
237
+ //
238
+ // It cannot be an isolation mechanism. The flag is a single static boolean, so
239
+ // opening a boundary anywhere opens it everywhere — there is no scoping by
240
+ // document, transaction or thread. Scoping it per document would mean walking
241
+ // to the root on every mutation and threading a root through all 26 call
242
+ // sites, to catch cross-document mutation: a bug class a single-threaded
243
+ // editor does not have.
244
+ //
245
+ // It cannot be airtight either. `children` is a public mutable array, and
246
+ // `treeTransformers` assigns to it directly in five places, bypassing these
247
+ // methods entirely. Sealing that means making `children` private, which is a
248
+ // large API change for the same small benefit.
249
+ //
250
+ // What it does earn: wrapping a structural edit marks intent at the call site,
251
+ // and mutating outside a boundary fails loudly instead of silently. That is
252
+ // worth one boolean check, so it stays — described honestly.
253
+
254
+ private static _isMutating = false
255
+
256
+ /**
257
+ * Run `fn` inside a mutation boundary, so the structural mutators below are
258
+ * allowed to run. Nests safely; the previous state is restored on exit.
259
+ *
260
+ * Not needed for building a fresh subtree — see {@link initChildren}.
261
+ */
262
+ static allowMutation<T>(fn: () => T): T {
263
+ const wasMutating = RedNode._isMutating
264
+ RedNode._isMutating = true
265
+ try {
266
+ return fn()
267
+ } finally {
268
+ RedNode._isMutating = wasMutating
269
+ }
270
+ }
271
+
272
+ private assertMutating(): void {
273
+ if (!RedNode._isMutating) {
274
+ throw new Error('RedNode mutation is only allowed within a mutation boundary (use RedNode.allowMutation).')
275
+ }
276
+ }
277
+
278
+ // ─── Properties ──────────────────────────────────────────
279
+
280
+ get text(): string {
281
+ return this.green.text
282
+ }
283
+
284
+ /** Absolute span in the source. */
285
+ get range(): Range {
286
+ this.materialize()
287
+ return this._range
288
+ }
289
+
290
+ /** Absolute offset of this node's first child, past its opening delimiter. */
291
+ get innerStart(): number {
292
+ this.materialize()
293
+ return this._range.start + this.green.leadingWidth
294
+ }
295
+
296
+ /** Absolute offset where this node's closing delimiter begins. */
297
+ get innerEnd(): number {
298
+ this.materialize()
299
+ return this._range.end - this.green.trailingWidth
300
+ }
301
+
302
+ get isLeaf(): boolean {
303
+ return this.green.isLeaf
304
+ }
305
+
306
+ get childCount(): number {
307
+ return this.children.length
308
+ }
309
+
310
+ // ─── Navigation ──────────────────────────────────────────
311
+
312
+ childAt(index: number): RedNode | undefined {
313
+ return this.children[index]
314
+ }
315
+
316
+ /** Get the root of the tree */
317
+ get root(): RedNode {
318
+ let node: RedNode = this
319
+ while (node.parent) {
320
+ node = node.parent
321
+ }
322
+ return node
323
+ }
324
+
325
+ /** Get the depth from root */
326
+ get depth(): number {
327
+ let d = 0
328
+ let node: RedNode | null = this.parent
329
+ while (node) {
330
+ d++
331
+ node = node.parent
332
+ }
333
+ return d
334
+ }
335
+
336
+ /** Get previous sibling */
337
+ get previousSibling(): RedNode | null {
338
+ const parent = this.parent
339
+ if (!parent) return null
340
+ const idx = this.index
341
+ return idx > 0 ? parent.children[idx - 1] : null
342
+ }
343
+
344
+ /** Get next sibling */
345
+ get nextSibling(): RedNode | null {
346
+ const parent = this.parent
347
+ if (!parent) return null
348
+ const idx = this.index
349
+ return idx >= 0 && idx < parent.children.length - 1 ? parent.children[idx + 1] : null
350
+ }
351
+
352
+ /**
353
+ * Position of this node within its parent, or -1 for a root.
354
+ *
355
+ * O(1) in the steady state. The cached index is seeded by the mutation methods
356
+ * (so the construction path never searches at all) and verified by a single
357
+ * reference compare on read, falling back to `indexOf` only when it is stale.
358
+ *
359
+ * This getter is hot: `HTMLRenderer.isPrevBlockBoundary` walks siblings
360
+ * backwards, so an O(n) implementation here made rendering quadratic —
361
+ * measured 14.11 ms for 4000 root siblings, growing 2.6x per doubling.
362
+ */
363
+ get index(): number {
364
+ const parent = this.parent
365
+ if (!parent) return -1
366
+ const siblings = parent.children
367
+ const cached = this._idxCache
368
+ if (siblings[cached] === this) return cached
369
+ const idx = siblings.indexOf(this)
370
+ this._idxCache = idx
371
+ return idx
372
+ }
373
+
374
+ /** Walk all descendants in pre-order */
375
+ walk(visitor: (node: RedNode, depth: number) => void | 'skip', depth: number = 0): void {
376
+ const result = visitor(this, depth)
377
+ if (result !== 'skip') {
378
+ for (const child of this.children) {
379
+ child.walk(visitor, depth + 1)
380
+ }
381
+ }
382
+ }
383
+
384
+ /** Find a descendant by ID */
385
+ findById(id: NodeId): RedNode | null {
386
+ if (this.id === id) return this
387
+ for (const child of this.children) {
388
+ const found = child.findById(id)
389
+ if (found) return found
390
+ }
391
+ return null
392
+ }
393
+
394
+ /**
395
+ * Find the deepest RedNode containing the given text offset.
396
+ *
397
+ * The range is half-open — a node owns `[start, end)` — y eso no es un
398
+ * detalle: con el final inclusivo, un offset que cae justo en una frontera
399
+ * pertenecía a DOS nodos, al que termina ahí y al que empieza. Como los
400
+ * hijos se recorren en orden, ganaba el que termina, así que preguntar por
401
+ * el principio de un nodo devolvía **el nodo anterior**.
402
+ *
403
+ * Se veía en el preview: en un degradado, cada carácter es su propio nodo
404
+ * de color, y hacer clic en la `m` de «Welcome» resaltaba la `o`. Monaco
405
+ * seleccionaba bien —usa el rango del nodo, no esta búsqueda—, así que el
406
+ * desfase era solo del resaltado, que es justo lo que hacía difícil verlo.
407
+ *
408
+ * La única excepción es el final del documento: un cursor aparcado tras el
409
+ * último carácter no tiene carácter que lo contenga, y la raíz lo reclama
410
+ * para que quien pregunte reciba algo con sentido.
411
+ */
412
+ findNodeAtOffset(offset: number): RedNode | null {
413
+ this.materialize()
414
+ const { start, end } = this._range
415
+ const isEndOfDocument = offset === end && this.parent === null
416
+ if ((offset < start || offset >= end) && !isEndOfDocument) return null
417
+
418
+ const titleNodes = this.metadata?.titleNodes as RedNode[] | undefined
419
+ if (titleNodes) {
420
+ for (const titleChild of titleNodes) {
421
+ const found = titleChild.findNodeAtOffset(offset)
422
+ if (found) return found
423
+ }
424
+ }
425
+
426
+ for (const child of this.children) {
427
+ const found = child.findNodeAtOffset(offset)
428
+ if (found) return found
429
+ }
430
+
431
+ return this
432
+ }
433
+
434
+ // ─── Construction ────────────────────────────────────────
435
+
436
+ /**
437
+ * Adopt a fully-built array of children in one shot.
438
+ *
439
+ * Construction-only, and deliberately outside the mutation lock. The tree
440
+ * builders used to call `allowMutation(() => { for (…) appendChild(…) })` once
441
+ * **per node**, which on a 1736-node document meant 1736 closures, 1736
442
+ * `try/finally` frames and 1736 static-flag saves — pure ceremony, since a
443
+ * node that no caller has seen yet cannot be observed mid-mutation. This
444
+ * populates `children`, `parent` and the index cache in a single pass instead.
445
+ *
446
+ * `version` intentionally stays at 1: a freshly built node has not been
447
+ * *edited*, and bumping once per child would make the initial version an
448
+ * accidental child count.
449
+ *
450
+ * Only safe while `this` is still unreachable from the rest of the tree. Use
451
+ * `appendChild` and friends for anything after that.
452
+ */
453
+ initChildren(children: RedNode[]): void {
454
+ const own = this.children
455
+ for (let i = 0; i < children.length; i++) {
456
+ const child = children[i]
457
+ child.parent = this
458
+ child._idxCache = i
459
+ own.push(child)
460
+ }
461
+ }
462
+
463
+ // ─── Mutation ────────────────────────────────────────────
464
+
465
+ /**
466
+ * Append a child node.
467
+ * This creates a new RedNode wrapping the green node.
468
+ */
469
+ appendChild(child: RedNode): void {
470
+ this.assertMutating()
471
+ child.parent = this
472
+ child._idxCache = this.children.length
473
+ this.children.push(child)
474
+ this.version++
475
+ }
476
+
477
+ /**
478
+ * Refresh the cached index of every child from `from` onwards.
479
+ *
480
+ * Called after a splice, which shifts the tail. This is O(n) but so is the
481
+ * splice it follows, so it costs nothing asymptotically and keeps reads O(1).
482
+ */
483
+ private reindexFrom(from: number): void {
484
+ const children = this.children
485
+ for (let i = from; i < children.length; i++) {
486
+ children[i]._idxCache = i
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Insert a child at a specific index.
492
+ */
493
+ insertChildAt(index: number, child: RedNode): void {
494
+ this.assertMutating()
495
+ child.parent = this
496
+ this.children.splice(index, 0, child)
497
+ this.reindexFrom(index)
498
+ this.version++
499
+ }
500
+
501
+ /**
502
+ * Remove a child by ID.
503
+ */
504
+ removeChild(id: NodeId): RedNode | null {
505
+ this.assertMutating()
506
+ const idx = this.children.findIndex(c => c.id === id)
507
+ if (idx === -1) return null
508
+ const [removed] = this.children.splice(idx, 1)
509
+ removed.parent = null
510
+ removed._idxCache = -1
511
+ this.reindexFrom(idx)
512
+ this.version++
513
+ return removed
514
+ }
515
+
516
+ /**
517
+ * Replace a child with a new one by ID.
518
+ */
519
+ replaceChild(id: NodeId, newChild: RedNode): boolean {
520
+ this.assertMutating()
521
+ const idx = this.children.findIndex(c => c.id === id)
522
+ if (idx === -1) return false
523
+ newChild.parent = this
524
+ newChild._idxCache = idx
525
+ this.children[idx] = newChild
526
+ this.version++
527
+ return true
528
+ }
529
+
530
+ /**
531
+ * Bump version (when attributes/metadata change without structural change).
532
+ */
533
+ bumpVersion(): void {
534
+ this.assertMutating()
535
+ this.version++
536
+ }
537
+
538
+ // ─── Conversion ──────────────────────────────────────────
539
+
540
+ /** Convert to the public DocumentNode interface */
541
+ toDocumentNode(): DocumentNode {
542
+ return {
543
+ id: this.id,
544
+ version: this.version,
545
+ kind: this.kind,
546
+ text: this.text,
547
+ attributes: {},
548
+ metadata: { ...this.metadata },
549
+ children: this.children.map(c => c.toDocumentNode()),
550
+ diagnostics: [...this.diagnostics],
551
+ sourceRange: { start: this.range.start, end: this.range.end },
552
+ parentId: this.parent?.id ?? null,
553
+ isSynthetic: this.green.kind === 'synthetic',
554
+ }
555
+ }
556
+
557
+ /** Debug string */
558
+ toString(depth: number = 0): string {
559
+ const indent = ' '.repeat(depth)
560
+ const rangeStr = `[${this.range.start}..${this.range.end}]`
561
+ const textPreview = this.text.length > 30
562
+ ? this.text.slice(0, 30) + '...'
563
+ : this.text
564
+ return `${indent}${this.kind} #${this.id} ${rangeStr} v${this.version} "${textPreview}"` +
565
+ (this.children.length > 0
566
+ ? '\n' + this.children.map(c => c.toString(depth + 1)).join('\n')
567
+ : '')
568
+ }
569
+ }