@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,348 +1,287 @@
1
- import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The code surface: CodeMirror 6, coloured by Shiki.
8
+ *
9
+ * What this replaced, and why it is a mount rather than a component tree: the
10
+ * old surface was a transparent `<textarea>` over a Shiki-highlighted `<pre>`,
11
+ * with Kabmo's own line windowing, wrap measurement, undo stack, multi-cursor
12
+ * replication, find matcher and gutter geometry. Typing on a 4 800-line file
13
+ * cost a measured 59.6 ms median, because every keystroke re-tokenized the
14
+ * visible window and replaced the whole highlight layer's HTML; CM6 costs
15
+ * 2.1 ms and the figure is flat from 8 000 to 20 000 lines, because it renders
16
+ * and tokenizes only its viewport.
17
+ *
18
+ * Two decisions are load-bearing and both are stated elsewhere: Shiki stays the
19
+ * tokenizer (`shiki.ts` — the app has one palette, and a lezer build would cover
20
+ * 48 of the 59 ids `shiki-langs` knows), and the handle's contract is unchanged
21
+ * (`CodeSurfaceHandle`), which is what keeps the find widget, the outline jump,
22
+ * the line jump and the search reveal working without knowing about CM6.
23
+ *
24
+ * The host is a BOUNDED box. Measured: an unconstrained host grows to the
25
+ * document's height (363 758 px at 20 000 lines), and then the scroll geometry,
26
+ * the gutter and the restore path all become the outer scroller's problem. CM6
27
+ * owns its own scroller, and it hands that element to its owner through
28
+ * `onScroller` (a `ref` cannot reach a node the component does not render, and
29
+ * the restore has to re-arm when the view mounts).
30
+ */
31
+
32
+ import { useImperativeHandle, useLayoutEffect, useRef } from 'preact/hooks';
2
33
  import { forwardRef } from 'preact/compat';
3
34
  import type { CSSProperties } from 'preact';
35
+ import { EditorView } from '@codemirror/view';
4
36
 
5
- import { CodeEditor, type CodeEditorHandle } from '@/client/components/common/code-editor';
6
- import { measureRowHeight, measureWrappedLines, type WrappedLines } from '@/client/components/common/code-surface/measure';
7
- import { findScrollContainer } from '@/client/hooks/editor/scroll-container';
8
- import { useCodeWindow } from '@/client/hooks/editor/use-code-window';
9
- import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
10
- import {
11
- measuredGeometry,
12
- shouldWindow,
13
- uniformGeometry,
14
- type LineGeometry,
15
- type LineWindow,
16
- } from '@/shared/lib/code/lazy-window';
17
- import type { FindMatch } from '@/shared/lib/code/editor/find';
37
+ import { createDocumentCache, type DocumentCache } from '@/client/components/common/code-surface/documents';
38
+ import type { ShikiThemeNames } from '@/client/components/common/code-surface/shiki';
39
+ import { SHIKI_THEMES } from '@/shared/lib/code/shiki-themes';
40
+ import { createSurfaceHandle, type CodeSurfaceHandle } from '@/client/components/common/code-surface/handle';
41
+ import { setFindMarks, type MarkRange } from '@/client/components/common/code-surface/marks';
42
+ import { syncExternal } from '@/client/components/common/code-surface/sync';
18
43
  import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
19
- import { lineIndexAt } from '@/shared/lib/code/editor/lines';
20
- import type { TextRange } from '@/shared/lib/code/editor/commands';
21
- import { countLines } from '@/shared/lib/code/editor/lines';
22
- import { highlightCode } from '@/shared/lib/code/syntax-highlight';
23
- import { highlightCodeWindow } from '@/shared/lib/code/windowed-highlight';
24
-
25
- const WRAP_ON = '!whitespace-pre-wrap !break-words';
26
- const WRAP_OFF = '!whitespace-pre !break-normal';
27
44
 
