@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
@@ -1,216 +0,0 @@
1
- /**
2
- * @license
3
- * SPDX-License-Identifier: Apache-2.0
4
- */
5
-
6
- /**
7
- * The editor's undo stack.
8
- *
9
- * The rule under test is the one that was wrong: the re-seed exists for a value
10
- * replaced from the OUTSIDE (a file switch, a refresh), but it also fired for
11
- * the component's own edits — every keystroke pushed a second, identical entry,
12
- * so the first ⌘Z restored the buffer to the state it was already in and undo
13
- * looked broken until the second press. `markApplied` is what separates the two,
14
- * and these assertions are what keeps it separating them.
15
- *
16
- * Runs against a real DOM (happy-dom) because it is a Preact hook holding a
17
- * `<textarea>`; the modules below are imported dynamically, after those globals
18
- * exist (Preact binds its environment at evaluation time).
19
- */
20
-
21
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
22
- import { Window } from 'happy-dom';
23
- import type { h as PreactH, render as PreactRender } from 'preact';
24
- import type { act as PreactAct } from 'preact/test-utils';
25
- import type { EditorHistory, useEditorHistory as UseEditorHistoryHook } from '@/client/hooks/editor/use-editor-history';
26
-
27
- let useEditorHistory: typeof UseEditorHistoryHook;
28
- let h: typeof PreactH;
29
- let render: typeof PreactRender;
30
- let act: typeof PreactAct;
31
-
32
- let api: EditorHistory | null = null;
33
- let container: HTMLElement;
34
- let textarea: HTMLTextAreaElement;
35
-
36
- /** The buffer the hook is told is on screen, changed from the outside by a test. */
37
- let externalValue = 'one';
38
-
39
- function Harness() {
40
- api = useEditorHistory({
41
- value: externalValue,
42
- inputRef: { current: textarea },
43
- onValueChange: (next) => {
44
- textarea.value = next;
45
- },
46
- });
47
- return null;
48
- }
49
-
50
- /** Flush the effect that watches `value`, with no timers: it resolves on a render. */
51
- async function flush() {
52
- for (let i = 0; i < 3; i++) {
53
- await act(async () => {
54
- for (let turn = 0; turn < 4; turn++) await Promise.resolve();
55
- });
56
- }
57
- }
58
-
59
- function domGlobals(win: Window): Record<string, unknown> {
60
- return {
61
- window: win,
62
- document: win.document,
63
- navigator: win.navigator,
64
- Node: win.Node,
65
- Element: win.Element,
66
- HTMLElement: win.HTMLElement,
67
- HTMLTextAreaElement: win.HTMLTextAreaElement,
68
- };
69
- }
70
-
71
- const installed: Record<string, unknown> = {};
72
- const displaced: Record<string, unknown> = {};
73
-
74
- beforeAll(async () => {
75
- const win = new Window({ url: 'http://localhost' });
76
- for (const [key, value] of Object.entries(domGlobals(win))) {
77
- displaced[key] = (globalThis as Record<string, unknown>)[key];
78
- installed[key] = value;
79
- (globalThis as Record<string, unknown>)[key] = value;
80
- }
81
- ({ useEditorHistory } = await import('@/client/hooks/editor/use-editor-history'));
82
- ({ h, render } = await import('preact'));
83
- ({ act } = await import('preact/test-utils'));
84
- });
85
-
86
- afterAll(() => {
87
- for (const key of Object.keys(installed)) {
88
- if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
89
- else (globalThis as Record<string, unknown>)[key] = displaced[key];
90
- }
91
- });
92
-
93
- beforeEach(async () => {
94
- api = null;
95
- externalValue = 'one';
96
- container = document.createElement('div');
97
- document.body.appendChild(container);
98
- textarea = document.createElement('textarea');
99
- textarea.value = 'one';
100
- container.appendChild(textarea);
101
- await act(async () => {
102
- render(h(Harness, {}), container);
103
- });
104
- });
105
-
106
- afterEach(() => {
107
- if (container) render(null, container);
108
- container?.remove();
109
- });
110
-
111
- describe('useEditorHistory', () => {
112
- test('commit writes the buffer, the caret and the caller’s state', async () => {
113
- await act(async () => {
114
- api?.commit({ value: 'two', selectionStart: 2, selectionEnd: 2 });
115
- });
116
-
117
- expect(textarea.value).toBe('two');
118
- expect(textarea.selectionStart).toBe(2);
119
- });
120
-
121
- test('an external value replacement is recorded, so undo reaches the old file', async () => {
122
- // A file switch: the hook did not write this, so it becomes a history entry
123
- // rather than silently replacing the stack.
124
- externalValue = 'switched';
125
- await act(async () => {
126
- render(h(Harness, {}), container);
127
- });
128
- await flush();
129
-
130
- await act(async () => {
131
- api?.step('undo');
132
- });
133
-
134
- expect(textarea.value).toBe('one');
135
- });
136
-
137
- test('a value the hook itself wrote is NOT re-recorded', async () => {
138
- // The bug this guards: an extra entry per keystroke made the first ⌘Z a
139
- // no-op, because it restored the buffer to what was already on screen.
140
- await act(async () => {
141
- api?.commit({ value: 'two', selectionStart: 2, selectionEnd: 2 });
142
- });
143
- externalValue = 'two';
144
- await act(async () => {
145
- render(h(Harness, {}), container);
146
- });
147
- await flush();
148
-
149
- await act(async () => {
150
- api?.step('undo');
151
- });
152
-
153
- // One press reaches the ORIGINAL buffer; a duplicate entry would have
154
- // restored 'two' again.
155
- expect(textarea.value).toBe('one');
156
- });
157
-
158
- test('markApplied declares a browser edit as already recorded', async () => {
159
- await act(async () => {
160
- api?.markApplied('typed');
161
- });
162
- externalValue = 'typed';
163
- await act(async () => {
164
- render(h(Harness, {}), container);
165
- });
166
- await flush();
167
-
168
- await act(async () => {
169
- api?.step('undo');
170
- });
171
-
172
- // Nothing was pushed for `typed`, so there is nothing to step back to: the
173
- // stack still holds only the initial record.
174
- expect(textarea.value).toBe('one');
175
- });
176
-
177
- test('step at the ends of the stack is a no-op', async () => {
178
- await act(async () => {
179
- api?.step('undo');
180
- });
181
- const afterUndo = textarea.value;
182
- await act(async () => {
183
- api?.step('redo');
184
- });
185
-
186
- expect(afterUndo).toBe('one');
187
- expect(textarea.value).toBe('one');
188
- });
189
-
190
- test('commitEdit supersedes the top entry, keeping its caret for the undo', async () => {
191
- // A line command replaces a whole range at once; undo must return the caret
192
- // to the selection the command replaced, not to where the edit ended.
193
- textarea.selectionStart = 1;
194
- textarea.selectionEnd = 3;
195
- await act(async () => {
196
- api?.commitEdit({ value: 'one-changed', selectionStart: 11, selectionEnd: 11 });
197
- });
198
- await act(async () => {
199
- api?.step('undo');
200
- });
201
-
202
- expect(textarea.value).toBe('one');
203
- expect(textarea.selectionStart).toBe(1);
204
- expect(textarea.selectionEnd).toBe(3);
205
- });
206
-
207
- test('the exposed buffer is never a render behind', async () => {
208
- // `handleChange` diffs against this; a value that lagged a keystroke read a
209
- // one-character edit as a whole-word replacement.
210
- await act(async () => {
211
- api?.commit({ value: 'fresh', selectionStart: 5, selectionEnd: 5 });
212
- });
213
-
214
- expect(api?.buffer).toBe('fresh');
215
- });
216
- });
@@ -1,134 +0,0 @@
1
- /**
2
- * @license
3
- * SPDX-License-Identifier: Apache-2.0
4
- */
5
-
6
- /**
7
- * The editor's undo stack, wired to the DOM node.
8
- *
9
- * `history.ts` is the pure bookkeeping (push, merge, step, cap); this is the
10
- * part that has to touch the `<textarea>`: committing an edit as one entry,
11
- * restoring a stepped record, and re-seeding the stack when the value is
12
- * replaced from the OUTSIDE.
13
- *
14
- * The re-seed is the subtle one. It exists for a file switch or a refresh, but
15
- * a naive `[value]` effect also fires for the component's own edits — every
16
- * keystroke then pushed a second, identical entry, so the first ⌘Z restored the
17
- * buffer to the state it was already in and undo looked broken until the second
18
- * press (and the no-op `onValueChange` truncated the redo stack). The `applied`
19
- * ref is what distinguishes "someone else replaced the value" from "we did".
20
- */
21
-
22
- import { useEffect, useRef } from 'preact/hooks';
23
-
24
- import { createHistory, pushEdit, pushRecord, stepHistory, type History, type HistoryRecord } from '@/shared/lib/code/editor/history';
25
-
26
- /** The fields every write path needs: a buffer and where the caret goes. */
27
- export type HistoryEdit = Pick<HistoryRecord, 'value' | 'selectionStart' | 'selectionEnd'>;
28
-
29
- export interface EditorHistory {
30
- /** The stack itself, for the typing-burst merge in the change handler. */
31
- history: History;
32
- /**
33
- * The buffer as this component last knew it — its own writes included, and
34
- * never a render behind. Diffing a browser edit has to start from here: the
35
- * `value` prop lags a keystroke, and a stale `before` reads a one-character
36
- * edit as a whole-word replacement.
37
- */
38
- readonly buffer: string;
39
- /** Commit an edit as ONE undoable entry. */
40
- commit: (edit: HistoryEdit) => void;
41
- /**
42
- * Commit an edit that SUPERSEDES the entry on top, keeping that entry's
43
- * caret so undo returns to the selection the edit replaced.
44
- */
45
- commitEdit: (edit: HistoryEdit) => void;
46
- /** Step the stack and restore the record; no-op at either end. */
47
- step: (direction: 'undo' | 'redo') => void;
48
- /**
49
- * Declare a value the BROWSER produced as already applied, so the re-seed
50
- * effect below does not record it a second time. The change handler calls
51
- * this for every keystroke and every replicated multi-cursor edit.
52
- */
53
- markApplied: (value: string) => void;
54
- }
55
-
56
- export interface UseEditorHistoryOptions {
57
- /** The controlled buffer, used to detect an external replacement. */
58
- value: string;
59
- /** The live `<textarea>`, so an external replacement records its own caret. */
60
- inputRef: { current: HTMLTextAreaElement | null };
61
- /** Push the committed buffer to the caller. */
62
- onValueChange: (value: string) => void;
63
- }
64
-
65
- export function useEditorHistory({ value, inputRef, onValueChange }: UseEditorHistoryOptions): EditorHistory {
66
- const historyRef = useRef<History>(createHistory());
67
- /** The value this component itself last wrote — see the module note. */
68
- const appliedRef = useRef<string | null>(null);
69
-
70
- // Re-record when the controlled value is replaced from the outside
71
- // (file switch, refresh) so undo does not jump between documents.
72
- useEffect(() => {
73
- const input = inputRef.current;
74
- if (!input || appliedRef.current === value) return;
75
- appliedRef.current = value;
76
- pushRecord(historyRef.current, {
77
- value: input.value,
78
- selectionStart: input.selectionStart,
79
- selectionEnd: input.selectionEnd,
80
- timestamp: Date.now(),
81
- });
82
- // eslint-disable-next-line react-hooks/exhaustive-deps
83
- }, [value]);
84
-
85
- /** Put a buffer on screen without touching the stack. */
86
- const write = (edit: HistoryEdit) => {
87
- appliedRef.current = edit.value;
88
- onValueChange(edit.value);
89
- const input = inputRef.current;
90
- if (!input) return;
91
- input.value = edit.value;
92
- input.selectionStart = edit.selectionStart;
93
- input.selectionEnd = edit.selectionEnd;
94
- };
95
-
96
- return {
97
- history: historyRef.current,
98
-
99
- get buffer() {
100
- return appliedRef.current ?? value;
101
- },
102
-
103
- commit: (edit) => {
104
- pushEdit(historyRef.current, { ...edit, timestamp: Date.now() });
105
- write(edit);
106
- },
107
-
108
- commitEdit: (edit) => {
109
- const history = historyRef.current;
110
- const last = history.stack[history.offset];
111
- const input = inputRef.current;
112
- if (last && input) {
113
- // The entry on top is about to be superseded: keep where its caret was,
114
- // so undo returns to the selection this edit replaced.
115
- history.stack[history.offset] = {
116
- ...last,
117
- selectionStart: input.selectionStart,
118
- selectionEnd: input.selectionEnd,
119
- };
120
- }
121
- pushEdit(history, { ...edit, timestamp: Date.now() });
122
- write(edit);
123
- },
124
-
125
- step: (direction) => {
126
- const record = stepHistory(historyRef.current, direction);
127
- if (record) write(record);
128
- },
129
-
130
- markApplied: (next) => {
131
- appliedRef.current = next;
132
- },
133
- };
134
- }
@@ -1,236 +0,0 @@
1
- /**
2
- * @license
3
- * SPDX-License-Identifier: Apache-2.0
4
- */
5
-
6
- /**
7
- * `useOccurrences` — the ranges behind ⌘D, ⌘⇧L and ⌘⇧D.
8
- *
9
- * The rules live in `shared/lib/code/editor/occurrences` and are tested there;
10
- * what this file covers is the hook's own contract: the caller's copy wins
11
- * (clearing the ranges in the panel must clear them here), every change is
12
- * announced so the surface repaints, and the textarea is read rather than
13
- * assumed.
14
- *
15
- * Runs against a real DOM (happy-dom) because it is a Preact hook; the modules
16
- * below are imported dynamically, after those globals exist.
17
- */
18
-
19
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
20
- import { Window } from 'happy-dom';
21
- import type { h as PreactH, render as PreactRender } from 'preact';
22
- import type { act as PreactAct } from 'preact/test-utils';
23
- import type { OccurrencesApi, useOccurrences as UseOccurrencesHook } from '@/client/hooks/editor/use-occurrences';
24
- import type { TextRange } from '@/shared/lib/code/editor/commands';
25
-
26
- let useOccurrences: typeof UseOccurrencesHook;
27
- let h: typeof PreactH;
28
- let render: typeof PreactRender;
29
- let act: typeof PreactAct;
30
-
31
- let api: OccurrencesApi | null = null;
32
- let container: HTMLElement;
33
- let textarea: HTMLTextAreaElement;
34
- /** What the caller passes in as its own copy, changed per test. */
35
- let controlled: TextRange[] | undefined;
36
- const announced: Array<readonly TextRange[]> = [];
37
- const selections: Array<[number, number]> = [];
38
-
39
- function Harness() {
40
- api = useOccurrences({
41
- controlled,
42
- onChange: (ranges) => announced.push(ranges),
43
- });
44
- return null;
45
- }
46
-
47
- async function flush() {
48
- for (let i = 0; i < 3; i++) {
49
- await act(async () => {
50
- for (let turn = 0; turn < 4; turn++) await Promise.resolve();
51
- });
52
- }
53
- }
54
-
55
- async function rerender() {
56
- await act(async () => {
57
- render(h(Harness, {}), container);
58
- });
59
- await flush();
60
- }
61
-
62
- function select(start: number, end = start) {
63
- textarea.selectionStart = start;
64
- textarea.selectionEnd = end;
65
- }
66
-
67
- /**
68
- * Stands in for the editor's own `select`: it MOVES the textarea's selection and
69
- * records it. The hook reads the textarea, so a callback that only recorded the
70
- * range would leave every later press looking at the original caret.
71
- */
72
- function selectingSurface() {
73
- return (start: number, end: number) => {
74
- selections.push([start, end]);
75
- select(start, end);
76
- };
77
- }
78
-
79
- function domGlobals(win: Window): Record<string, unknown> {
80
- return {
81
- window: win,
82
- document: win.document,
83
- navigator: win.navigator,
84
- Node: win.Node,
85
- Element: win.Element,
86
- HTMLElement: win.HTMLElement,
87
- HTMLTextAreaElement: win.HTMLTextAreaElement,
88
- };
89
- }
90
-
91
- const installed: Record<string, unknown> = {};
92
- const displaced: Record<string, unknown> = {};
93
-
94
- beforeAll(async () => {
95
- const win = new Window({ url: 'http://localhost' });
96
- for (const [key, value] of Object.entries(domGlobals(win))) {
97
- displaced[key] = (globalThis as Record<string, unknown>)[key];
98
- installed[key] = value;
99
- (globalThis as Record<string, unknown>)[key] = value;
100
- }
101
- ({ useOccurrences } = await import('@/client/hooks/editor/use-occurrences'));
102
- ({ h, render } = await import('preact'));
103
- ({ act } = await import('preact/test-utils'));
104
- });
105
-
106
- afterAll(() => {
107
- for (const key of Object.keys(installed)) {
108
- if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
109
- else (globalThis as Record<string, unknown>)[key] = displaced[key];
110
- }
111
- });
112
-
113
- beforeEach(async () => {
114
- api = null;
115
- controlled = undefined;
116
- announced.length = 0;
117
- selections.length = 0;
118
- container = document.createElement('div');
119
- document.body.appendChild(container);
120
- textarea = document.createElement('textarea');
121
- textarea.value = 'alpha beta alpha gamma alpha';
122
- container.appendChild(textarea);
123
- await rerender();
124
- });
125
-
126
- afterEach(() => {
127
- if (container) render(null, container);
128
- container?.remove();
129
- });
130
-
131
- describe('useOccurrences', () => {
132
- test('starts with no extra ranges', () => {
133
- expect(api?.ranges).toEqual([]);
134
- });
135
-
136
- test('⌘D selects the word first, then grows one occurrence per press', async () => {
137
- select(2);
138
- await act(async () => {
139
- api?.selectNext(textarea, selectingSurface());
140
- });
141
- expect(selections.at(-1)).toEqual([0, 5]);
142
- expect(api?.ranges).toEqual([]);
143
-
144
- await act(async () => {
145
- api?.selectNext(textarea, selectingSurface());
146
- });
147
- expect(api?.ranges).toEqual([{ start: 11, end: 16 }]);
148
- });
149
-
150
- test('⌘⇧L takes every occurrence, primary first', async () => {
151
- select(0, 5);
152
- await act(async () => {
153
- api?.selectAll(textarea, selectingSurface());
154
- });
155
-
156
- expect(selections.at(-1)).toEqual([0, 5]);
157
- expect(api?.ranges).toEqual([
158
- { start: 11, end: 16 },
159
- { start: 23, end: 28 },
160
- ]);
161
- });
162
-
163
- test('every change is announced so the surface can repaint', async () => {
164
- select(0, 5);
165
- await act(async () => {
166
- api?.selectAll(textarea, () => {});
167
- });
168
-
169
- expect(announced.at(-1)?.length).toBe(2);
170
- });
171
-
172
- test('clear drops every range and says so', async () => {
173
- select(0, 5);
174
- await act(async () => {
175
- api?.selectAll(textarea, () => {});
176
- });
177
- await act(async () => {
178
- api?.clear();
179
- });
180
-
181
- expect(api?.ranges).toEqual([]);
182
- expect(announced.at(-1)).toEqual([]);
183
- });
184
-
185
- test('the caller’s copy is authoritative', async () => {
186
- // The panel can clear the ranges without the editor asking (a file switch,
187
- // a save); the hook has to follow, not keep its own stale list.
188
- controlled = [{ start: 1, end: 3 }];
189
- await rerender();
190
-
191
- expect(api?.ranges).toEqual([{ start: 1, end: 3 }]);
192
-
193
- controlled = [];
194
- await rerender();
195
-
196
- expect(api?.ranges).toEqual([]);
197
- });
198
-
199
- test('⌘⇧D adds a caret below on the same column', async () => {
200
- textarea.value = 'one\ntwo\nthree';
201
- select(1);
202
- await act(async () => {
203
- api?.insertCursorBelow(textarea);
204
- });
205
-
206
- expect(api?.ranges).toEqual([{ start: 5, end: 5 }]);
207
- });
208
-
209
- test('⌘⇧D on the last line adds nothing', async () => {
210
- textarea.value = 'one\ntwo';
211
- select(5);
212
- await act(async () => {
213
- api?.insertCursorBelow(textarea);
214
- });
215
-
216
- expect(api?.ranges).toEqual([]);
217
- });
218
-
219
- test('replicate reports nothing without extra ranges', () => {
220
- expect(api?.replicate('abc', 'abXc', { start: 2, end: 2 })).toBeNull();
221
- });
222
-
223
- test('replicate replays the browser’s edit at every extra range', async () => {
224
- select(0, 5);
225
- await act(async () => {
226
- api?.selectAll(textarea, () => {});
227
- });
228
-
229
- // The browser replaced `alpha` at 0..5 with `Z`; the other two follow.
230
- const applied = api?.replicate('alpha beta alpha gamma alpha', 'Z beta alpha gamma alpha', { start: 0, end: 5 });
231
-
232
- expect(applied?.value).toBe('Z beta Z gamma Z');
233
- expect(applied?.caret).toBe(1);
234
- expect(applied?.rest.length).toBe(2);
235
- });
236
- });
@@ -1,98 +0,0 @@
1
- /**
2
- * @license
3
- * SPDX-License-Identifier: Apache-2.0
4
- */
5
-
6
- /**
7
- * The hook that owns the editor's extra selection ranges.
8
- *
9
- * It is a thin shell over `shared/lib/code/editor/occurrences`: the rules live
10
- * there as pure functions, and this holds the ranges in a ref (the keydown and
11
- * `input` handlers read them synchronously, inside one event) and tells the
12
- * caller through `onChange` so the surface can paint them.
13
- *
14
- * The split is deliberate: a hook that decides where a range goes cannot be
15
- * tested without a DOM, and every rule here is one a user notices when it is
16
- * wrong.
17
- */
18
-
19
- import { useRef } from 'preact/hooks';
20
-
21
- import {
22
- growToNextOccurrence,
23
- offsetBelow,
24
- replicateAtRanges,
25
- selectAllOccurrences,
26
- } from '@/shared/lib/code/editor/occurrences';
27
- import type { TextRange } from '@/shared/lib/code/editor/commands';
28
-
29
- export interface UseOccurrencesOptions {
30
- /** Called with the extra ranges whenever they change, so the surface repaints. */
31
- onChange: (ranges: readonly TextRange[]) => void;
32
- /** The caller's copy of the ranges — clearing them there must clear them here. */
33
- controlled?: readonly TextRange[];
34
- }
35
-
36
- export interface OccurrencesApi {
37
- /** The extra ranges, excluding the textarea's own (primary) selection. */
38
- readonly ranges: readonly TextRange[];
39
- /** Replace the ranges and notify the surface. */
40
- set: (ranges: readonly TextRange[]) => void;
41
- /** Drop every extra range. */
42
- clear: () => void;
43
- /** ⌘D — grow the primary selection to the next occurrence of what it covers. */
44
- selectNext: (input: HTMLTextAreaElement, select: (start: number, end: number) => void) => void;
45
- /** ⌘⇧L — select every occurrence of the selection/word at the caret. */
46
- selectAll: (input: HTMLTextAreaElement, select: (start: number, end: number) => void) => void;
47
- /** ⌘⇧D — add an empty caret on the line below. */
48
- insertCursorBelow: (input: HTMLTextAreaElement) => void;
49
- /**
50
- * Replay the browser's edit at every extra range. Returns the buffer and
51
- * caret the caller should commit, or null when there is nothing to replicate.
52
- */
53
- replicate: (before: string, after: string, primaryBefore: TextRange) => { value: string; caret: number; rest: TextRange[] } | null;
54
- }
55
-
56
- export function useOccurrences({ onChange, controlled }: UseOccurrencesOptions): OccurrencesApi {
57
- const ref = useRef<readonly TextRange[]>([]);
58
- // The caller may clear the ranges without the editor asking (a file switch, a
59
- // save), so its copy is mirrored into the ref the handlers read.
60
- if (controlled && controlled !== ref.current) ref.current = controlled;
61
-
62
- const set = (ranges: readonly TextRange[]) => {
63
- ref.current = ranges;
64
- onChange(ranges);
65
- };
66
-
67
- return {
68
- get ranges() {
69
- return ref.current;
70
- },
71
- set,
72
- clear: () => set([]),
73
-
74
- selectNext: (input, select) => {
75
- const current: TextRange = { start: input.selectionStart, end: input.selectionEnd };
76
- const next = growToNextOccurrence(input.value, current, ref.current);
77
- if (!next) return;
78
- select(next.primary.start, next.primary.end);
79
- set(next.extras);
80
- },
81
-
82
- selectAll: (input, select) => {
83
- const current: TextRange = { start: input.selectionStart, end: input.selectionEnd };
84
- const all = selectAllOccurrences(input.value, current);
85
- if (!all) return;
86
- select(all.primary.start, all.primary.end);
87
- set(all.extras);
88
- },
89
-
90
- insertCursorBelow: (input) => {
91
- const target = offsetBelow(input.value, input.selectionStart);
92
- if (target === null) return;
93
- set([...ref.current, { start: target, end: target }]);
94
- },
95
-
96
- replicate: (before, after, primaryBefore) => replicateAtRanges(before, after, primaryBefore, ref.current),
97
- };
98
- }