@abide/abide 0.49.0 → 0.50.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 (325) hide show
  1. package/AGENTS.md +70 -52
  2. package/CHANGELOG.md +177 -0
  3. package/README.md +12 -7
  4. package/package.json +14 -5
  5. package/src/abideLsp.ts +5 -6
  6. package/src/abideResolverPlugin.ts +197 -169
  7. package/src/build.ts +79 -56
  8. package/src/buildDisconnected.ts +0 -2
  9. package/src/checkAbide.ts +12 -2
  10. package/src/devEntry.ts +113 -28
  11. package/src/discoveryEntry.ts +3 -2
  12. package/src/lib/bundle/disconnected.abide +69 -73
  13. package/src/lib/bundle/infoPlist.ts +13 -7
  14. package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
  15. package/src/lib/bundle/waitForServer.ts +6 -1
  16. package/src/lib/mcp/mcpTools.ts +15 -6
  17. package/src/lib/server/error.ts +6 -6
  18. package/src/lib/server/json.ts +17 -3
  19. package/src/lib/server/rpc/defineRpc.ts +58 -24
  20. package/src/lib/server/rpc/parseArgs.ts +98 -13
  21. package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
  22. package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
  23. package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
  24. package/src/lib/server/rpc/validationError.ts +1 -1
  25. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
  26. package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
  27. package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
  28. package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
  29. package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
  30. package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
  31. package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
  32. package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
  33. package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
  34. package/src/lib/server/runtime/createServer.ts +120 -324
  35. package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
  36. package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
  37. package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
  38. package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
  39. package/src/lib/server/runtime/pathStore.ts +15 -0
  40. package/src/lib/server/runtime/registryManifests.ts +1 -1
  41. package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
  42. package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
  43. package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
  44. package/src/lib/server/runtime/snapshotEntryFromCache.ts +14 -14
  45. package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
  46. package/src/lib/server/runtime/streamFromIterator.ts +30 -2
  47. package/src/lib/server/runtime/textResponse.ts +23 -0
  48. package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
  49. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  50. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  51. package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
  52. package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
  53. package/src/lib/server/sockets/defineSocket.ts +5 -4
  54. package/src/lib/server/sse.ts +5 -1
  55. package/src/lib/shared/ASYNC_CELL.ts +5 -0
  56. package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
  57. package/src/lib/shared/HTTP_METHODS.ts +6 -0
  58. package/src/lib/shared/HttpError.ts +1 -5
  59. package/src/lib/shared/MCP_PATH.ts +6 -0
  60. package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
  61. package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
  62. package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
  63. package/src/lib/shared/activeCacheStore.ts +1 -0
  64. package/src/lib/shared/activePage.ts +1 -0
  65. package/src/lib/shared/buildArtifact.ts +4 -1
  66. package/src/lib/shared/buildSocketOverChannel.ts +4 -3
  67. package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
  68. package/src/lib/shared/cache.ts +156 -131
  69. package/src/lib/shared/changeAffectsClient.ts +12 -7
  70. package/src/lib/shared/createCacheStore.ts +31 -8
  71. package/src/lib/shared/createLifecycleChannel.ts +7 -1
  72. package/src/lib/shared/createReachable.ts +47 -78
  73. package/src/lib/shared/createRemoteFunction.ts +52 -30
  74. package/src/lib/shared/createRpcServerProgram.ts +820 -0
  75. package/src/lib/shared/decodeRefJson.ts +4 -4
  76. package/src/lib/shared/decodeResponse.ts +2 -2
  77. package/src/lib/shared/decodeWireBody.ts +35 -0
  78. package/src/lib/shared/detectRpcMethod.ts +8 -1
  79. package/src/lib/shared/done.ts +8 -2
  80. package/src/lib/shared/encodeRefJson.ts +4 -4
  81. package/src/lib/shared/encodeWireBody.ts +18 -0
  82. package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
  83. package/src/lib/shared/generateDeclarations.ts +72 -0
  84. package/src/lib/shared/hasSeedableRequest.ts +25 -0
  85. package/src/lib/shared/hydrationWindow.ts +53 -0
  86. package/src/lib/shared/isAsyncCell.ts +11 -0
  87. package/src/lib/shared/isAsyncIterable.ts +8 -0
  88. package/src/lib/shared/isSubscribable.ts +3 -3
  89. package/src/lib/shared/isThenable.ts +9 -0
  90. package/src/lib/shared/jsonSchemaForType.ts +258 -0
  91. package/src/lib/shared/lenientDecode.ts +15 -0
  92. package/src/lib/shared/loadProjectTsConfig.ts +28 -0
  93. package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
  94. package/src/lib/shared/matchRoute.ts +4 -14
  95. package/src/lib/shared/peek.ts +14 -0
  96. package/src/lib/shared/pending.ts +9 -6
  97. package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
  98. package/src/lib/shared/prepareRpcModule.ts +91 -93
  99. package/src/lib/shared/prepareSocketModule.ts +3 -2
  100. package/src/lib/shared/probeRegistries.ts +5 -18
  101. package/src/lib/shared/reachable.ts +7 -10
  102. package/src/lib/shared/refresh.ts +12 -2
  103. package/src/lib/shared/refreshing.ts +8 -2
  104. package/src/lib/shared/resolvedCellsSlot.ts +13 -0
  105. package/src/lib/shared/reviveWireField.ts +49 -0
  106. package/src/lib/shared/reviveWireOutput.ts +36 -0
  107. package/src/lib/shared/rpcServerForRoot.ts +25 -0
  108. package/src/lib/shared/scanPages.ts +25 -0
  109. package/src/lib/shared/snapshotShippable.ts +8 -7
  110. package/src/lib/shared/streamedCellsSlot.ts +14 -0
  111. package/src/lib/shared/subscribableFromResponse.ts +4 -4
  112. package/src/lib/shared/subscribableProbes.ts +1 -1
  113. package/src/lib/shared/tailProbeSlot.ts +1 -1
  114. package/src/lib/shared/types/AsyncComputed.ts +20 -0
  115. package/src/lib/shared/types/AsyncState.ts +13 -0
  116. package/src/lib/shared/types/CacheEntry.ts +7 -7
  117. package/src/lib/shared/types/CacheOptions.ts +23 -37
  118. package/src/lib/shared/types/CachePolicy.ts +25 -0
  119. package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
  120. package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
  121. package/src/lib/shared/types/HttpMethod.ts +3 -1
  122. package/src/lib/shared/types/InputCoercion.ts +17 -0
  123. package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
  124. package/src/lib/shared/types/OutputWirePlan.ts +17 -0
  125. package/src/lib/shared/types/PagesScan.ts +7 -0
  126. package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
  127. package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
  128. package/src/lib/shared/types/RemoteCallable.ts +8 -7
  129. package/src/lib/shared/types/RemoteFunction.ts +16 -15
  130. package/src/lib/shared/types/ResolvedCells.ts +11 -0
  131. package/src/lib/shared/types/ReturnBody.ts +15 -0
  132. package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
  133. package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
  134. package/src/lib/shared/types/Socket.ts +4 -4
  135. package/src/lib/shared/types/SsrPayload.ts +5 -1
  136. package/src/lib/shared/types/StreamPolicy.ts +11 -0
  137. package/src/lib/shared/types/StreamedCells.ts +19 -0
  138. package/src/lib/shared/types/StreamedResolution.ts +24 -6
  139. package/src/lib/shared/types/TailHooks.ts +1 -1
  140. package/src/lib/shared/types/WireKind.ts +11 -0
  141. package/src/lib/shared/validationHttpError.ts +33 -0
  142. package/src/lib/shared/warmSeedKey.ts +16 -0
  143. package/src/lib/shared/wireJsonReplacer.ts +30 -0
  144. package/src/lib/shared/writeRpcDts.ts +11 -1
  145. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  146. package/src/lib/test/createTestApp.ts +19 -1
  147. package/src/lib/ui/README.md +1 -1
  148. package/src/lib/ui/activePendingCells.ts +14 -0
  149. package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
  150. package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
  151. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -8
  152. package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
  153. package/src/lib/ui/compile/analyzeComponent.ts +89 -6
  154. package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
  155. package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
  156. package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
  157. package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
  158. package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
  159. package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
  160. package/src/lib/ui/compile/catchBinding.ts +7 -5
  161. package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
  162. package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
  163. package/src/lib/ui/compile/compileComponent.ts +19 -3
  164. package/src/lib/ui/compile/compileModule.ts +27 -58
  165. package/src/lib/ui/compile/compileSSR.ts +51 -12
  166. package/src/lib/ui/compile/compileShadow.ts +145 -18
  167. package/src/lib/ui/compile/composeProps.ts +12 -2
  168. package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
  169. package/src/lib/ui/compile/createShadowProgram.ts +35 -6
  170. package/src/lib/ui/compile/declaredNames.ts +36 -0
  171. package/src/lib/ui/compile/desugarSignals.ts +418 -36
  172. package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
  173. package/src/lib/ui/compile/generateBuild.ts +52 -9
  174. package/src/lib/ui/compile/generateSSR.ts +242 -43
  175. package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
  176. package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
  177. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
  178. package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
  179. package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
  180. package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
  181. package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
  182. package/src/lib/ui/compile/lowerContext.ts +10 -0
  183. package/src/lib/ui/compile/lowerDocAccess.ts +26 -23
  184. package/src/lib/ui/compile/lowerScript.ts +40 -4
  185. package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
  186. package/src/lib/ui/compile/parseTemplate.ts +18 -1092
  187. package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
  188. package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
  189. package/src/lib/ui/compile/renameSignalRefs.ts +21 -22
  190. package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
  191. package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
  192. package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
  193. package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
  194. package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
  195. package/src/lib/ui/compile/templateStartOffset.ts +15 -0
  196. package/src/lib/ui/compile/tryPlan.ts +4 -3
  197. package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
  198. package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
  199. package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
  200. package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
  201. package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
  202. package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
  203. package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
  204. package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
  205. package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
  206. package/src/lib/ui/computed.ts +28 -1
  207. package/src/lib/ui/createScope.ts +35 -68
  208. package/src/lib/ui/dom/anchoredBranch.ts +142 -0
  209. package/src/lib/ui/dom/appendText.ts +8 -3
  210. package/src/lib/ui/dom/awaitBlock.ts +43 -94
  211. package/src/lib/ui/dom/bindProp.ts +22 -0
  212. package/src/lib/ui/dom/bindableProp.ts +47 -0
  213. package/src/lib/ui/dom/cellPending.ts +24 -0
  214. package/src/lib/ui/dom/disposeRange.ts +2 -1
  215. package/src/lib/ui/dom/each.ts +26 -4
  216. package/src/lib/ui/dom/eachAsync.ts +39 -3
  217. package/src/lib/ui/dom/fillBoundary.ts +2 -3
  218. package/src/lib/ui/dom/fillRange.ts +4 -4
  219. package/src/lib/ui/dom/hydrate.ts +6 -12
  220. package/src/lib/ui/dom/isComment.ts +1 -1
  221. package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
  222. package/src/lib/ui/dom/mount.ts +1 -2
  223. package/src/lib/ui/dom/mountChild.ts +9 -40
  224. package/src/lib/ui/dom/mountRange.ts +2 -3
  225. package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
  226. package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
  227. package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
  228. package/src/lib/ui/dom/on.ts +2 -2
  229. package/src/lib/ui/dom/readCell.ts +40 -0
  230. package/src/lib/ui/dom/switchBlock.ts +30 -7
  231. package/src/lib/ui/dom/tryBlock.ts +230 -66
  232. package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
  233. package/src/lib/ui/dom/when.ts +12 -2
  234. package/src/lib/ui/dom/withScope.ts +2 -2
  235. package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
  236. package/src/lib/ui/flight.ts +56 -0
  237. package/src/lib/ui/html.ts +12 -1
  238. package/src/lib/ui/isolateCellBarrier.ts +17 -0
  239. package/src/lib/ui/linked.ts +48 -2
  240. package/src/lib/ui/remoteProxy.ts +84 -159
  241. package/src/lib/ui/renderChain.ts +49 -13
  242. package/src/lib/ui/renderToStream.ts +22 -18
  243. package/src/lib/ui/resumeSeedScript.ts +1 -1
  244. package/src/lib/ui/router.ts +63 -36
  245. package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
  246. package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
  247. package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
  248. package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
  249. package/src/lib/ui/runtime/RENDER.ts +10 -7
  250. package/src/lib/ui/runtime/RESUME.ts +4 -4
  251. package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
  252. package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
  253. package/src/lib/ui/runtime/applyPatchToTree.ts +1 -1
  254. package/src/lib/ui/runtime/blockId.ts +31 -0
  255. package/src/lib/ui/runtime/boundaryFor.ts +11 -0
  256. package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
  257. package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
  258. package/src/lib/ui/runtime/createDoc.ts +3 -42
  259. package/src/lib/ui/runtime/createEffectNode.ts +9 -0
  260. package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
  261. package/src/lib/ui/runtime/flushEffects.ts +16 -6
  262. package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
  263. package/src/lib/ui/runtime/nextBlockId.ts +8 -7
  264. package/src/lib/ui/runtime/renderPath.ts +16 -0
  265. package/src/lib/ui/runtime/types/Boundary.ts +9 -0
  266. package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
  267. package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
  268. package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
  269. package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
  270. package/src/lib/ui/runtime/withPath.ts +16 -0
  271. package/src/lib/ui/runtime/withPathFrom.ts +20 -0
  272. package/src/lib/ui/seedStreamedResolution.ts +31 -6
  273. package/src/lib/ui/settleAsyncCells.ts +24 -0
  274. package/src/lib/ui/socketProxy.ts +1 -1
  275. package/src/lib/ui/startClient.ts +16 -31
  276. package/src/lib/ui/trackedComputed.ts +68 -0
  277. package/src/lib/ui/types/Scope.ts +8 -24
  278. package/src/lib/ui/watch.ts +3 -3
  279. package/src/serverEntry.ts +14 -0
  280. package/template/src/server/rpc/getHello.ts +15 -13
  281. package/template/test/app.test.ts +1 -1
  282. package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
  283. package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
  284. package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
  285. package/src/lib/shared/hasReplayableRequest.ts +0 -17
  286. package/src/lib/shared/hydratingSlot.ts +0 -12
  287. package/src/lib/shared/outboxProbeSlot.ts +0 -20
  288. package/src/lib/shared/types/Outbox.ts +0 -9
  289. package/src/lib/shared/types/OutboxEntry.ts +0 -27
  290. package/src/lib/shared/types/SmartReadOptions.ts +0 -36
  291. package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
  292. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
  293. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
  294. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
  295. package/src/lib/ui/compile/markupTokens.ts +0 -313
  296. package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
  297. package/src/lib/ui/dom/applyResolved.ts +0 -87
  298. package/src/lib/ui/dom/mutateDocArray.ts +0 -38
  299. package/src/lib/ui/dom/scopeLabel.ts +0 -21
  300. package/src/lib/ui/dom/text.ts +0 -20
  301. package/src/lib/ui/history.ts +0 -108
  302. package/src/lib/ui/installHotBridge.ts +0 -95
  303. package/src/lib/ui/installInspectorBridge.ts +0 -140
  304. package/src/lib/ui/outbox.ts +0 -35
  305. package/src/lib/ui/persist.ts +0 -115
  306. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
  307. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
  308. package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
  309. package/src/lib/ui/runtime/captureModelDoc.ts +0 -43
  310. package/src/lib/ui/runtime/hotInstances.ts +0 -10
  311. package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
  312. package/src/lib/ui/runtime/hotReplace.ts +0 -38
  313. package/src/lib/ui/runtime/liveScopes.ts +0 -15
  314. package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
  315. package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
  316. package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
  317. package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
  318. package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
  319. package/src/lib/ui/seedResolved.ts +0 -28
  320. package/src/lib/ui/sync.ts +0 -48
  321. package/src/lib/ui/types/History.ts +0 -14
  322. package/src/lib/ui/types/PersistHandle.ts +0 -11
  323. package/src/lib/ui/types/PersistenceStore.ts +0 -12
  324. package/src/lib/ui/types/ResolvedFrame.ts +0 -15
  325. package/src/lib/ui/types/SyncTransport.ts +0 -13