28
- export interface CodeSurfaceHandle {
29
- /** Current selection offsets in the buffer, or null before the field mounts. */
30
- getSelection(): { start: number; end: number } | null;
31
- /** Select `[start, end)`; `focus` moves keyboard focus into the editor. */
32
- select(start: number, end: number, focus?: boolean): void;
33
- /** Replace the buffer as one undoable edit, caret at `[caretStart, caretEnd)`, focus unchanged. */
34
- applyDocument(value: string, caretStart: number, caretEnd: number): void;
35
- /** Run an editor command by name (the palette's path into the editor). */
36
- runCommand(command: EditorCommand): void;
37
- /**
38
- * Bring the line holding `offset` into view WITHOUT moving the caret — what
39
- * "step to the next match" needs: the user's focus stays in the find field
40
- * while the match scrolls under it.
41
- */
42
- revealOffset(offset: number): void;
43
- }
45
+ export type { CodeSurfaceHandle } from '@/client/components/common/code-surface/handle';
44
46
 
45
- interface CodeSurfaceProps {
47
+ export interface CodeSurfaceProps {
46
48
  value: string;
47
49
  onValueChange: (value: string) => void;
48
50
  /** Shiki language id, already resolved from the file name. */
49
51
  language: string;
50
52
  wordWrap: boolean;
53
+ /** Which cached `EditorState` this document owns — a tab switch applies another. */
54
+ documentKey: string;
51
55
  /** Find matches (document offsets) to paint; the current one is marked. */
52
- marks?: readonly FindMatch[];
56
+ marks?: readonly MarkRange[];
53
57
  currentMark?: number;
54
- /**
55
- * Extra ranges the ⌘D workflow has selected. The textarea can only hold one
56
- * selection, so the surface paints the others from here — and it must, or
57
- * ⌘D would look like it did nothing.
58
- */
59
- occurrences?: readonly TextRange[];
60
58
  /** Commands the editor does not own (find bar chords, word wrap, save). */
61
59
  onCommand?: (command: EditorCommand) => void;
62
- /** Called with the extra ranges whenever ⌘D / ⌘⇧L change them. */
63
- onOccurrencesChange?: (ranges: readonly TextRange[]) => void;
64
- /** Outer flex row that holds the gutter and the editor column. */
65
- rootClassName?: string;
66
- gutterClassName?: string;
67
- gutterStyle?: CSSProperties;
68
- /** Class on each gutter line number. */
69
- gutterLineClassName?: string;
70
- /** Column wrapping the CodeEditor (padding/overflow live here). */
71
- editorWrapperClassName?: string;
72
- editorClassName?: string;
73
- editorPadding?: number;
74
- editorStyle?: CSSProperties;
75
- }
76
-
77
- /**
78
- * The root must be as tall as the DOCUMENT, never as tall as the scroller it
79
- * sits in.
80
- *
81
- * Both callers put this surface inside a scroll container, and the desktop
82
- * editor's is `display: flex` — so the default `align-items: stretch` sized the
83
- * root to the container's viewport height while its content (the highlighted
84
- * `<pre>`, one box per line) overflowed it. The gutter is a stretched row of
85
- * that root, so its `bg-canvas` band and its `border-r` rule ended after one
86
- * viewport's worth of lines: scrolling a long file left the line numbers
87
- * painting over the scroller's own background, with the divider gone. Measured
88
- * on a 368-line file in a 770px pane — the gutter's box stayed 770px tall at
89
- * every scroll offset while the document was 10 218px.
90
- *
91
- * `align-self: flex-start` is what escapes the stretch; `min-height: 100%` is
92
- * the floor that keeps a SHORT document's gutter full-height. Together they
93
- * give the box the height the phone's layout already had (`min-h-full` inside a
94
- * non-flex scroller), so the two layouts no longer differ here — and the rule
95
- * lives in the component, because it is a property of the surface, not of a
96
- * caller's chrome. `align-self` is inert outside a flex parent.
97
- */
98
- const ROOT_STYLE: CSSProperties = { alignSelf: 'flex-start', minHeight: '100%' };
99
-
100
- /** Same measurement, so a re-render does not resize every gutter row. */
101
- function sameWrappedLines(previous: WrappedLines | null, next: WrappedLines | null): boolean {
102
- if (previous === next) return true;
103
- if (!previous || !next || previous.rowHeight !== next.rowHeight) return false;
104
- return (
105
- previous.heights.length === next.heights.length &&
106
- previous.heights.every((height, index) => height === next.heights[index])
107
- );
60
+ /** A viewer: both `readOnly` and `editable(false)`, so nothing can write. */
61
+ readOnly?: boolean;
62
+ /**
63
+ * The editor THEME, as CSS custom properties (`editorThemeVars`).
64
+ *
65
+ * Written on the host element, so the chrome repaints through CSS and a theme
66
+ * switch costs one attribute write. Empty/absent means `auto`: every
67
+ * declaration in `theme.ts` falls back to the app palette.
68
+ */
69
+ themeVars?: Record<string, string>;
70
+ /** The two Shiki theme names to tokenize with (the same theme twice when the
71
+ * editor theme is explicit). Defaults to the app's dual pair. */
72
+ themes?: ShikiThemeNames;
73
+ /** Font family / size / line height (the zoom control and the font setting). */
74
+ style?: CSSProperties;
75
+ /** Padding around the document, in px. */
76
+ padding?: number;
77
+ /** `data-testid` for the scroller, so the e2e layer can address it. */
78
+ scrollTestId?: string;
79
+ /**
80
+ * The element that actually scrolls, reported on mount and cleared on unmount.
81
+ *
82
+ * It exists only once the view does, and it is CodeMirror's (`view.scrollDOM`)
83
+ * — a caller that needs to remember the reader's position (`useScrollRestore`)
84
+ * cannot reach it through a `ref`, so the surface hands it over.
85
+ */
86
+ onScroller?: (element: HTMLElement | null) => void;
87
+ className?: string;
108
88
  }
109
89
 
110
- /**
111
- * The line-number gutter + syntax-highlighted editing surface shared by the
112
- * desktop editor and the phone's full-screen editor. Callers own only the
113
- * layout classes (padding, font, zoom) that genuinely differ per layout; the
114
- * gutter, word-wrap behaviour and Shiki highlighting live here so the two
115
- * surfaces cannot drift.
116
- *
117
- * The gutter spaces its numbers from the editor's measured wrap layout: with
118
- * word wrap on, a long line covers several rows and every number below it has
119
- * to move down by the same amount.
120
- *
121
- * A long document is drawn lazily: only the lines inside the scroll container's
122
- * viewport are highlighted and numbered, and the space of everything else is
123
- * reserved from the same geometry the caret is aligned to. That keeps opening a
124
- * 20 000-line file from spending minutes in the tokenizer, and a keystroke in it
125
- * from re-tokenizing the file at all.
126
- */
127
- export const CodeSurface = forwardRef<CodeSurfaceHandle, CodeSurfaceProps>(function CodeSurface({
128
- value,
129
- onValueChange,
130
- language,
131
- wordWrap,
132
- marks,
133
- currentMark,
134
- occurrences,
135
- onCommand,
136
- onOccurrencesChange,
137
- rootClassName,
138
- gutterClassName,
139
- gutterStyle,
140
- gutterLineClassName,
141
- editorWrapperClassName,
142
- editorClassName,
143
- editorPadding = 0,
144
- editorStyle,
145
- }, ref) {
146
- const wrapClass = wordWrap ? WRAP_ON : WRAP_OFF;
147
- const preRef = useRef<HTMLPreElement | null>(null);
148
- const rootRef = useRef<HTMLDivElement | null>(null);
149
- const editorRef = useRef<CodeEditorHandle | null>(null);
150
- const [wrapped, setWrapped] = useState<WrappedLines | null>(null);
151
- const [rowHeight, setRowHeight] = useState<number | null>(null);
152
- /** Lines the last measurement pass described; -1 before the first one runs. */
153
- const [measuredLines, setMeasuredLines] = useState(-1);
154
- useSyntaxReady();
90
+ export const CodeSurface = forwardRef<CodeSurfaceHandle, CodeSurfaceProps>(function CodeSurface(
91
+ {
92
+ value,
93
+ onValueChange,
94
+ language,
95
+ wordWrap,
96
+ documentKey,
97
+ marks,
98
+ currentMark,
99
+ onCommand,
100
+ readOnly = false,
101
+ themeVars,
102
+ themes,
103
+ style,
104
+ padding = 16,
105
+ scrollTestId,
106
+ onScroller,
107
+ className,
108
+ },
109
+ ref,
110
+ ) {
111
+ const hostRef = useRef<HTMLDivElement | null>(null);
112
+ const viewRef = useRef<EditorView | null>(null);
113
+ /** The document the live state holds, so a switch can park it under the right key. */
114
+ const docKeyRef = useRef<string>('');
115
+ /** The last mark signature applied, so a re-render does not re-dispatch it. */
116
+ const markSignatureRef = useRef<string>('');
117
+ /**
118
+ * True while this component writes the buffer itself.
119
+ *
120
+ * Load-bearing: the update listener reports every document change upward, and
121
+ * an external sync (a file re-read, a tab switch) must not be reported back as
122
+ * the user's edit — the write path would then arm an autosave for a change
123
+ * nobody typed.
124
+ */
125
+ const writingRef = useRef(false);
126
+ /**
127
+ * The text the view last REPORTED (or last wrote), so the per-render sync can
128
+ * skip without materializing the document.
129
+ *
130
+ * `syncExternal` used to compare `value` against `doc.toString()`, which is a
131
+ * full stringify on every render — and the parent re-renders on every
132
+ * keystroke, so a large file paid it twice per character. Measured in Bun:
133
+ * `toString()` is 7.2 ms at 20 MB, 13.7 ms at 30 MB, and the browser showed
134
+ * the knee — 34 ms per keystroke at 20 MB, 67 ms at 30, 83 ms at 40. The
135
+ * parent's `value` is the very string this component handed it, so the usual
136
+ * comparison is one reference check.
137
+ */
138
+ const lastValueRef = useRef(value);
155
139
 
156
- const lineCount = useMemo(() => countLines(value), [value]);
140
+ // Live mirrors: the state's own extensions close over these, and rebuilding
141
+ // the state because a callback identity moved would drop the undo history.
142
+ const onChangeRef = useRef(onValueChange);
143
+ onChangeRef.current = onValueChange;
144
+ const onCommandRef = useRef(onCommand);
145
+ onCommandRef.current = onCommand;
146
+ const onScrollerRef = useRef(onScroller);
147
+ onScrollerRef.current = onScroller;
148
+ /** The marks as of the last render, for the re-apply a document switch owes. */
149
+ const marksRef = useRef<readonly MarkRange[]>(marks ?? []);
150
+ const currentMarkRef = useRef(currentMark ?? -1);
151
+ marksRef.current = marks ?? [];
152
+ currentMarkRef.current = currentMark ?? -1;
153
+ /** The tokenizer themes, mirrored for the cache's first build. */
154
+ const activeThemes = themes ?? SHIKI_THEMES;
155
+ const themesRef = useRef<ShikiThemeNames>(activeThemes);
156
+ themesRef.current = activeThemes;
157
157
 
158
- // Re-measured on the inputs that move a wrap point: the text, the wrap
159
- // preference, and the editor's font size / padding.
160
- const measure = useCallback(() => {
161
- const pre = preRef.current;
162
- if (!pre) return;
163
- // Wrapped heights are only needed with word wrap on; without it every line
164
- // is one row and the row height alone describes the document.
165
- const next = wordWrap ? measureWrappedLines(pre, value) : null;
166
- setWrapped((previous) => (sameWrappedLines(previous, next) ? previous : next));
167
- const height = next?.rowHeight ?? measureRowHeight(pre);
168
- setRowHeight((previous) => (previous === height ? previous : height));
169
- setMeasuredLines(lineCount);
170
- // eslint-disable-next-line react-hooks/exhaustive-deps
171
- }, [value, lineCount, wordWrap, editorPadding, editorStyle?.fontSize]);
158
+ // One cache for the component's life: it owns the per-document states.
159
+ const cacheRef = useRef<DocumentCache | null>(null);
160
+ if (!cacheRef.current) {
161
+ cacheRef.current = createDocumentCache({
162
+ wordWrap,
163
+ readOnly,
164
+ themes: themesRef.current,
165
+ onDocChange: (next) => {
166
+ // Recorded BEFORE the writing guard: this is what the document holds,
167
+ // whether or not the change came from the user.
168
+ lastValueRef.current = next;
169
+ if (writingRef.current) return;
170
+ onChangeRef.current(next);
171
+ },
172
+ onState: (state) => {
173
+ const key = docKeyRef.current;
174
+ if (key) cacheRef.current?.remember(key, state);
175
+ },
176
+ onCommand: (command) => onCommandRef.current?.(command),
177
+ });
178
+ }
172
179
 
180
+ // Mount once. Every later change is a state swap or a dispatch — never a
181
+ // second view, which would drop the undo history the cache exists to keep.
173
182
  useLayoutEffect(() => {
174
- measure();
175
- }, [measure]);
176
-
177
- // The wrap width also changes without a re-render — dragging the panel
178
- // splitter, resizing the window, a zoom that reflows — so the editor's own
179
- // box drives a re-measure too.
180
- const measureRef = useRef(measure);
181
- measureRef.current = measure;
182
- useEffect(() => {
183
- const pre = preRef.current;
184
- if (!pre || typeof ResizeObserver === 'undefined') return;
185
- const observer = new ResizeObserver(() => measureRef.current());
186
- observer.observe(pre);
187
- return () => observer.disconnect();
188
- }, []);
183
+ const host = hostRef.current;
184
+ const cache = cacheRef.current;
185
+ if (!host || !cache) return;
189
186
 
190
- const geometry = useMemo<LineGeometry | null>(() => {
191
- if (rowHeight === null) return null;
192
- return wrapped
193
- ? measuredGeometry(wrapped.heights, wrapped.rowHeight, lineCount)
194
- : uniformGeometry(rowHeight, lineCount);
195
- }, [rowHeight, wrapped, lineCount]);
196
-
197
- // Windowing costs a scroll listener and a spacer; a document short enough to
198
- // draw in one pass keeps the plain surface.
199
- const lazy = shouldWindow(lineCount, value.length);
200
- const virtual = useCodeWindow({
201
- rootRef,
202
- lineCount,
203
- geometry,
204
- // A document whose geometry could not be measured still falls back to the
205
- // plain surface — but one whose measurement has not run *yet* must not:
206
- // that path would draw the whole file, which is what windowing exists to
207
- // avoid. `measuredLines` is why switching files (which leaves a geometry
208
- // for the previous document behind) does not take it either.
209
- enabled: lazy && (measuredLines !== lineCount || geometry !== null),
210
- });
211
-
212
- /**
213
- * What the highlight layer paints: the find matches, plus every extra ⌘D
214
- * range. The find widget's current match is the one the caret is aimed at, so
215
- * it keeps `currentMark` — an occurrence range never claims that role.
216
- */
217
- const painted = useMemo<readonly FindMatch[]>(() => {
218
- if (!occurrences || occurrences.length === 0) return marks ?? [];
219
- return [...(marks ?? []), ...occurrences];
220
- }, [marks, occurrences]);
221
- /** Where the extra selections start inside `painted`; -1 when there are none. */
222
- const occurrenceFrom = occurrences && occurrences.length > 0 ? (marks?.length ?? 0) : -1;
187
+ docKeyRef.current = documentKey;
188
+ const view = new EditorView({
189
+ state: cache.stateFor(documentKey, language, value),
190
+ parent: host,
191
+ });
192
+ viewRef.current = view;
193
+ lastValueRef.current = value;
194
+ if (scrollTestId) view.scrollDOM.dataset.testid = scrollTestId;
195
+ onScrollerRef.current?.(view.scrollDOM);
223
196
 
224
- const highlight = useCallback(
225
- (code: string, range: LineWindow | null) =>
226
- range
227
- ? highlightCodeWindow(code, language, range, { marks: painted, currentMark, occurrenceFrom })
228
- : highlightCode(code, language, { marks: painted, currentMark, occurrenceFrom }),
229
- [language, painted, currentMark, occurrenceFrom],
230
- );
197
+ return () => {
198
+ onScrollerRef.current?.(null);
199
+ view.destroy();
200
+ viewRef.current = null;
201
+ };
202
+ // Mount only: the effects below own every later change, and re-running this
203
+ // one would rebuild the view (and its history).
204
+ // eslint-disable-next-line react-hooks/exhaustive-deps
205
+ }, []);
231
206
 
232
- /**
233
- * Bring `offset`'s row into view without touching the caret.
234
- *
235
- * `scrollIntoView` cannot be used: the caret's row and the match's row are
236
- * different elements (the highlighted `<pre>` is `aria-hidden` and
237
- * `pointer-events: none`), and the browser's own scroll-into-view on
238
- * `setSelectionRange` would fight this. Instead the row's pixel offset comes
239
- * from the same geometry the window is resolved against, so the two agree by
240
- * construction, and the scroll is a plain assignment on the container the
241
- * window reads.
242
- *
243
- * A reveal is REPLAYED while the geometry is still moving, and that is
244
- * load-bearing rather than defensive. Opening the editor panel animates its
245
- * width, so the document is measured once at each intermediate width: a
246
- * reveal that scrolled with the first (narrow) measurement put line 800 of a
247
- * 900-line file near the bottom, and the settled, wider measurement then left
248
- * the scroller clamped there — showing the file's tail with the hit's own
249
- * highlight unpainted.
250
- */
251
- const pendingRevealRef = useRef<{ offset: number; until: number } | null>(null);
207
+ // A different document: park the live state, then apply the other one.
208
+ useLayoutEffect(() => {
209
+ const view = viewRef.current;
210
+ const cache = cacheRef.current;
211
+ if (!view || !cache) return;
212
+ if (docKeyRef.current === documentKey) return;
213
+ cache.remember(docKeyRef.current, view.state);
214
+ docKeyRef.current = documentKey;
215
+ view.setState(cache.stateFor(documentKey, language, value));
216
+ // The cached state carries the configuration it was CREATED with, and
217
+ // `setState` installs that wholesale — so the current wrap/read-only pair
218
+ // and the current editor theme are re-applied, or a tab opened while wrap
219
+ // was off comes back unwrapped and a theme chosen since comes back stale.
220
+ view.dispatch({ effects: cache.currentEffects(documentKey) });
221
+ syncExternal(view, value, writingRef, lastValueRef);
222
+ // The marks belonged to the previous document's offsets, and the field lives
223
+ // in that state. A fresh state starts empty, so this re-applies whatever the
224
+ // new document's owner asked for rather than leaving a stale set behind.
225
+ markSignatureRef.current = '';
226
+ view.dispatch({
227
+ effects: setFindMarks.of({ marks: marksRef.current, current: currentMarkRef.current }),
228
+ });
229
+ // `value` is deliberately absent: this effect is about the KEY moving, and
230
+ // the external sync below covers a document whose bytes were replaced.
231
+ // eslint-disable-next-line react-hooks/exhaustive-deps
232
+ }, [documentKey]);
252
233
 
253
- const applyReveal = useCallback(
254
- (offset: number) => {
255
- const root = rootRef.current;
256
- if (!root || !geometry) return;
257
- const rowTop = geometry.offsetOf(lineIndexAt(value, offset));
258
- const container = findScrollContainer(root);
259
- if (!container) return;
260
- const frameTop = root.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
261
- const band = container.clientHeight;
262
- const target = frameTop + rowTop;
263
- // Only scroll when the row is actually outside the band; a reveal that
264
- // always re-centers would make every "next match" jump the whole view.
265
- if (target < container.scrollTop + 8 || target + geometry.rowHeight > container.scrollTop + band - 8) {
266
- container.scrollTop = Math.max(0, target - band / 3);
267
- }
268
- },
269
- [geometry, value],
270
- );
234
+ // The parent replaced the buffer (a re-read, a refresh, a replace-all).
235
+ useLayoutEffect(() => {
236
+ const view = viewRef.current;
237
+ if (!view || docKeyRef.current !== documentKey) return;
238
+ syncExternal(view, value, writingRef, lastValueRef);
239
+ }, [value, documentKey]);
271
240
 
272
- const revealOffset = useCallback(
273
- (offset: number) => {
274
- // The window covers the panel's 200ms width transition plus a margin; a
275
- // user scrolling within it is not a case this feature has to serve.
276
- pendingRevealRef.current = { offset, until: performance.now() + 600 };
277
- applyReveal(offset);
278
- },
279
- [applyReveal],
280
- );
241
+ // Wrap, read-only and the editor theme are per-view settings: reconfiguring
242
+ // keeps the document, the selection and the history, which is what a wrap
243
+ // toggle — or a colour change — must not disturb.
244
+ useLayoutEffect(() => {
245
+ const view = viewRef.current;
246
+ const cache = cacheRef.current;
247
+ if (!view || !cache) return;
248
+ const effects = cache.optionsEffects({ wordWrap, readOnly, themes: activeThemes }, docKeyRef.current);
249
+ if (effects.length > 0) view.dispatch({ effects });
250
+ }, [wordWrap, readOnly, activeThemes.light, activeThemes.dark]);
281
251
 
282
- // Re-apply the pending reveal as the geometry moves, and drop it once the
283
- // document has settled (or the window closed).
252
+ // Find marks: a field write, never a re-tokenize.
284
253
  useLayoutEffect(() => {
285
- const pending = pendingRevealRef.current;
286
- if (!pending) return;
287
- if (performance.now() > pending.until) {
288
- pendingRevealRef.current = null;
289
- return;
290
- }
291
- applyReveal(pending.offset);
292
- }, [applyReveal]);
254
+ const view = viewRef.current;
255
+ if (!view) return;
256
+ const list = marks ?? [];
257
+ const signature = `${currentMark ?? -1}:${list.map((mark) => `${mark.start}-${mark.end}`).join(',')}`;
258
+ if (signature === markSignatureRef.current) return;
259
+ markSignatureRef.current = signature;
260
+ view.dispatch({ effects: setFindMarks.of({ marks: list, current: currentMark ?? -1 }) });
261
+ }, [marks, currentMark]);
293
262
 
294
- useImperativeHandle(
295
- ref,
296
- () => ({
297
- getSelection: () => editorRef.current?.getSelection() ?? null,
298
- select: (start, end, focus = true) => editorRef.current?.select(start, end, focus),
299
- applyDocument: (next, caretStart, caretEnd) => editorRef.current?.applyDocument(next, caretStart, caretEnd),
300
- runCommand: (command) => editorRef.current?.run(command),
301
- revealOffset,
302
- }),
303
- [revealOffset],
304
- );
263
+ // Built once: the handle closes over refs, so a consumer holding it never
264
+ // re-subscribes, and the view it reaches is whatever is live at the call.
265
+ useImperativeHandle(ref, () => createSurfaceHandle({ view: viewRef, onCommand: onCommandRef }), []);
305
266
 
306
- const firstLine = virtual ? virtual.start : 0;
307
- const lastLine = virtual ? Math.min(virtual.end, lineCount) : lineCount;
308
- const rows: number[] = [];
309
- for (let i = firstLine; i < lastLine; i++) rows.push(i);
310
- // Row heights are only usable for the document they were measured against.
311
- const lineHeights = wrapped && wrapped.heights.length === lineCount ? wrapped.heights : null;
267
+ const hostStyle = {
268
+ ...themeVars,
269
+ ...style,
270
+ // The document's padding, read back by the theme as a CSS variable so a
271
+ // per-layout value needs no theme reconfigure.
272
+ '--surface-pad': `${padding}px`,
273
+ } as CSSProperties;
312
274
 
313
275
  return (
314
- <div className={rootClassName} style={ROOT_STYLE} ref={rootRef}>
315
- <div className={gutterClassName} style={gutterStyle}>
316
- {virtual && virtual.top > 0 ? <div style={{ height: virtual.top, flexShrink: 0 }} aria-hidden="true" /> : null}
317
- {rows.map((index) => (
318
- <div
319
- key={index + 1}
320
- className={gutterLineClassName}
321
- style={lineHeights ? { height: lineHeights[index], flexShrink: 0 } : undefined}
322
- >
323
- {index + 1}
324
- </div>
325
- ))}
326
- </div>
327
- <div className={editorWrapperClassName}>
328
- <CodeEditor
329
- value={value}
330
- onValueChange={onValueChange}
331
- highlight={highlight}
332
- padding={editorPadding}
333
- preRef={preRef}
334
- handleRef={editorRef}
335
- textareaClassName={`focus:outline-none ${wrapClass}`}
336
- preClassName={wrapClass}
337
- style={editorStyle}
338
- className={editorClassName}
339
- virtual={virtual}
340
- language={language}
341
- occurrences={occurrences}
342
- onCommand={onCommand}
343
- onOccurrencesChange={onOccurrencesChange}
344
- />
345
- </div>
346
- </div>
276
+ <div
277
+ ref={hostRef}
278
+ className={className}
279
+ style={hostStyle}
280
+ // The panel's drop handler claims a file drop in the CAPTURE phase when
281
+ // the drag is over this element: CodeMirror's own `drop` handler inserts a
282
+ // dropped file's TEXT into the document, which would rewrite the open file
283
+ // on the next autosave. See `use-editor-drop.ts`.
284
+ data-code-surface=""
285
+ />
347
286
  );
348
287
  });