@kabmo-labs/kabmo 0.7.0 → 0.9.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 (351) hide show
  1. package/README.md +7 -0
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/index.js +403 -384
  4. package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
  5. package/dist/client/static/js/index-k315p1e8.css +1 -0
  6. package/package.json +10 -1
  7. package/src/client/components/common/code-surface/documents.test.ts +120 -0
  8. package/src/client/components/common/code-surface/documents.ts +201 -0
  9. package/src/client/components/common/code-surface/extensions.test.ts +110 -0
  10. package/src/client/components/common/code-surface/extensions.ts +162 -0
  11. package/src/client/components/common/code-surface/handle.ts +102 -0
  12. package/src/client/components/common/code-surface/index.tsx +253 -314
  13. package/src/client/components/common/code-surface/keymap.test.ts +133 -0
  14. package/src/client/components/common/code-surface/keymap.ts +267 -0
  15. package/src/client/components/common/code-surface/marks.ts +74 -0
  16. package/src/client/components/common/code-surface/shiki.ts +234 -0
  17. package/src/client/components/common/code-surface/sync.test.ts +185 -0
  18. package/src/client/components/common/code-surface/sync.ts +58 -0
  19. package/src/client/components/common/code-surface/theme.test.ts +202 -0
  20. package/src/client/components/common/code-surface/theme.ts +111 -0
  21. package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
  22. package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
  23. package/src/client/components/common/header-controls/index.tsx +24 -1
  24. package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
  25. package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
  26. package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
  27. package/src/client/components/common/shell-update-button/index.tsx +125 -0
  28. package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
  29. package/src/client/components/common/toolbar-row/index.tsx +189 -0
  30. package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
  31. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
  32. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
  33. package/src/client/components/layout/desktop-layout/index.tsx +55 -69
  34. package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
  35. package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
  36. package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
  37. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
  38. package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
  39. package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
  40. package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
  41. package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
  42. package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
  43. package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
  44. package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
  45. package/src/client/components/mobile/RightSidebar.tsx +21 -10
  46. package/src/client/components/mobile/SessionSidebar.tsx +8 -0
  47. package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
  48. package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
  49. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
  50. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
  51. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
  52. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
  53. package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
  54. package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
  55. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
  56. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
  57. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
  58. package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
  59. package/src/client/components/mobile/worktree-session.ts +51 -0
  60. package/src/client/components/settings/Modal.tsx +8 -0
  61. package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
  62. package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
  63. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
  64. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
  65. package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
  66. package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
  67. package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
  68. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
  69. package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
  70. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
  71. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
  72. package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
  73. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
  74. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
  75. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
  76. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
  77. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
  78. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
  79. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
  80. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
  81. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
  82. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
  83. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
  84. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
  85. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
  86. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
  87. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
  88. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
  89. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
  90. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
  91. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
  92. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
  93. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
  94. package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
  95. package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
  96. package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
  97. package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
  98. package/src/client/components/workspace/diff-panel/index.tsx +76 -145
  99. package/src/client/components/workspace/diff-panel/merge.ts +315 -0
  100. package/src/client/components/workspace/editor/Body.tsx +4 -7
  101. package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
  102. package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
  103. package/src/client/components/workspace/editor/FilePane.tsx +4 -7
  104. package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
  105. package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
  106. package/src/client/components/workspace/editor/index.tsx +7 -9
  107. package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
  108. package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
  109. package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
  110. package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
  111. package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
  112. package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
  113. package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
  114. package/src/client/components/workspace/file-explorer/filter.ts +47 -0
  115. package/src/client/components/workspace/file-explorer/index.tsx +77 -64
  116. package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
  117. package/src/client/components/workspace/search-panel/Body.tsx +1 -1
  118. package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
  119. package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
  120. package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
  121. package/src/client/hooks/chat/omp/reconnect.ts +187 -0
  122. package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
  123. package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
  124. package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
  125. package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
  126. package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
  127. package/src/client/hooks/chat/omp/session-list.tsx +69 -9
  128. package/src/client/hooks/chat/timeline/generating.ts +19 -2
  129. package/src/client/hooks/chat/timeline/index.ts +27 -28
  130. package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
  131. package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
  132. package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
  133. package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
  134. package/src/client/hooks/chat/timeline/send.test.ts +2 -0
  135. package/src/client/hooks/chat/timeline/send.ts +11 -1
  136. package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
  137. package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
  138. package/src/client/hooks/chat/timeline/session-load.ts +69 -37
  139. package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
  140. package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
  141. package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
  142. package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
  143. package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
  144. package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
  145. package/src/client/hooks/code/deferred-highlight.ts +129 -0
  146. package/src/client/hooks/editor/find-state.ts +1 -1
  147. package/src/client/hooks/editor/theme.test.ts +206 -0
  148. package/src/client/hooks/editor/theme.ts +82 -0
  149. package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
  150. package/src/client/hooks/editor/use-editor-drop.ts +67 -14
  151. package/src/client/hooks/editor/use-editor-find.ts +7 -7
  152. package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
  153. package/src/client/hooks/editor/use-outline-view.ts +94 -7
  154. package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
  155. package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
  156. package/src/client/hooks/editor/use-search-reveal.ts +1 -1
  157. package/src/client/hooks/ui/long-press.test.ts +174 -0
  158. package/src/client/hooks/ui/long-press.ts +112 -0
  159. package/src/client/hooks/ui/settings-modal.ts +56 -0
  160. package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
  161. package/src/client/hooks/ui/updates/shell-download.ts +153 -28
  162. package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
  163. package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
  164. package/src/client/hooks/ui/updates/shell-update.ts +119 -0
  165. package/src/client/hooks/workspace/diff-file.ts +29 -10
  166. package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
  167. package/src/client/hooks/workspace/editor-view.ts +61 -0
  168. package/src/client/hooks/workspace/file-create.ts +41 -11
  169. package/src/client/hooks/workspace/file-listing.ts +56 -1
  170. package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
  171. package/src/client/hooks/workspace/file-tabs.ts +24 -11
  172. package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
  173. package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
  174. package/src/client/hooks/workspace/worktree/index.ts +165 -0
  175. package/src/client/lib/desktop/bridge.ts +45 -0
  176. package/src/client/lib/search-reveal.ts +1 -1
  177. package/src/client/main.tsx +5 -1
  178. package/src/client/tailwind.css +56 -8
  179. package/src/server/index.ts +7 -2
  180. package/src/server/lib/auth/guard.test.ts +28 -0
  181. package/src/server/lib/auth/guard.ts +10 -0
  182. package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
  183. package/src/server/lib/db/schema.ts +4 -0
  184. package/src/server/lib/db/worktree-registry.server.ts +91 -0
  185. package/src/server/lib/fs/canonical.ts +52 -0
  186. package/src/server/lib/fs/relative-path.ts +48 -0
  187. package/src/server/lib/fs/root-worktree.test.ts +116 -0
  188. package/src/server/lib/fs/root.ts +39 -10
  189. package/src/server/lib/fs/worktree/carry.ts +134 -0
  190. package/src/server/lib/fs/worktree/create.test.ts +249 -0
  191. package/src/server/lib/fs/worktree/create.ts +262 -0
  192. package/src/server/lib/fs/worktree/errors.ts +43 -0
  193. package/src/server/lib/fs/worktree/list.test.ts +207 -0
  194. package/src/server/lib/fs/worktree/list.ts +142 -0
  195. package/src/server/lib/fs/worktree/location.test.ts +206 -0
  196. package/src/server/lib/fs/worktree/location.ts +114 -0
  197. package/src/server/lib/fs/worktree/project-id.ts +53 -0
  198. package/src/server/lib/fs/worktree/remove.test.ts +242 -0
  199. package/src/server/lib/fs/worktree/remove.ts +152 -0
  200. package/src/server/lib/fs/worktree/settings.server.ts +56 -0
  201. package/src/server/lib/fs/worktree/topology.ts +142 -0
  202. package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
  203. package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
  204. package/src/server/lib/lifecycle/port-guard.ts +39 -7
  205. package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
  206. package/src/server/lib/lifecycle/probe.test.ts +77 -164
  207. package/src/server/lib/lifecycle/probe.ts +121 -13
  208. package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
  209. package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
  210. package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
  211. package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
  212. package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
  213. package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
  214. package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
  215. package/src/server/lib/omp/session/telemetry.test.ts +33 -0
  216. package/src/server/lib/panels/local-edits.server.ts +33 -0
  217. package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
  218. package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
  219. package/src/server/lib/panels/sync.server.test.ts +125 -147
  220. package/src/server/lib/panels/sync.server.ts +153 -48
  221. package/src/server/lib/realtime/signals.server.ts +10 -0
  222. package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
  223. package/src/server/lib/realtime/topics.server.ts +16 -0
  224. package/src/server/lib/updates/shell-assets.test.ts +114 -0
  225. package/src/server/lib/updates/shell-assets.ts +200 -0
  226. package/src/server/lib/updates/shell-cache.ts +105 -0
  227. package/src/server/lib/updates/shell-pages.test.ts +187 -0
  228. package/src/server/lib/updates/shell-pages.ts +157 -0
  229. package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
  230. package/src/server/lib/updates/shell-release.test.ts +59 -71
  231. package/src/server/lib/updates/shell-release.ts +178 -198
  232. package/src/server/routes/fs/action-create.test.ts +18 -13
  233. package/src/server/routes/fs/action-nested.test.ts +184 -0
  234. package/src/server/routes/fs/action.ts +47 -16
  235. package/src/server/routes/fs/git-merge.test.ts +11 -1
  236. package/src/server/routes/fs/read.test.ts +24 -7
  237. package/src/server/routes/fs/scope.test.ts +15 -10
  238. package/src/server/routes/fs/scope.ts +3 -14
  239. package/src/server/routes/index.ts +2 -0
  240. package/src/server/routes/updates/android.ts +6 -1
  241. package/src/server/routes/updates/desktop.ts +59 -0
  242. package/src/server/routes/updates/index.ts +2 -0
  243. package/src/server/routes/updates/ios.ts +5 -1
  244. package/src/server/routes/worktrees/create.ts +138 -0
  245. package/src/server/routes/worktrees/index.ts +25 -0
  246. package/src/server/routes/worktrees/list.ts +134 -0
  247. package/src/server/routes/worktrees/remove.ts +102 -0
  248. package/src/server/routes/worktrees/routes.test.ts +326 -0
  249. package/src/server/routes/worktrees/shared.ts +90 -0
  250. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
  251. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
  252. package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
  253. package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
  254. package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
  255. package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
  256. package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
  257. package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
  258. package/src/shared/lib/code/editor/find-scan.ts +177 -0
  259. package/src/shared/lib/code/editor/keymap.ts +3 -3
  260. package/src/shared/lib/code/editor/lines.test.ts +7 -55
  261. package/src/shared/lib/code/editor/lines.ts +8 -48
  262. package/src/shared/lib/code/editor/marks.test.ts +4 -5
  263. package/src/shared/lib/code/editor/marks.ts +13 -25
  264. package/src/shared/lib/code/editor/theme-loader.ts +97 -0
  265. package/src/shared/lib/code/editor/themes.generated.ts +156 -0
  266. package/src/shared/lib/code/editor/themes.test.ts +165 -0
  267. package/src/shared/lib/code/editor/themes.ts +165 -0
  268. package/src/shared/lib/code/highlight-memo.test.ts +103 -0
  269. package/src/shared/lib/code/highlight-memo.ts +149 -0
  270. package/src/shared/lib/code/highlighter-warmup.ts +112 -0
  271. package/src/shared/lib/code/highlighter.ts +3 -1
  272. package/src/shared/lib/code/idle.ts +37 -0
  273. package/src/shared/lib/code/outline/rows.test.ts +96 -0
  274. package/src/shared/lib/code/outline/rows.ts +94 -0
  275. package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
  276. package/src/shared/lib/code/syntax-highlight.ts +2 -3
  277. package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
  278. package/src/shared/lib/fs/file-kind.ts +47 -13
  279. package/src/shared/lib/fs/file-tree.test.ts +183 -0
  280. package/src/shared/lib/fs/file-tree.ts +159 -0
  281. package/src/shared/lib/fs/file-url.test.ts +52 -0
  282. package/src/shared/lib/fs/file-url.ts +37 -0
  283. package/src/shared/lib/fs/patch-sides.test.ts +114 -0
  284. package/src/shared/lib/fs/patch-sides.ts +139 -0
  285. package/src/shared/lib/fs/paths.test.ts +24 -1
  286. package/src/shared/lib/fs/paths.ts +17 -0
  287. package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
  288. package/src/shared/lib/omp/session/prewarm.ts +7 -0
  289. package/src/shared/lib/realtime/protocol.ts +16 -0
  290. package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
  291. package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
  292. package/src/shared/lib/updates/shell-arch.ts +54 -0
  293. package/src/shared/lib/updates/shell-ua.ts +24 -4
  294. package/src/shared/lib/workspace/active-project.ts +23 -0
  295. package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
  296. package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
  297. package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
  298. package/src/shared/lib/workspace/panel-widths.ts +25 -5
  299. package/src/shared/lib/workspace/session-state/cache.ts +159 -0
  300. package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
  301. package/src/shared/lib/workspace/session-state/merge.ts +100 -0
  302. package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
  303. package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
  304. package/src/shared/lib/workspace/session-state/store.ts +92 -130
  305. package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
  306. package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
  307. package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
  308. package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
  309. package/src/shared/lib/workspace/worktree/types.ts +104 -0
  310. package/src/shared/types/fs.ts +10 -0
  311. package/src/shared/types/realtime.ts +29 -0
  312. package/src/shared/types/settings/state.ts +10 -0
  313. package/src/shared/types/workspace.ts +14 -0
  314. package/src/test-support/file-explorer-disk.test-util.ts +76 -0
  315. package/src/test-support/file-explorer.test-util.ts +71 -40
  316. package/src/test-support/loopback.ts +183 -0
  317. package/dist/client/static/js/index-nzyjf92m.css +0 -1
  318. package/src/client/components/common/code-editor/handler.test.ts +0 -274
  319. package/src/client/components/common/code-editor/handler.ts +0 -175
  320. package/src/client/components/common/code-editor/index.tsx +0 -314
  321. package/src/client/components/common/code-surface/measure.ts +0 -148
  322. package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
  323. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
  324. package/src/client/hooks/editor/scroll-container.test.ts +0 -102
  325. package/src/client/hooks/editor/scroll-container.ts +0 -26
  326. package/src/client/hooks/editor/use-code-window.ts +0 -100
  327. package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
  328. package/src/client/hooks/editor/use-editor-history.ts +0 -134
  329. package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
  330. package/src/client/hooks/editor/use-occurrences.ts +0 -98
  331. package/src/shared/lib/code/editor/commands.test.ts +0 -155
  332. package/src/shared/lib/code/editor/commands.ts +0 -260
  333. package/src/shared/lib/code/editor/edits.test.ts +0 -100
  334. package/src/shared/lib/code/editor/edits.ts +0 -122
  335. package/src/shared/lib/code/editor/history.test.ts +0 -118
  336. package/src/shared/lib/code/editor/history.ts +0 -101
  337. package/src/shared/lib/code/editor/layers.test.ts +0 -77
  338. package/src/shared/lib/code/editor/layers.ts +0 -67
  339. package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
  340. package/src/shared/lib/code/editor/line-edits.ts +0 -159
  341. package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
  342. package/src/shared/lib/code/editor/occurrences.ts +0 -111
  343. package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
  344. package/src/shared/lib/code/editor/reset-css.ts +0 -32
  345. package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
  346. package/src/shared/lib/code/editor/text-commands.ts +0 -154
  347. package/src/shared/lib/code/lazy-window.test.ts +0 -161
  348. package/src/shared/lib/code/lazy-window.ts +0 -156
  349. package/src/shared/lib/code/windowed-highlight.ts +0 -114
  350. package/src/shared/lib/fs/diff-parser.test.ts +0 -116
  351. package/src/shared/lib/fs/diff-parser.ts +0 -213
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * One `EditorState` per open document, applied with `view.setState`.
8
+ *
9
+ * This is what the tab model actually needs and what the textarea surface could
10
+ * only fake: a tab's caret, its extra cursors and its UNDO HISTORY belong to the
11
+ * tab, not to the panel. Measured in CM6: switching states and coming back
12
+ * restores all three, and an undo in one tab cannot touch another — with the
13
+ * single buffer the panel used to hold, `⌘Z` after a tab switch undid an edit in
14
+ * the file the user was no longer looking at.
15
+ *
16
+ * The cache is bounded. A session can open a dozen files and each history holds
17
+ * whole-document snapshots, so the least recently used state is dropped past
18
+ * `MAX_CACHED`. Losing a closed tab's history is the intended cost; losing an
19
+ * OPEN tab's is not, which is why the drop is LRU rather than oldest-first.
20
+ *
21
+ * `wordWrap` and `readOnly` are per-VIEW settings, so they live in compartments
22
+ * this module owns. A cached state is NOT reconfigured in place: `setState`
23
+ * installs that state's own configuration, so the caller re-applies the current
24
+ * options after every swap (`optionsEffects`). Reconfiguring in the cache too
25
+ * would be a second place the same answer is stored.
26
+ */
27
+
28
+ import { Compartment, EditorState, type Extension, type StateEffect } from '@codemirror/state';
29
+
30
+ import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
31
+ import { readOnlyExtension, surfaceExtensions, wrapExtension } from '@/client/components/common/code-surface/extensions';
32
+ import { shikiHighlight, type ShikiThemeNames } from '@/client/components/common/code-surface/shiki';
33
+
34
+ /** States kept alive; past this the least recently used one is dropped. */
35
+ const MAX_CACHED = 12;
36
+
37
+ export interface SurfaceOptions {
38
+ wordWrap: boolean;
39
+ readOnly: boolean;
40
+ /**
41
+ * The tokenizer's two theme names.
42
+ *
43
+ * Part of the live options rather than of the document key: two tabs in
44
+ * different languages are two states (their decorations are built from
45
+ * different grammars), but a THEME is one setting for every tab — a new key
46
+ * per theme would throw away every open tab's undo history on a colour change.
47
+ */
48
+ themes: ShikiThemeNames;
49
+ }
50
+
51
+ export interface DocumentCacheDeps extends SurfaceOptions {
52
+ /** Panel-owned commands the editor delegates instead of running. */
53
+ onCommand?: (command: EditorCommand) => void;
54
+ /** Every user edit, with the whole new document. */
55
+ onDocChange: (value: string) => void;
56
+ /** Every state worth remembering (an edit or a selection move). */
57
+ onState: (state: EditorState) => void;
58
+ }
59
+
60
+ export interface DocumentCache {
61
+ /** The state for `key`, created (and cached) from `value` when absent. */
62
+ stateFor(key: string, language: string, value: string): EditorState;
63
+ /** Store the live state so a switch back finds it. */
64
+ remember(key: string, state: EditorState): void;
65
+ /** Drop a closed tab's history. */
66
+ drop(key: string): void;
67
+ /**
68
+ * Effects that bring a view to the current options; empty when nothing moved.
69
+ *
70
+ * `key` names the document the effects will be dispatched to, because the
71
+ * tokenizer is per-LANGUAGE: a theme reconfigure must rebuild it from that
72
+ * document's own grammar, not from whichever one the cache saw last.
73
+ */
74
+ optionsEffects(next: SurfaceOptions, key?: string): StateEffect<unknown>[];
75
+ /**
76
+ * Effects that install the CURRENT options unconditionally.
77
+ *
78
+ * A cached state carries the configuration it was created with, and
79
+ * `view.setState` installs that configuration wholesale — so a tab opened
80
+ * while wrap was off came back unwrapped after a toggle. This is the re-apply
81
+ * a document switch owes.
82
+ */
83
+ currentEffects(key?: string): StateEffect<unknown>[];
84
+ }
85
+
86
+ /** Whether two theme pairs name the same Shiki themes. */
87
+ function sameThemes(a: ShikiThemeNames | undefined, b: ShikiThemeNames): boolean {
88
+ return a !== undefined && a.light === b.light && a.dark === b.dark;
89
+ }
90
+
91
+ export function createDocumentCache(deps: DocumentCacheDeps): DocumentCache {
92
+ const states = new Map<string, EditorState>();
93
+ const options: SurfaceOptions = {
94
+ wordWrap: deps.wordWrap,
95
+ readOnly: deps.readOnly,
96
+ themes: deps.themes,
97
+ };
98
+ const wordWrapCompartment = new Compartment();
99
+ const readOnlyCompartment = new Compartment();
100
+ const themeCompartment = new Compartment();
101
+ /** The language each cached state was built for, so a theme reconfigure can
102
+ * rebuild its tokenizer with the SAME grammar. */
103
+ const languages = new Map<string, string>();
104
+ /**
105
+ * The theme pair each cached state's tokenizer currently holds.
106
+ *
107
+ * Tracked because a theme reconfigure REBUILDS the decoration plugin, which
108
+ * re-tokenizes the viewport. `currentEffects` runs on every tab switch, and
109
+ * paying that for a state that already has the right theme is work for
110
+ * nothing — 2.1 ms on a 20 000-line file, every switch.
111
+ */
112
+ const themesByKey = new Map<string, ShikiThemeNames>();
113
+
114
+ const extensionsFor = (language: string): Extension[] =>
115
+ surfaceExtensions({
116
+ language,
117
+ wordWrapCompartment,
118
+ readOnlyCompartment,
119
+ themeCompartment,
120
+ onCommand: deps.onCommand,
121
+ onDocChange: deps.onDocChange,
122
+ onState: deps.onState,
123
+ ...options,
124
+ });
125
+
126
+ /** Move `key` to the end of the map so the LRU drop takes the right one. */
127
+ const touch = (key: string, state: EditorState): void => {
128
+ states.delete(key);
129
+ states.set(key, state);
130
+ };
131
+
132
+ const evict = (): void => {
133
+ while (states.size > MAX_CACHED) {
134
+ const oldest = states.keys().next();
135
+ if (oldest.done) return;
136
+ states.delete(oldest.value);
137
+ }
138
+ };
139
+
140
+ return {
141
+ stateFor(key, language, value) {
142
+ const cached = states.get(key);
143
+ if (cached) {
144
+ touch(key, cached);
145
+ return cached;
146
+ }
147
+ const state = EditorState.create({ doc: value, extensions: extensionsFor(language) });
148
+ states.set(key, state);
149
+ languages.set(key, language);
150
+ themesByKey.set(key, options.themes);
151
+ evict();
152
+ return state;
153
+ },
154
+
155
+ remember(key, state) {
156
+ touch(key, state);
157
+ },
158
+
159
+ drop(key) {
160
+ states.delete(key);
161
+ languages.delete(key);
162
+ themesByKey.delete(key);
163
+ },
164
+
165
+ optionsEffects(next, key) {
166
+ const wrapChanged = next.wordWrap !== options.wordWrap;
167
+ const readOnlyChanged = next.readOnly !== options.readOnly;
168
+ const themeChanged = next.themes.light !== options.themes.light || next.themes.dark !== options.themes.dark;
169
+ options.wordWrap = next.wordWrap;
170
+ options.readOnly = next.readOnly;
171
+ options.themes = next.themes;
172
+ const effects: StateEffect<unknown>[] = [];
173
+ if (wrapChanged) effects.push(wordWrapCompartment.reconfigure(wrapExtension(options.wordWrap)));
174
+ if (readOnlyChanged) effects.push(readOnlyCompartment.reconfigure(readOnlyExtension(options.readOnly)));
175
+ // The plugin carries the language, so the tokenizer is rebuilt from THIS
176
+ // document's grammar. Other cached states are handled by `currentEffects`
177
+ // when they are applied, which is the moment they need it.
178
+ const language = key ? languages.get(key) : undefined;
179
+ if (themeChanged && language) {
180
+ effects.push(themeCompartment.reconfigure(shikiHighlight(language, options.themes)));
181
+ if (key) themesByKey.set(key, options.themes);
182
+ }
183
+ return effects;
184
+ },
185
+
186
+ currentEffects(key) {
187
+ const effects: StateEffect<unknown>[] = [
188
+ wordWrapCompartment.reconfigure(wrapExtension(options.wordWrap)),
189
+ readOnlyCompartment.reconfigure(readOnlyExtension(options.readOnly)),
190
+ ];
191
+ const language = key ? languages.get(key) : undefined;
192
+ // Only when this state's tokenizer still holds another theme: a switch
193
+ // back to a tab that already has it must not re-tokenize.
194
+ if (key && language && !sameThemes(themesByKey.get(key), options.themes)) {
195
+ effects.push(themeCompartment.reconfigure(shikiHighlight(language, options.themes)));
196
+ themesByKey.set(key, options.themes);
197
+ }
198
+ return effects;
199
+ },
200
+ };
201
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The editor's extension stack, and the one thing in it that fails SILENTLY.
8
+ *
9
+ * `toggleComment` reads its tokens out of the `languageData` facet, and
10
+ * `languageDataAt` looks the name up in each provider's RESULT — so a provider
11
+ * that returns the tokens bare (`() => [{ line: '//' }]`) yields an empty list,
12
+ * the command returns `false`, and ⌘/ does nothing at all. Measured: the line
13
+ * stayed uncommented in TypeScript and in YAML, with no error anywhere. These
14
+ * cases assert the command's own effect on a state built from the same stack the
15
+ * component mounts.
16
+ */
17
+
18
+ import { describe, expect, test } from 'bun:test';
19
+ import { Compartment, EditorSelection, EditorState } from '@codemirror/state';
20
+ import { toggleComment } from '@codemirror/commands';
21
+
22
+ import { readOnlyExtension, surfaceExtensions, wrapExtension } from '@/client/components/common/code-surface/extensions';
23
+ import { SHIKI_THEMES } from '@/shared/lib/code/shiki-themes';
24
+
25
+ /** A state from the component's own stack, with a bare caret on line 1. */
26
+ function stateFor(language: string, doc = 'const alpha = 1;\n') {
27
+ return EditorState.create({
28
+ doc,
29
+ extensions: surfaceExtensions({
30
+ language,
31
+ wordWrapCompartment: new Compartment(),
32
+ readOnlyCompartment: new Compartment(),
33
+ themeCompartment: new Compartment(),
34
+ wordWrap: false,
35
+ readOnly: false,
36
+ themes: SHIKI_THEMES,
37
+ onDocChange: () => {},
38
+ onState: () => {},
39
+ }),
40
+ });
41
+ }
42
+
43
+ /** Apply `toggleComment` the way a `Command` would, returning the new text. */
44
+ function toggled(state: EditorState): string {
45
+ let next = state;
46
+ toggleComment({ state, dispatch: (transaction) => { next = transaction.state; } });
47
+ return next.doc.toString();
48
+ }
49
+
50
+ describe('comment tokens', () => {
51
+ test('toggling comments a TypeScript line with //', () => {
52
+ expect(toggled(stateFor('typescript'))).toBe('// const alpha = 1;\n');
53
+ });
54
+
55
+ test('toggling uses the language’s own token, not a hard-coded //', () => {
56
+ // A `//` in YAML is a syntax error, which is what makes this the case worth
57
+ // pinning: the table has to reach the command.
58
+ expect(toggled(stateFor('yaml', 'key: value\n'))).toBe('# key: value\n');
59
+ expect(toggled(stateFor('sql', 'select 1;\n'))).toBe('-- select 1;\n');
60
+ });
61
+
62
+ test('a block-only language is wrapped instead of prefixed', () => {
63
+ expect(toggled(stateFor('css', 'a { color: red; }\n'))).toContain('/*');
64
+ });
65
+
66
+ test('a second toggle restores the line', () => {
67
+ const once = toggled(stateFor('typescript'));
68
+ const twice = toggled(stateFor('typescript', once));
69
+ expect(twice).toBe('const alpha = 1;\n');
70
+ });
71
+ });
72
+
73
+ describe('read-only', () => {
74
+ test('both halves are installed, so neither typing nor a dispatch can edit', () => {
75
+ const state = EditorState.create({
76
+ doc: 'x',
77
+ extensions: [readOnlyExtension(true)],
78
+ });
79
+ // `readOnly` is what stops the commands; `editable` is what stops the DOM.
80
+ expect(state.readOnly).toBe(true);
81
+ });
82
+
83
+ test('an editable surface installs neither', () => {
84
+ const state = EditorState.create({ doc: 'x', extensions: [readOnlyExtension(false)] });
85
+ expect(state.readOnly).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe('wrap', () => {
90
+ test('wrapping is a compartment payload, so it can be swapped without a re-mount', () => {
91
+ const compartment = new Compartment();
92
+ const state = EditorState.create({ doc: 'x', extensions: [compartment.of(wrapExtension(true))] });
93
+ const unwrapped = state.update({ effects: compartment.reconfigure(wrapExtension(false)) }).state;
94
+ // Same document and selection: the reconfigure must not disturb either.
95
+ expect(unwrapped.doc.toString()).toBe('x');
96
+ expect(unwrapped.selection.main.head).toBe(state.selection.main.head);
97
+ });
98
+
99
+ test('the selection survives a wrap toggle', () => {
100
+ const compartment = new Compartment();
101
+ const state = EditorState.create({
102
+ doc: 'alpha beta',
103
+ selection: EditorSelection.range(0, 5),
104
+ extensions: [compartment.of(wrapExtension(false))],
105
+ });
106
+ const wrapped = state.update({ effects: compartment.reconfigure(wrapExtension(true)) }).state;
107
+ expect(wrapped.selection.main.from).toBe(0);
108
+ expect(wrapped.selection.main.to).toBe(5);
109
+ });
110
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The code surface's extension stack, in one place.
8
+ *
9
+ * Split from the component because the mount and the configuration are two
10
+ * different concerns, and because this is the list a reader checks when asking
11
+ * "what does this editor do". Each entry is here for a reason worth stating:
12
+ *
13
+ * - `history()` replaces the hand-rolled undo stack. It is per `EditorState`,
14
+ * and every document gets its own (see `documents.ts`), so history is already
15
+ * isolated per tab.
16
+ * - `drawSelection()` paints the selection and the multi-cursor bands. Without
17
+ * it a second cursor from `⌘D` is a state change with nothing on screen.
18
+ * - `rectangularSelection` / `crosshairCursor` are CM6's own answer to a column
19
+ * selection; the textarea surface could not do one at all.
20
+ * - `languageData` carries the comment tokens, which is the whole of what
21
+ * `toggleComment` needs — no lezer grammar, no syntax tree. It is read from
22
+ * the same table the old text commands used, so `#` stays right in YAML and
23
+ * `/* *\/` stays right in CSS.
24
+ * - Read-only is BOTH facets: `readOnly` stops the commands, `editable(false)`
25
+ * sets `contenteditable=false`, and only the pair makes a viewer a viewer
26
+ * (measured: `readOnly` alone let a programmatic `dispatch({changes})` through,
27
+ * which a viewer has no business accepting).
28
+ *
29
+ * `wordWrap` and `readOnly` arrive as compartments because they are per-view
30
+ * settings that must reconfigure a live editor without re-creating it — a
31
+ * reconfigure keeps the document, the selection and the history, which is
32
+ * exactly what a wrap toggle must not disturb.
33
+ */
34
+
35
+ import { type Compartment, EditorState, type Extension } from '@codemirror/state';
36
+ import {
37
+ crosshairCursor,
38
+ drawSelection,
39
+ dropCursor,
40
+ EditorView,
41
+ lineNumbers,
42
+ rectangularSelection,
43
+ } from '@codemirror/view';
44
+ import { history } from '@codemirror/commands';
45
+ import { indentUnit } from '@codemirror/language';
46
+
47
+ import { commentSyntaxFor } from '@/shared/lib/code/editor/comment-syntax';
48
+ import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
49
+ import { findMarksField } from '@/client/components/common/code-surface/marks';
50
+ import { surfaceKeymap } from '@/client/components/common/code-surface/keymap';
51
+ import { shikiHighlight, type ShikiThemeNames } from '@/client/components/common/code-surface/shiki';
52
+ import { surfaceTheme } from '@/client/components/common/code-surface/theme';
53
+
54
+ /** Indentation the surface inserts, in spaces. */
55
+ const INDENT = ' ';
56
+
57
+ /** Wrap on: CM6 wraps the content box; the panel no longer measures anything. */
58
+ export function wrapExtension(wordWrap: boolean): Extension {
59
+ return wordWrap ? EditorView.lineWrapping : [];
60
+ }
61
+
62
+ /** Both halves of read-only, or nothing for an editable surface. */
63
+ export function readOnlyExtension(readOnly: boolean): Extension {
64
+ return readOnly ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : [];
65
+ }
66
+
67
+ /** The comment tokens `toggleComment` reads out of `languageData`. */
68
+ function commentTokens(language: string): { line?: string; block?: { open: string; close: string } } {
69
+ const syntax = commentSyntaxFor(language);
70
+ if ('line' in syntax) return { line: syntax.line };
71
+ return { block: { open: syntax.block[0], close: syntax.block[1] } };
72
+ }
73
+
74
+ export interface SurfaceExtensionsOptions {
75
+ language: string;
76
+ /** Compartment holding `wrapExtension`. */
77
+ wordWrapCompartment: Compartment;
78
+ /** Compartment holding `readOnlyExtension`. */
79
+ readOnlyCompartment: Compartment;
80
+ /**
81
+ * Compartment holding the tokenizer.
82
+ *
83
+ * The editor theme is baked into the decoration plugin (Shiki is handed the
84
+ * theme NAMES at tokenize time), so a theme change reconfigures this — which
85
+ * rebuilds the plugin while the document, the selection and the history stay
86
+ * exactly where they were.
87
+ */
88
+ themeCompartment: Compartment;
89
+ wordWrap: boolean;
90
+ readOnly: boolean;
91
+ /** The two Shiki theme names this surface tokenizes with. */
92
+ themes: ShikiThemeNames;
93
+ /** Panel-owned commands the editor delegates instead of running. */
94
+ onCommand?: (command: EditorCommand) => void;
95
+ /** Fired for every user edit, with the whole new document. */
96
+ onDocChange: (value: string) => void;
97
+ /** Fired on every state that must be remembered (an edit or a selection move). */
98
+ onState: (state: EditorState) => void;
99
+ }
100
+
101
+ /** Everything the editor needs, built once per document state. */
102
+ export function surfaceExtensions({
103
+ language,
104
+ wordWrapCompartment,
105
+ readOnlyCompartment,
106
+ themeCompartment,
107
+ wordWrap,
108
+ readOnly,
109
+ themes,
110
+ onCommand,
111
+ onDocChange,
112
+ onState,
113
+ }: SurfaceExtensionsOptions): Extension[] {
114
+ return [
115
+ lineNumbers(),
116
+ history(),
117
+ drawSelection(),
118
+ dropCursor(),
119
+ rectangularSelection(),
120
+ crosshairCursor(),
121
+ EditorState.allowMultipleSelections.of(true),
122
+ indentUnit.of(INDENT),
123
+ // `toggleComment` with no grammar: the tokens are the entire input it needs.
124
+ // The provider must return an object KEYED by the data name —
125
+ // `languageDataAt` looks the name up in each result, so returning the tokens
126
+ // bare yields an empty list and the command silently does nothing.
127
+ EditorState.languageData.of(() => [{ commentTokens: commentTokens(language) }]),
128
+ findMarksField,
129
+ themeCompartment.of(shikiHighlight(language, themes)),
130
+ surfaceTheme(),
131
+ wordWrapCompartment.of(wrapExtension(wordWrap)),
132
+ readOnlyCompartment.of(readOnlyExtension(readOnly)),
133
+ // The browser's own spellchecker underlines every identifier and offers to
134
+ // "correct" them; autocapitalise/autocorrect mangle code on a phone. CM6
135
+ // sets these itself, and stating them keeps a future default from silently
136
+ // regressing a mobile keyboard.
137
+ EditorView.contentAttributes.of({
138
+ spellcheck: 'false',
139
+ autocorrect: 'off',
140
+ autocapitalize: 'off',
141
+ translate: 'no',
142
+ }),
143
+ EditorView.updateListener.of((update) => {
144
+ // The per-tab cache stores the LIVE state so a switch back finds its
145
+ // caret: a selection move is as much of that state as an edit is.
146
+ if (update.docChanged || update.selectionSet) onState(update.state);
147
+ if (update.docChanged) onDocChange(update.state.doc.toString());
148
+ // The selection as a DOM fact, because a `contenteditable` exposes none.
149
+ // The old surface was a `<textarea>`, whose `selectionStart`/`selectionEnd`
150
+ // and line arithmetic were readable from the element; these two attributes
151
+ // are the same facts for the same readers (the e2e layer's outline and
152
+ // caret specs), and nothing in the app reads them.
153
+ if (update.docChanged || update.selectionSet) {
154
+ const range = update.state.selection.main;
155
+ const attrs = update.view.contentDOM.dataset;
156
+ attrs.selection = `${range.from},${range.to}`;
157
+ attrs.line = String(update.state.doc.lineAt(range.from).number);
158
+ }
159
+ }),
160
+ surfaceKeymap(onCommand, readOnly),
161
+ ];
162
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The imperative surface handle, built from the mount's refs.
8
+ *
9
+ * Split out of `index.tsx` because the component is at the repository's line
10
+ * ceiling and this half is dispatch, not rendering: every method here is "look
11
+ * up the live view, then hand CodeMirror a transaction". Keeping it a factory
12
+ * over the refs (rather than a hook) is what makes the identity stable — the
13
+ * handle is built once for the component's life, so a consumer holding it never
14
+ * re-subscribes.
15
+ *
16
+ * Two rules live here and nowhere else, both measured:
17
+ *
18
+ * - **`applyDocument` is a USER edit.** It is deliberately not silenced as an
19
+ * external write: the find bar's replace has to reach `onValueChange` exactly
20
+ * like typing does, or the parent keeps the pre-replace buffer and the next
21
+ * render writes the OLD text back over it.
22
+ * - **A reveal cancels a pending scroll restore.** The restore's
23
+ * `ResizeObserver` can run BEFORE the scroll event a reveal's dispatch causes,
24
+ * so it would write the remembered offset back and the view would never move.
25
+ */
26
+
27
+ import { EditorView } from '@codemirror/view';
28
+ import type { MutableRef } from '@/client/components/common/code-surface/sync';
29
+ import { cancelScrollRestore } from '@/client/hooks/editor/use-scroll-restore';
30
+ import { runSurfaceCommand } from '@/client/components/common/code-surface/keymap';
31
+ import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
32
+
33
+ /** The imperative surface every consumer of the editor holds. */
34
+ export interface CodeSurfaceHandle {
35
+ /** Current selection offsets in the buffer, or null before the view mounts. */
36
+ getSelection(): { start: number; end: number } | null;
37
+ /** Select `[start, end)`; `focus` moves keyboard focus into the editor. */
38
+ select(start: number, end: number, focus?: boolean): void;
39
+ /** Replace the buffer as one undoable edit, caret at `[caretStart, caretEnd)`, focus unchanged. */
40
+ applyDocument(value: string, caretStart: number, caretEnd: number): void;
41
+ /** Run an editor command by name (the palette's path into the editor). */
42
+ runCommand(command: EditorCommand): void;
43
+ /**
44
+ * Bring the line holding `offset` into view WITHOUT moving the caret — what
45
+ * "step to the next match" needs: the user's focus stays in the find field
46
+ * while the match scrolls under it.
47
+ */
48
+ revealOffset(offset: number): void;
49
+ }
50
+
51
+ export interface SurfaceHandleRefs {
52
+ view: MutableRef<EditorView | null>;
53
+ /** The live `onCommand`, mirrored so the handle never captures a stale one. */
54
+ onCommand: MutableRef<((command: EditorCommand) => void) | undefined>;
55
+ }
56
+
57
+ /** The handle a `forwardRef` caller receives. Built once per mount. */
58
+ export function createSurfaceHandle({ view: viewRef, onCommand: onCommandRef }: SurfaceHandleRefs): CodeSurfaceHandle {
59
+ return {
60
+ getSelection: () => {
61
+ const view = viewRef.current;
62
+ if (!view) return null;
63
+ const range = view.state.selection.main;
64
+ return { start: range.from, end: range.to };
65
+ },
66
+ select: (start, end, focus = true) => {
67
+ const view = viewRef.current;
68
+ if (!view) return;
69
+ view.dispatch({ selection: { anchor: start, head: end }, scrollIntoView: false });
70
+ // `focus()` is CM6's own `focusPreventScroll`, which is what stops the
71
+ // browser scrolling the caret into view a second time — the caller's
72
+ // `revealOffset` is the only thing that scrolls.
73
+ if (focus) view.focus();
74
+ },
75
+ applyDocument: (next, caretStart, caretEnd) => {
76
+ const view = viewRef.current;
77
+ if (!view) return;
78
+ // One transaction, so ⌘Z undoes the whole replacement: a replace-all that
79
+ // took one undo step per match is what the old surface had to hand-roll a
80
+ // history for.
81
+ view.dispatch({
82
+ changes: { from: 0, to: view.state.doc.length, insert: next },
83
+ selection: { anchor: caretStart, head: caretEnd },
84
+ });
85
+ },
86
+ runCommand: (command) => {
87
+ const view = viewRef.current;
88
+ if (!view) return;
89
+ runSurfaceCommand(command, view, onCommandRef.current);
90
+ },
91
+ revealOffset: (offset) => {
92
+ const view = viewRef.current;
93
+ if (!view) return;
94
+ const at = Math.max(0, Math.min(offset, view.state.doc.length));
95
+ cancelScrollRestore(view.scrollDOM);
96
+ // `y: 'nearest'` is the rule the old surface hand-rolled: a reveal only
97
+ // scrolls when the target is outside the visible band, so stepping
98
+ // through matches does not jump the whole view on every step.
99
+ view.dispatch({ effects: EditorView.scrollIntoView(at, { y: 'nearest', yMargin: 24 }) });
100
+ },
101
+ };
102
+ }