@@ -1,313 +0,0 @@
1
- import type { SemanticToken } from './types/SemanticToken.ts'
2
-
3
- /*
4
- Lexical highlighting for the HTML-shaped markup of a `.abide` component — element
5
- and component tag names, attribute names and string values, comments, and the
6
- `<`/`>`/`=` punctuation. A pure scan of raw source (independent of a successful
7
- parse, so it survives mid-edit), it exists so the LSP owns the *structure* too,
8
- not just `{…}` interiors: editors reuse tree-sitter-html as the grammar of record,
9
- which has no production for abide's `attr={expr}` values — a multiline
10
- template-literal attribute (`code={`…`}`) drops it into error recovery that never
11
- re-syncs, miscoloring every element below. Emitting these tokens overrides that
12
- broken parse wherever real structure sits.
13
-
14
- `{…}` expression regions (attribute values, interpolations, `{#…}` block heads) and
15
- the raw bodies of `<script>`/`<style>` are skipped here — those are the shadow
16
- type-checker's and the structural block tokenizer's jobs. Lowercase tags get the
17
- `tag` type, uppercase (components) get `type` (a constructor), mirroring the
18
- tree-sitter highlight split.
19
- */
20
-
21
- /* Raw-text elements whose body is JS/CSS, not markup, so the scan jumps past it. */
22
- const RAW_ELEMENTS = new Set(['script', 'style'])
23
-
24
- const operator = (start: number, length: number): SemanticToken => ({
25
- start,
26
- length,
27
- type: 'operator',
28
- modifiers: [],
29
- })
30
-
31
- const isNameStart = (char: string): boolean => /[A-Za-z]/.test(char)
32
-
33
- /* Characters that end a tag or attribute name. */
34
- const isNameBreak = (char: string): boolean => /[\s=/>{}"'<]/.test(char)
35
-
36
- export function markupTokens(source: string): SemanticToken[] {
37
- const tokens: SemanticToken[] = []
38
- let cursor = 0
39
- while (cursor < source.length) {
40
- const char = source.charAt(cursor)
41
- /* An expression region — interiors are the shadow classifier's job. */
42
- if (char === '{') {
43
- cursor = skipExpression(source, cursor)
44
- continue
45
- }
46
- if (char === '<') {
47
- if (source.startsWith('<!--', cursor)) {
48
- const end = source.indexOf('-->', cursor + 4)
49
- const close = end === -1 ? source.length : end + 3
50
- tokens.push({
51
- start: cursor,
52
- length: close - cursor,
53
- type: 'comment',
54
- modifiers: [],
55
- })
56
- cursor = close
57
- continue
58
- }
59
- const isClose = source.charAt(cursor + 1) === '/'
60
- const nameStart = cursor + (isClose ? 2 : 1)
61
- if (isNameStart(source.charAt(nameStart))) {
62
- tokens.push(operator(cursor, isClose ? 2 : 1))
63
- const name = readName(source, nameStart)
64
- const type = /[A-Z]/.test(name.charAt(0)) ? 'type' : 'tag'
65
- tokens.push({ start: nameStart, length: name.length, type, modifiers: [] })
66
- const afterName = nameStart + name.length
67
- if (isClose) {
68
- cursor = scanToClose(source, afterName, tokens)
69
- continue
70
- }
71
- const { end, selfClosed } = scanAttributes(source, afterName, tokens)
72
- cursor =
73
- RAW_ELEMENTS.has(name) && !selfClosed ? skipRawBody(source, end, name) : end
74
- continue
75
- }
76
- }
77
- cursor += 1
78
- }
79
- return tokens
80
- }
81
-
82
- /* Reads a tag/element name from `start` (letters, digits, `-`, `_`). */
83
- function readName(source: string, start: number): string {
84
- let cursor = start
85
- while (cursor < source.length && /[A-Za-z0-9_-]/.test(source.charAt(cursor))) {
86
- cursor += 1
87
- }
88
- return source.slice(start, cursor)
89
- }
90
-
91
- /* From after a close tag's name, emits the `>` operator and returns past it. */
92
- function scanToClose(source: string, from: number, tokens: SemanticToken[]): number {
93
- let cursor = from
94
- while (cursor < source.length && source.charAt(cursor) !== '>') {
95
- cursor += 1
96
- }
97
- if (cursor < source.length) {
98
- tokens.push(operator(cursor, 1))
99
- return cursor + 1
100
- }
101
- return cursor
102
- }
103
-
104
- /* Scans an open tag's attributes from after its name to the closing `>`/`/>`,
105
- emitting an `attribute` token per name, `operator` for `=`, and string tokens
106
- for quoted values; `{…}` values and spreads are left to the shadow. */
107
- function scanAttributes(
108
- source: string,
109
- from: number,
110
- tokens: SemanticToken[],
111
- ): { end: number; selfClosed: boolean } {
112
- let cursor = from
113
- while (cursor < source.length) {
114
- const char = source.charAt(cursor)
115
- if (/\s/.test(char)) {
116
- cursor += 1
117
- continue
118
- }
119
- if (char === '>') {
120
- tokens.push(operator(cursor, 1))
121
- return { end: cursor + 1, selfClosed: false }
122
- }
123
- if (char === '/' && source.charAt(cursor + 1) === '>') {
124
- tokens.push(operator(cursor, 2))
125
- return { end: cursor + 2, selfClosed: true }
126
- }
127
- /* `{…code}` spread / expression attribute — shadow's job. */
128
- if (char === '{') {
129
- cursor = skipExpression(source, cursor)
130
- continue
131
- }
132
- const nameStart = cursor
133
- while (cursor < source.length && !isNameBreak(source.charAt(cursor))) {
134
- cursor += 1
135
- }
136
- if (cursor === nameStart) {
137
- /* No progress on a stray delimiter — advance to avoid a stall. */
138
- cursor += 1
139
- continue
140
- }
141
- tokens.push({
142
- start: nameStart,
143
- length: cursor - nameStart,
144
- type: 'attribute',
145
- modifiers: [],
146
- })
147
- let eq = cursor
148
- while (eq < source.length && /\s/.test(source.charAt(eq))) {
149
- eq += 1
150
- }
151
- if (source.charAt(eq) === '=') {
152
- tokens.push(operator(eq, 1))
153
- eq += 1
154
- while (eq < source.length && /\s/.test(source.charAt(eq))) {
155
- eq += 1
156
- }
157
- cursor = scanAttributeValue(source, eq, tokens)
158
- } else {
159
- cursor = eq
160
- }
161
- }
162
- return { end: cursor, selfClosed: false }
163
- }
164
-
165
- /* Scans an attribute value: a `{…}` expression (skipped), a quoted string (split
166
- around any `{…}` interpolations), or a bare unquoted run. Returns past it. */
167
- function scanAttributeValue(source: string, from: number, tokens: SemanticToken[]): number {
168
- const char = source.charAt(from)
169
- if (char === '{') {
170
- return skipExpression(source, from)
171
- }
172
- if (char === '"' || char === "'") {
173
- return scanQuotedValue(source, from, tokens)
174
- }
175
- let cursor = from
176
- /* Unquoted values run to whitespace or `>`; a `/` is part of the value (a URL)
177
- UNLESS it's the self-close `/>`, so stop before that `/` and leave it for the
178
- attribute scanner to read as the self-close marker. */
179
- while (cursor < source.length) {
180
- const valueChar = source.charAt(cursor)
181
- if (/[\s>]/.test(valueChar) || (valueChar === '/' && source.charAt(cursor + 1) === '>')) {
182
- break
183
- }
184
- cursor += 1
185
- }
186
- if (cursor > from) {
187
- tokens.push({ start: from, length: cursor - from, type: 'string', modifiers: [] })
188
- }
189
- return cursor
190
- }
191
-
192
- /* Emits string tokens for the literal runs of a quoted value (delimiters
193
- included), skipping `{…}` interpolations so the shadow colors their interiors. */
194
- function scanQuotedValue(source: string, start: number, tokens: SemanticToken[]): number {
195
- const quote = source.charAt(start)
196
- let segmentStart = start
197
- let cursor = start + 1
198
- const flush = (end: number): void => {
199
- if (end > segmentStart) {
200
- tokens.push({
201
- start: segmentStart,
202
- length: end - segmentStart,
203
- type: 'string',
204
- modifiers: [],
205
- })
206
- }
207
- }
208
- while (cursor < source.length) {
209
- const char = source.charAt(cursor)
210
- if (char === quote) {
211
- flush(cursor + 1)
212
- return cursor + 1
213
- }
214
- if (char === '{') {
215
- flush(cursor)
216
- cursor = skipExpression(source, cursor)
217
- segmentStart = cursor
218
- continue
219
- }
220
- cursor += 1
221
- }
222
- flush(cursor)
223
- return cursor
224
- }
225
-
226
- /* From the close `>` of a raw-text element's open tag, returns the index of its
227
- `</tag` so the close tag is scanned as markup but the body is left untouched. */
228
- function skipRawBody(source: string, from: number, tag: string): number {
229
- const close = source.indexOf(`</${tag}`, from)
230
- return close === -1 ? source.length : close
231
- }
232
-
233
- /* From the opening `{` at `start`, returns the index past the matching `}`,
234
- tracking nested braces and skipping strings, template literals (with `${…}`),
235
- and JS comments so their contents never sway the depth. A regex literal holding
236
- an unbalanced brace is the one unhandled case — rare, accepted for a highlighter. */
237
- function skipExpression(source: string, start: number): number {
238
- let depth = 0
239
- let cursor = start
240
- while (cursor < source.length) {
241
- const char = source.charAt(cursor)
242
- if (char === '"' || char === "'") {
243
- cursor = skipString(source, cursor)
244
- continue
245
- }
246
- if (char === '`') {
247
- cursor = skipTemplate(source, cursor)
248
- continue
249
- }
250
- if (char === '/' && source.charAt(cursor + 1) === '/') {
251
- const newline = source.indexOf('\n', cursor)
252
- cursor = newline === -1 ? source.length : newline
253
- continue
254
- }
255
- if (char === '/' && source.charAt(cursor + 1) === '*') {
256
- const end = source.indexOf('*/', cursor + 2)
257
- cursor = end === -1 ? source.length : end + 2
258
- continue
259
- }
260
- if (char === '{') {
261
- depth += 1
262
- } else if (char === '}') {
263
- depth -= 1
264
- if (depth === 0) {
265
- return cursor + 1
266
- }
267
- }
268
- cursor += 1
269
- }
270
- return cursor
271
- }
272
-
273
- /* From the opening quote at `start`, returns the index past the matching quote;
274
- a backslash escapes the next character. */
275
- function skipString(source: string, start: number): number {
276
- const quote = source.charAt(start)
277
- let cursor = start + 1
278
- while (cursor < source.length) {
279
- const char = source.charAt(cursor)
280
- if (char === '\\') {
281
- cursor += 2
282
- continue
283
- }
284
- if (char === quote) {
285
- return cursor + 1
286
- }
287
- cursor += 1
288
- }
289
- return cursor
290
- }
291
-
292
- /* From the opening backtick at `start`, returns the index past the matching
293
- backtick. `${…}` re-enters expression scanning so its inner braces balance;
294
- braces in the literal text are ignored. */
295
- function skipTemplate(source: string, start: number): number {
296
- let cursor = start + 1
297
- while (cursor < source.length) {
298
- const char = source.charAt(cursor)
299
- if (char === '\\') {
300
- cursor += 2
301
- continue
302
- }
303
- if (char === '`') {
304
- return cursor + 1
305
- }
306
- if (char === '$' && source.charAt(cursor + 1) === '{') {
307
- cursor = skipExpression(source, cursor + 1)
308
- continue
309
- }
310
- cursor += 1
311
- }
312
- return cursor
313
- }
@@ -1,100 +0,0 @@
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 bare `{expr}` interpolations are NOT touched here — those are the shadow's job.
10
- A keyword allowlist after the sigil prevents matching arbitrary `{:foo}` text.
11
- Longest phrases first so `for await` beats `for` and `else if` beats `else`.
12
- */
13
- const BLOCK_KEYWORDS = [
14
- 'for await',
15
- 'else if',
16
- 'if',
17
- 'for',
18
- 'await',
19
- 'switch',
20
- 'case',
21
- 'default',
22
- 'try',
23
- 'catch',
24
- 'finally',
25
- 'then',
26
- 'else',
27
- ]
28
-
29
- const BLOCK_HEAD = new RegExp(`\\{([#:/])\\s*(${BLOCK_KEYWORDS.join('|')})\\b`, 'g')
30
-
31
- export function structuralBlockTokens(source: string): SemanticToken[] {
32
- const tokens: SemanticToken[] = []
33
- for (const match of source.matchAll(BLOCK_HEAD)) {
34
- const braceStart = match.index
35
- const sigil = match[1]
36
- const keyword = match[2]
37
- if (keyword === undefined) {
38
- continue
39
- }
40
- const keywordStart = braceStart + match[0].length - keyword.length
41
- tokens.push({ start: braceStart, length: 2, type: 'operator', modifiers: [] })
42
- tokens.push({ start: keywordStart, length: keyword.length, type: 'keyword', modifiers: [] })
43
- /* A `{#for …}` head carries abide-only connectors `of`/`by` that the shadow
44
- lowers away (`of`) or never emits (`by`), so color them here. */
45
- if (sigil === '#' && (keyword === 'for' || keyword === 'for await')) {
46
- tokens.push(...forHeadConnectors(source, keywordStart + keyword.length))
47
- }
48
- }
49
- return tokens
50
- }
51
-
52
- /*
53
- Scans a `{#for …}` head from `from` to its closing `}`, emitting `keyword` tokens
54
- for the `of`/`by` connectors at brace depth 0 — skipping any `of`/`by` nested in a
55
- destructure, call, or string so an identifier or object key is never miscolored.
56
- */
57
- function forHeadConnectors(source: string, from: number): SemanticToken[] {
58
- const tokens: SemanticToken[] = []
59
- let depth = 0
60
- let cursor = from
61
- while (cursor < source.length) {
62
- const char = source.charAt(cursor)
63
- if (char === '"' || char === "'" || char === '`') {
64
- cursor += 1
65
- while (cursor < source.length && source.charAt(cursor) !== char) {
66
- if (source.charAt(cursor) === '\\') {
67
- cursor += 1
68
- }
69
- cursor += 1
70
- }
71
- } else if (char === '{' || char === '(' || char === '[') {
72
- depth += 1
73
- } else if (char === ')' || char === ']') {
74
- depth -= 1
75
- } else if (char === '}') {
76
- if (depth === 0) {
77
- break
78
- }
79
- depth -= 1
80
- } else if (depth === 0 && (char === 'o' || char === 'b')) {
81
- const word = source.startsWith('of', cursor)
82
- ? 'of'
83
- : source.startsWith('by', cursor)
84
- ? 'by'
85
- : undefined
86
- const isWordBoundary = (offset: number): boolean => /\s/.test(source.charAt(offset))
87
- if (
88
- word !== undefined &&
89
- isWordBoundary(cursor - 1) &&
90
- isWordBoundary(cursor + word.length)
91
- ) {
92
- tokens.push({ start: cursor, length: word.length, type: 'keyword', modifiers: [] })
93
- cursor += word.length
94
- continue
95
- }
96
- }
97
- cursor += 1
98
- }
99
- return tokens
100
- }
@@ -1,87 +0,0 @@
1
- import { seedResolved } from '../seedResolved.ts'
2
- import { isComment } from './isComment.ts'
3
-
4
- /*
5
- Bundle-side consumer of an SSR stream chunk, the counterpart of the doc stream's inline
6
- vanilla scripts (`SSR_SWAP_SCRIPT`'s `__abideSwap` and `CACHE_RESOLVE_SCRIPT`'s `__abideResolve`) for a stream the
7
- running bundle consumes itself — streaming SPA navigation, socket-delivered SSR. It routes
8
- the two chunk kinds the stream interleaves:
9
-
10
- - `<abide-cache>{StreamedResolution}</abide-cache>` → seed the streamed cache partition.
11
- A script set via innerHTML never runs, so the cache channel rides a data frame here
12
- (not the doc stream's `<script>__abideResolve(…)</script>`); seeding it keeps the
13
- pending {#await} read warm instead of cold-missing to the network.
14
- - `<abide-resolve data-id="ID"><script type="application/json">…</script>…</abide-resolve>`
15
- → register the value in the resume manifest (for hydration), find the matching
16
- `<!--abide:await:ID-->…<!--/abide:await:ID-->` boundary in `root`, remove the pending
17
- nodes between the markers, and insert the resolved content in their place.
18
-
19
- The pending shell painted instantly; this swaps in each value as it arrives — completing
20
- the out-of-order streaming loop on the client.
21
- */
22
- // @documentation plumbing
23
- export function applyResolved(root: Element, frame: string): void {
24
- const holder = document.createElement('div')
25
- holder.innerHTML = frame
26
- const resolved = holder.firstChild as Element | null
27
- if (resolved === null || resolved.getAttribute === undefined) {
28
- return
29
- }
30
- /* A cache-seed frame warms the streamed cache partition — paired with the DOM swap so a
31
- bundle-consumed stream can't adopt a resolved branch while dropping its cache key. */
32
- if (resolved.nodeName === 'ABIDE-CACHE') {
33
- try {
34
- seedResolved({ kind: 'cache', resolution: JSON.parse(resolved.textContent ?? 'null') })
35
- } catch {
36
- /* malformed payload — leave unseeded; the read falls back to a live fetch */
37
- }
38
- return
39
- }
40
- const id = resolved.getAttribute('data-id')
41
- if (id === null) {
42
- return
43
- }
44
- /* The resolved value rides in a leading <script type=application/json> as ref-json
45
- text; store it raw and remove the node so only the resolved markup moves into the
46
- boundary. Decoding is deferred to the read in `awaitBlock` (which has the codec);
47
- recording it lets a later hydrate adopt this branch (no re-fetch). */
48
- const payload = resolved.firstChild as Element | null
49
- if (payload !== null && payload.nodeName === 'SCRIPT') {
50
- seedResolved({ kind: 'resume', id: Number(id), resume: payload.textContent ?? '' })
51
- payload.remove()
52
- }
53
- const open = `abide:await:${id}`
54
- const close = `/abide:await:${id}`
55
- const boundary = findBoundary(root, open)
56
- if (boundary === undefined) {
57
- return
58
- }
59
- const { parent, start } = boundary
60
- /* Remove the pending nodes between the markers (exclusive of the markers). */
61
- let node = start.nextSibling
62
- while (node !== null && !isComment(node, close)) {
63
- const next = node.nextSibling
64
- parent.removeChild(node)
65
- node = next
66
- }
67
- /* Insert resolved content after the open marker. */
68
- let anchor = start.nextSibling
69
- for (const child of [...resolved.childNodes]) {
70
- parent.insertBefore(child, anchor)
71
- anchor = child.nextSibling
72
- }
73
- }
74
-
75
- /* Depth-first search for the parent + open-marker comment of a boundary. */
76
- function findBoundary(node: Node, open: string): { parent: Node; start: Node } | undefined {
77
- for (const child of [...node.childNodes]) {
78
- if (isComment(child, open)) {
79
- return { parent: node, start: child }
80
- }
81
- const nested = findBoundary(child, open)
82
- if (nested !== undefined) {
83
- return nested
84
- }
85
- }
86
- return undefined
87
- }
@@ -1,38 +0,0 @@
1
- import type { Doc } from '../runtime/types/Doc.ts'
2
- import { readCall } from './readCall.ts'
3
-
4
- /*
5
- An in-place-mutating array method lowered on a reactive-document array. The
6
- doc-access lowering rewrites e.g. `model.items.splice(i, 1)` to
7
- `$$mutateDocArray(model, "items", "splice", [i, 1])` rather than the bare
8
- `$$readCall(model.read("items"), …)` it emits for non-mutating calls. The reason:
9
- `read` returns the live tree array by reference, so a bare `.splice()`/`.sort()`/…
10
- would mutate the document in place while never emitting a patch — no reader would
11
- re-render and undo/persistence/multiplayer sync (all keyed off `apply`) would
12
- never see the change (`.push` is the one already-handled exception, lowered to
13
- `add` patches). Cloning the array, applying the mutation to the copy, and writing
14
- it back through `replace` emits a real patch: readers wake and the change is
15
- journalled. Element identity is preserved for untouched entries (a shallow
16
- `slice`), so a keyed `{#for}` reconciles instead of rebuilding. The native
17
- method's return value (spliced-out elements, the new length, …) is returned
18
- unchanged.
19
- */
20
- // @documentation plumbing
21
- export function mutateDocArray(doc: Doc, path: string, member: string, args: unknown[]): unknown {
22
- const current = doc.read<unknown>(path)
23
- /* Not an array — nothing patchable to clone. Fall back to the guarded in-place
24
- call so the author still gets readCall's authored-scope error message. */
25
- if (!Array.isArray(current)) {
26
- return readCall(current, path, member, args)
27
- }
28
- const copy = current.slice()
29
- const method = (copy as unknown as Record<string, unknown>)[member]
30
- if (typeof method !== 'function') {
31
- throw new TypeError(
32
- `abide: cannot call .${member}() — "${path}".${member} is not a function (got ${typeof method})`,
33
- )
34
- }
35
- const returned = (method as (...callArgs: unknown[]) => unknown).apply(copy, args)
36
- doc.replace(path, copy)
37
- return returned
38
- }
@@ -1,21 +0,0 @@
1
- /*
2
- The human-readable name for the scope the direct-mount entry (`mount`/`hydrate`)
3
- establishes, derived from its host element's tag. The router instead fills layers into
4
- marker boundaries with no host element, so it passes each layer's route key as an
5
- explicit label (`fillBoundary`); a nested child likewise passes its component name (see
6
- `mountRange`). The `abide-` prefix strip turns a framework host tag like `abide-resolve`
7
- into `resolve`; any other host yields its lowercased tag. Feeds the inspector's
8
- Reactive tab a readable scope name; computed and stored in all builds (only the
9
- inspector surface is dev-only). Returns undefined when there's no element to name from.
10
- */
11
- import { COMPONENT_WRAPPER_PREFIX } from '../COMPONENT_WRAPPER_PREFIX.ts'
12
-
13
- export function scopeLabel(host: Element): string | undefined {
14
- const tag = host.tagName?.toLowerCase()
15
- if (tag === undefined) {
16
- return undefined
17
- }
18
- return tag.startsWith(COMPONENT_WRAPPER_PREFIX)
19
- ? tag.slice(COMPONENT_WRAPPER_PREFIX.length)
20
- : tag
21
- }
@@ -1,20 +0,0 @@
1
- import { effect } from '../effect.ts'
2
-
3
- /*
4
- A text node whose content tracks `read()`. The effect captures whatever reactive
5
- cells `read` touches, so the node — and only this node — re-renders when they
6
- change: one text node, one effect — fine-grained granularity.
7
-
8
- Plain-text only — `read()` is coerced with String(). For a `{expr}` interpolation
9
- that may yield an `html\`…\`` or snippet value the compiler emits `appendText`,
10
- which branches on the branded value and inserts raw markup; `text` cannot host
11
- multiple nodes and would stringify such a value to `[object Object]`.
12
- */
13
- // @documentation plumbing
14
- export function text(read: () => unknown): Text {
15
- const node = document.createTextNode('')
16
- effect(() => {
17
- node.data = String(read())
18
- })
19
- return node
20
- }
@@ -1,108 +0,0 @@
1
- import { PATCH_BUS } from './runtime/PATCH_BUS.ts'
2
- import type { Doc } from './runtime/types/Doc.ts'
3
- import type { Patch } from './runtime/types/Patch.ts'
4
- import type { History } from './types/History.ts'
5
-
6
- /*
7
- Undo/redo for a document. Subscribes the `PATCH_BUS`, filters to this `doc`, and
8
- journals each applied patch's `inverse` into a bounded stack — O(size of the
9
- change) per step, never a tree snapshot. `undo`/`redo` replay inverses through
10
- `doc.apply`; the patches that replay itself emits are routed into a `sink` so a
11
- replay never opens a fresh entry nor clears the opposite stack (it instead fills
12
- it, keeping the move reversible). A genuine new edit clears the redo stack.
13
- `transaction(fn)` groups every patch `fn` emits into one entry. `limit` caps
14
- depth — the oldest entry drops. Inert (but harmless) on the server.
15
- */
16
- // @documentation plumbing
17
- export function history(doc: Doc, { limit = 100 }: { limit?: number } = {}): History {
18
- const undoStack: Patch[][] = []
19
- const redoStack: Patch[][] = []
20
- /* When set, captured inverses accumulate here instead of opening a new undo entry
21
- — held by an in-progress transaction and by an undo/redo replay. */
22
- let sink: Patch[] | undefined
23
-
24
- /* Push one finished entry, evicting the oldest past `limit`. */
25
- const push = (stack: Patch[][], entry: Patch[]): void => {
26
- stack.push(entry)
27
- if (stack.length > limit) {
28
- stack.shift()
29
- }
30
- }
31
-
32
- const unsubscribe = PATCH_BUS.subscribe((event) => {
33
- if (event.doc !== doc || event.inverse === undefined) {
34
- return
35
- }
36
- if (sink !== undefined) {
37
- sink.push(event.inverse)
38
- return
39
- }
40
- push(undoStack, [event.inverse])
41
- redoStack.length = 0
42
- })
43
-
44
- /* Apply an entry's inverses in reverse application order (LIFO), capturing the
45
- inverses they emit — the original forward patches — onto `target`, so the move
46
- is itself reversible and lands on the opposite stack. */
47
- const replay = (entry: Patch[], target: Patch[][]): void => {
48
- const captured: Patch[] = []
49
- sink = captured
50
- try {
51
- for (let index = entry.length - 1; index >= 0; index -= 1) {
52
- doc.apply(entry[index] as Patch)
53
- }
54
- } finally {
55
- sink = undefined
56
- /* Push captured inverses even if `apply` threw mid-loop: the already-popped
57
- entry is gone, so the forward patches for what DID apply must land on the
58
- opposite stack or a partial replay becomes irreversible. */
59
- if (captured.length > 0) {
60
- push(target, captured)
61
- }
62
- }
63
- }
64
-
65
- return {
66
- undo: () => {
67
- const entry = undoStack.pop()
68
- if (entry !== undefined) {
69
- replay(entry, redoStack)
70
- }
71
- },
72
- redo: () => {
73
- const entry = redoStack.pop()
74
- if (entry !== undefined) {
75
- replay(entry, undoStack)
76
- }
77
- },
78
- canUndo: () => undoStack.length > 0,
79
- canRedo: () => redoStack.length > 0,
80
- transaction: (run: () => void) => {
81
- /* A nested transaction folds into the one already collecting. */
82
- if (sink !== undefined) {
83
- run()
84
- return
85
- }
86
- const entry: Patch[] = []
87
- sink = entry
88
- try {
89
- run()
90
- } finally {
91
- sink = undefined
92
- /* Commit whatever was applied even if `run` threw partway: those patches are
93
- already live in the doc, so their inverses must reach the undo stack or the
94
- committed changes are left with no way to undo them. The throw still
95
- propagates after this. */
96
- if (entry.length > 0) {
97
- push(undoStack, entry)
98
- redoStack.length = 0
99
- }
100
- }
101
- },
102
- dispose: () => {
103
- unsubscribe()
104
- undoStack.length = 0
105
- redoStack.length = 0
106
- },
107
- }
108
- }