@kabmo-labs/kabmo 0.8.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 (254) hide show
  1. package/README.md +7 -0
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/index.js +314 -314
  4. package/dist/client/static/js/{index-qwpvd9fv.js → index-bpptfkz2.js} +922 -892
  5. package/dist/client/static/js/index-k315p1e8.css +1 -0
  6. package/package.json +9 -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/shell-update-button/Modal.tsx +163 -0
  24. package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
  25. package/src/client/components/common/shell-update-button/index.tsx +125 -0
  26. package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
  27. package/src/client/components/common/toolbar-row/index.tsx +189 -0
  28. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
  29. package/src/client/components/layout/desktop-layout/index.tsx +47 -67
  30. package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
  31. package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
  32. package/src/client/components/mobile/LayoutWrapper.tsx +1 -0
  33. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
  34. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
  35. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
  36. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
  37. package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
  38. package/src/client/components/settings/Modal.tsx +8 -0
  39. package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
  40. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
  41. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
  42. package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +8 -2
  43. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +16 -8
  44. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
  45. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
  46. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
  47. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
  48. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
  49. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
  50. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
  51. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
  52. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
  53. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
  54. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
  55. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +8 -11
  56. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
  57. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
  58. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
  59. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
  60. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
  62. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
  63. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
  64. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
  65. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
  66. package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
  67. package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
  68. package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
  69. package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
  70. package/src/client/components/workspace/diff-panel/index.tsx +76 -145
  71. package/src/client/components/workspace/diff-panel/merge.ts +315 -0
  72. package/src/client/components/workspace/editor/Body.tsx +4 -7
  73. package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
  74. package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
  75. package/src/client/components/workspace/editor/FilePane.tsx +4 -7
  76. package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
  77. package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
  78. package/src/client/components/workspace/editor/index.tsx +7 -9
  79. package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
  80. package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
  81. package/src/client/components/workspace/search-panel/Body.tsx +1 -1
  82. package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
  83. package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
  84. package/src/client/hooks/chat/omp/reconnect.ts +187 -0
  85. package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
  86. package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
  87. package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
  88. package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
  89. package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
  90. package/src/client/hooks/chat/omp/session-list.tsx +69 -9
  91. package/src/client/hooks/chat/timeline/generating.ts +19 -2
  92. package/src/client/hooks/chat/timeline/index.ts +19 -24
  93. package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
  94. package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
  95. package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
  96. package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
  97. package/src/client/hooks/chat/timeline/session-load.ts +69 -37
  98. package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
  99. package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
  100. package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
  101. package/src/client/hooks/code/deferred-highlight.ts +129 -0
  102. package/src/client/hooks/editor/find-state.ts +1 -1
  103. package/src/client/hooks/editor/theme.test.ts +206 -0
  104. package/src/client/hooks/editor/theme.ts +82 -0
  105. package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
  106. package/src/client/hooks/editor/use-editor-drop.ts +67 -14
  107. package/src/client/hooks/editor/use-editor-find.ts +7 -7
  108. package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
  109. package/src/client/hooks/editor/use-outline-view.ts +94 -7
  110. package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
  111. package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
  112. package/src/client/hooks/editor/use-search-reveal.ts +1 -1
  113. package/src/client/hooks/ui/settings-modal.ts +56 -0
  114. package/src/client/hooks/ui/updates/shell-download.test.ts +2 -0
  115. package/src/client/hooks/ui/updates/shell-download.ts +55 -14
  116. package/src/client/hooks/ui/updates/shell-offer.test.ts +26 -0
  117. package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
  118. package/src/client/hooks/ui/updates/shell-update.ts +119 -0
  119. package/src/client/hooks/workspace/diff-file.ts +29 -10
  120. package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
  121. package/src/client/hooks/workspace/editor-view.ts +61 -0
  122. package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
  123. package/src/client/hooks/workspace/file-tabs.ts +24 -11
  124. package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
  125. package/src/client/hooks/workspace/worktree/index.ts +20 -5
  126. package/src/client/lib/desktop/bridge.ts +45 -0
  127. package/src/client/lib/search-reveal.ts +1 -1
  128. package/src/client/main.tsx +5 -1
  129. package/src/client/tailwind.css +37 -8
  130. package/src/server/lib/auth/guard.test.ts +28 -0
  131. package/src/server/lib/auth/guard.ts +10 -0
  132. package/src/server/lib/fs/worktree/create.ts +2 -3
  133. package/src/server/lib/fs/worktree/list.ts +13 -0
  134. package/src/server/lib/fs/worktree/remove.ts +3 -3
  135. package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
  136. package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
  137. package/src/server/lib/lifecycle/port-guard.ts +39 -7
  138. package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
  139. package/src/server/lib/lifecycle/probe.test.ts +77 -164
  140. package/src/server/lib/lifecycle/probe.ts +121 -13
  141. package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
  142. package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
  143. package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
  144. package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
  145. package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
  146. package/src/server/lib/omp/session/telemetry.test.ts +33 -0
  147. package/src/server/lib/realtime/signals.server.ts +10 -0
  148. package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
  149. package/src/server/lib/realtime/topics.server.ts +16 -0
  150. package/src/server/lib/updates/shell-assets.ts +94 -3
  151. package/src/server/lib/updates/shell-cache.ts +105 -0
  152. package/src/server/lib/updates/shell-pages.test.ts +33 -2
  153. package/src/server/lib/updates/shell-pages.ts +17 -5
  154. package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
  155. package/src/server/lib/updates/shell-release.test.ts +10 -27
  156. package/src/server/lib/updates/shell-release.ts +107 -111
  157. package/src/server/routes/fs/git-merge.test.ts +11 -1
  158. package/src/server/routes/fs/read.test.ts +24 -7
  159. package/src/server/routes/fs/scope.test.ts +15 -10
  160. package/src/server/routes/fs/scope.ts +3 -14
  161. package/src/server/routes/updates/android.ts +6 -1
  162. package/src/server/routes/updates/desktop.ts +29 -7
  163. package/src/server/routes/updates/ios.ts +5 -1
  164. package/src/server/routes/worktrees/list.ts +11 -1
  165. package/src/server/routes/worktrees/routes.test.ts +17 -0
  166. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
  167. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
  168. package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
  169. package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
  170. package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
  171. package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
  172. package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
  173. package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
  174. package/src/shared/lib/code/editor/find-scan.ts +177 -0
  175. package/src/shared/lib/code/editor/keymap.ts +3 -3
  176. package/src/shared/lib/code/editor/lines.test.ts +7 -55
  177. package/src/shared/lib/code/editor/lines.ts +8 -48
  178. package/src/shared/lib/code/editor/marks.test.ts +4 -5
  179. package/src/shared/lib/code/editor/marks.ts +13 -25
  180. package/src/shared/lib/code/editor/theme-loader.ts +97 -0
  181. package/src/shared/lib/code/editor/themes.generated.ts +156 -0
  182. package/src/shared/lib/code/editor/themes.test.ts +165 -0
  183. package/src/shared/lib/code/editor/themes.ts +165 -0
  184. package/src/shared/lib/code/highlight-memo.test.ts +103 -0
  185. package/src/shared/lib/code/highlight-memo.ts +149 -0
  186. package/src/shared/lib/code/highlighter-warmup.ts +112 -0
  187. package/src/shared/lib/code/highlighter.ts +3 -1
  188. package/src/shared/lib/code/idle.ts +37 -0
  189. package/src/shared/lib/code/outline/rows.test.ts +96 -0
  190. package/src/shared/lib/code/outline/rows.ts +94 -0
  191. package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
  192. package/src/shared/lib/code/syntax-highlight.ts +2 -3
  193. package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
  194. package/src/shared/lib/fs/file-kind.ts +47 -13
  195. package/src/shared/lib/fs/file-url.test.ts +52 -0
  196. package/src/shared/lib/fs/file-url.ts +37 -0
  197. package/src/shared/lib/fs/patch-sides.test.ts +114 -0
  198. package/src/shared/lib/fs/patch-sides.ts +139 -0
  199. package/src/shared/lib/realtime/protocol.ts +16 -0
  200. package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
  201. package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
  202. package/src/shared/lib/updates/shell-arch.ts +54 -0
  203. package/src/shared/lib/updates/shell-ua.ts +4 -4
  204. package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
  205. package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
  206. package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
  207. package/src/shared/lib/workspace/panel-widths.ts +25 -5
  208. package/src/shared/lib/workspace/session-state/cache.ts +159 -0
  209. package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
  210. package/src/shared/lib/workspace/session-state/merge.ts +100 -0
  211. package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
  212. package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
  213. package/src/shared/lib/workspace/session-state/store.ts +92 -130
  214. package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
  215. package/src/shared/types/realtime.ts +29 -0
  216. package/src/shared/types/settings/state.ts +10 -0
  217. package/src/test-support/loopback.ts +183 -0
  218. package/dist/client/static/js/index-gtbpewr7.css +0 -1
  219. package/src/client/components/common/code-editor/handler.test.ts +0 -274
  220. package/src/client/components/common/code-editor/handler.ts +0 -175
  221. package/src/client/components/common/code-editor/index.tsx +0 -314
  222. package/src/client/components/common/code-surface/measure.ts +0 -148
  223. package/src/client/components/workspace/chat-timeline/target-picker/index.test.tsx +0 -227
  224. package/src/client/components/workspace/chat-timeline/target-picker/index.tsx +0 -260
  225. package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
  226. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
  227. package/src/client/hooks/editor/scroll-container.test.ts +0 -102
  228. package/src/client/hooks/editor/scroll-container.ts +0 -26
  229. package/src/client/hooks/editor/use-code-window.ts +0 -100
  230. package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
  231. package/src/client/hooks/editor/use-editor-history.ts +0 -134
  232. package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
  233. package/src/client/hooks/editor/use-occurrences.ts +0 -98
  234. package/src/shared/lib/code/editor/commands.test.ts +0 -155
  235. package/src/shared/lib/code/editor/commands.ts +0 -260
  236. package/src/shared/lib/code/editor/edits.test.ts +0 -100
  237. package/src/shared/lib/code/editor/edits.ts +0 -122
  238. package/src/shared/lib/code/editor/history.test.ts +0 -118
  239. package/src/shared/lib/code/editor/history.ts +0 -101
  240. package/src/shared/lib/code/editor/layers.test.ts +0 -77
  241. package/src/shared/lib/code/editor/layers.ts +0 -67
  242. package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
  243. package/src/shared/lib/code/editor/line-edits.ts +0 -159
  244. package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
  245. package/src/shared/lib/code/editor/occurrences.ts +0 -111
  246. package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
  247. package/src/shared/lib/code/editor/reset-css.ts +0 -32
  248. package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
  249. package/src/shared/lib/code/editor/text-commands.ts +0 -154
  250. package/src/shared/lib/code/lazy-window.test.ts +0 -161
  251. package/src/shared/lib/code/lazy-window.ts +0 -156
  252. package/src/shared/lib/code/windowed-highlight.ts +0 -114
  253. package/src/shared/lib/fs/diff-parser.test.ts +0 -116
  254. package/src/shared/lib/fs/diff-parser.ts +0 -213
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * A bounded memo in front of the syntax highlighter, for BLOCK highlights — the
8
+ * code a tool card draws when it opens.
9
+ *
10
+ * Shiki tokenization is the expensive half of rendering a tool card: profiled on
11
+ * a throttled phone, a `read` expand spent 75% of its long task in
12
+ * `findNextMatchSync`/the grammar engine, and the same card re-expanded in 33 ms
13
+ * once its grammar was warm. Two consequences follow, and this module is the
14
+ * second half of both (the first is `idle.ts`, which moves the work off the tap):
15
+ *
16
+ * 1. A card that is closed and reopened is the same code and the same language,
17
+ * so its markup is the same string. Tokenizing it again is pure waste, and a
18
+ * reader paging through a transcript does exactly that repeatedly.
19
+ * 2. `Edit` asked for three highlights per render (old, new, preview) when at
20
+ * most one of them is ever drawn. Caching per `(language, code)` makes the
21
+ * duplicates free even before the call sites are gated.
22
+ *
23
+ * The key is a two-level map — language, then the code string itself — rather
24
+ * than one composite string, because the composite would COPY the whole file on
25
+ * every lookup, which is exactly the per-frame allocation this exists to remove.
26
+ * Insertion order is the LRU order.
27
+ *
28
+ * Nothing is cached while the language's grammar has not landed: `highlightCode`
29
+ * answers escaped text in that state, and caching it would pin the plain
30
+ * fallback for the life of the entry (the trap `highlight-cache.ts` documents
31
+ * for search rows). `useSyntaxReady` re-runs the caller when a grammar arrives,
32
+ * which is when the entry finally becomes cacheable.
33
+ */
34
+
35
+ import { isLanguageReady } from '@/shared/lib/code/highlighter-lazy';
36
+ import { highlightCode, highlightLines, type HighlightLinesOptions } from '@/shared/lib/code/syntax-highlight';
37
+
38
+ /** Per language: a transcript page's worth of distinct blocks. */
39
+ const MAX_BLOCKS_PER_LANGUAGE = 24;
40
+ /** Across every language, so a long session cannot grow without bound. */
41
+ const MAX_CACHED_CHARS = 512 * 1024;
42
+
43
+ const blocks = new Map<string, Map<string, string>>();
44
+ let cachedChars = 0;
45
+
46
+ function bucket(language: string): Map<string, string> {
47
+ const existing = blocks.get(language);
48
+ if (existing) return existing;
49
+ const created = new Map<string, string>();
50
+ blocks.set(language, created);
51
+ return created;
52
+ }
53
+
54
+ /** Drop the oldest entry of the first language that has one. */
55
+ function evictOne(): boolean {
56
+ for (const entries of blocks.values()) {
57
+ const oldest = entries.keys().next();
58
+ if (oldest.done) continue;
59
+ const value = entries.get(oldest.value) ?? '';
60
+ entries.delete(oldest.value);
61
+ cachedChars -= value.length;
62
+ return true;
63
+ }
64
+ return false;
65
+ }
66
+
67
+ function remember(language: string, code: string, html: string): void {
68
+ const entries = bucket(language);
69
+ const previous = entries.get(code);
70
+ if (previous !== undefined) {
71
+ entries.delete(code);
72
+ cachedChars -= previous.length;
73
+ }
74
+ entries.set(code, html);
75
+ cachedChars += html.length;
76
+ while (entries.size > MAX_BLOCKS_PER_LANGUAGE) {
77
+ const oldest = entries.keys().next();
78
+ if (oldest.done) break;
79
+ const value = entries.get(oldest.value) ?? '';
80
+ entries.delete(oldest.value);
81
+ cachedChars -= value.length;
82
+ }
83
+ while (cachedChars > MAX_CACHED_CHARS && evictOne()) {
84
+ // Keep dropping until the budget holds or nothing is left to drop.
85
+ }
86
+ }
87
+
88
+ /** The cached markup for `(language, code)`, or `undefined`. */
89
+ export function cachedHighlightBlock(code: string, language: string): string | undefined {
90
+ const entries = blocks.get(language);
91
+ if (!entries) return undefined;
92
+ const hit = entries.get(code);
93
+ if (hit === undefined) return undefined;
94
+ // Reading is a use: move it to the most-recently-used end.
95
+ entries.delete(code);
96
+ entries.set(code, hit);
97
+ return hit;
98
+ }
99
+
100
+ /** The cached per-line markup, or `undefined`. */
101
+ export function cachedHighlightBlockLines(code: string, language: string): string[] | undefined {
102
+ const hit = cachedHighlightBlock(code, language);
103
+ return hit === undefined ? undefined : hit.split('\n');
104
+ }
105
+
106
+ function cacheable(language: string, options: HighlightLinesOptions): boolean {
107
+ // A find-in-progress needs the match markup, which only the line path emits —
108
+ // and which changes per query, so those results are never shared.
109
+ if (options.marks && options.marks.length > 0) return false;
110
+ return isLanguageReady(language);
111
+ }
112
+
113
+ /** `highlightCode` for one block, memoized per `(language, code)`. */
114
+ export function highlightBlock(code: string, language: string, options: HighlightLinesOptions = {}): string {
115
+ if (!code) return '';
116
+ const cached = options.marks?.length ? undefined : cachedHighlightBlock(code, language);
117
+ if (cached !== undefined) return cached;
118
+ const html = highlightCode(code, language, options);
119
+ if (cacheable(language, options)) remember(language, code, html);
120
+ return html;
121
+ }
122
+
123
+ /** `highlightLines` for one block, memoized per `(language, code)`. */
124
+ export function highlightBlockLines(
125
+ code: string,
126
+ language: string,
127
+ options: HighlightLinesOptions = {},
128
+ ): string[] {
129
+ if (!code) return [''];
130
+ const cached = options.marks?.length ? undefined : cachedHighlightBlockLines(code, language);
131
+ if (cached !== undefined) return cached;
132
+ const lines = highlightLines(code, language, options);
133
+ if (cacheable(language, options)) remember(language, code, lines.join('\n'));
134
+ return lines;
135
+ }
136
+
137
+ /** Drop every entry. For tests and for a highlighter restart (grammar reload). */
138
+ export function clearHighlightMemo(): void {
139
+ blocks.clear();
140
+ cachedChars = 0;
141
+ }
142
+
143
+ /** How many blocks are held — a hit and a recompute return equal strings, so
144
+ * the map is the only observable. */
145
+ export function highlightMemoSize(): number {
146
+ let total = 0;
147
+ for (const entries of blocks.values()) total += entries.size;
148
+ return total;
149
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Pay the tokenizer's start-up cost before the reader taps anything.
8
+ *
9
+ * `EAGER_LANGS` are loaded in the background at boot, but loading a grammar is
10
+ * not compiling it: the grammar's regexes are built and JIT-ed on the first
11
+ * tokenize, which is why the FIRST expand of a card in a given language measured
12
+ * 220–300 ms at 4× CPU while the same card re-expanded in 33 ms. That first
13
+ * expand is the one a reader actually experiences — a transcript opens with
14
+ * `read` and `bash` cards.
15
+ *
16
+ * So each eager grammar is tokenized once, on a tiny sample, while the main
17
+ * thread is idle. The sample's markup is discarded; the work being paid for is
18
+ * the compile behind it. One language per idle slice, so a slow grammar cannot
19
+ * become the long task this exists to prevent — and a grammar that has not
20
+ * landed yet is retried rather than skipped, because tokenizing before it
21
+ * arrives compiles nothing.
22
+ *
23
+ * Booted from `main.tsx` rather than from `highlighter.ts`: the warm-up needs
24
+ * the memo, which needs the highlighter, and an import in the other direction
25
+ * would close that cycle.
26
+ */
27
+
28
+ import { isLanguageReady } from '@/shared/lib/code/highlighter-lazy';
29
+ import { highlightBlock } from '@/shared/lib/code/highlight-memo';
30
+ import { scheduleIdle } from '@/shared/lib/code/idle';
31
+ import { EAGER_LANGS } from '@/shared/lib/code/shiki-langs';
32
+
33
+ /**
34
+ * A few lines that make the grammar walk its own rule set: a keyword, a string,
35
+ * a comment, a call and a block. The exact text does not matter — the compile
36
+ * does — so these are deliberately tiny.
37
+ */
38
+ const SAMPLES: Record<string, string> = {
39
+ typescript: '// warm\nconst value: number = 1;\nfunction read(input: string) {\n return input;\n}\n',
40
+ tsx: '// warm\nexport const View = ({ title }: { title: string }) => (\n <div className="row">{title}</div>\n);\n',
41
+ javascript: '// warm\nconst value = 1;\nfunction read(input) {\n return input;\n}\n',
42
+ jsx: '// warm\nexport const View = ({ title }) => (\n <div className="row">{title}</div>\n);\n',
43
+ json: '{\n "name": "warm",\n "items": [1, 2, 3],\n "ok": true\n}\n',
44
+ bash: '#!/bin/sh\n# warm\nset -e\nfor file in *.ts; do\n echo "$file"\ndone\n',
45
+ python: '# warm\ndef read(value: str) -> str:\n return value.strip()\n',
46
+ markdown: '# warm\n\nA paragraph with **bold** text and a `code` span.\n\n- item\n',
47
+ yaml: 'name: warm\nitems:\n - one\n - two\nenabled: true\n',
48
+ diff: '--- a/file.ts\n+++ b/file.ts\n@@ -1,2 +1,2 @@\n-old line\n+new line\n context\n',
49
+ };
50
+
51
+ const DEFAULT_SAMPLE = '// warm\nconst value = 1;\nfunction read(input) {\n return input;\n}\n';
52
+
53
+ /** How many idle slices a not-yet-loaded grammar is retried for. */
54
+ const MAX_WAIT_SLICES = 120;
55
+
56
+ export interface WarmOptions {
57
+ /** Languages to warm; defaults to the eager set. */
58
+ languages?: readonly string[];
59
+ /** The tokenizer; injected so a test can observe the schedule. */
60
+ highlight?: (code: string, language: string) => void;
61
+ /** Grammar readiness; injected for the same reason. */
62
+ ready?: (language: string) => boolean;
63
+ /** The idle scheduler; injected so a test can drive it synchronously. */
64
+ schedule?: (run: () => void) => () => void;
65
+ }
66
+
67
+ let started = false;
68
+
69
+ /**
70
+ * Warm every eager grammar once per page load, one per idle slice.
71
+ *
72
+ * Idempotent: a second call is a no-op, so a re-evaluated client entry cannot
73
+ * re-tokenize every language.
74
+ */
75
+ export function warmSyntaxTokenizers(options: WarmOptions = {}): void {
76
+ if (started) return;
77
+ if (typeof window === 'undefined') return;
78
+ started = true;
79
+
80
+ const languages = options.languages ?? EAGER_LANGS;
81
+ const highlight = options.highlight ?? ((code: string, language: string) => { highlightBlock(code, language); });
82
+ const ready = options.ready ?? isLanguageReady;
83
+ const schedule = options.schedule ?? ((run: () => void) => scheduleIdle(run));
84
+
85
+ const queue = [...languages];
86
+ let waiting = 0;
87
+ const step = () => {
88
+ const language = queue.shift();
89
+ if (language === undefined) return;
90
+ if (!ready(language)) {
91
+ // The grammar is still being fetched. Warming it now would compile
92
+ // nothing and the sample would not even be cacheable.
93
+ if (++waiting <= MAX_WAIT_SLICES) queue.push(language);
94
+ schedule(step);
95
+ return;
96
+ }
97
+ waiting = 0;
98
+ try {
99
+ highlight(SAMPLES[language] ?? DEFAULT_SAMPLE, language);
100
+ } catch {
101
+ // A grammar that cannot warm is not an error: the first real highlight
102
+ // falls back to escaped text and retries when the grammar lands.
103
+ }
104
+ schedule(step);
105
+ };
106
+ schedule(step);
107
+ }
108
+
109
+ /** Test/restart seam: forget that the warm-up already ran. */
110
+ export function resetTokenizerWarmup(): void {
111
+ started = false;
112
+ }
@@ -69,7 +69,9 @@ async function boot(): Promise<HighlighterCore | null> {
69
69
  notify();
70
70
  // Grammars arrive on demand (`ensureLanguage`); the eager set is only a
71
71
  // head start for the languages a chat session hits immediately, and boot
72
- // does not wait on it.
72
+ // does not wait on it. `main.tsx` pays the tokenizer's compile cost once
73
+ // the eager set has landed — loading a grammar is not compiling it, and the
74
+ // first card expand used to pay that compile inside the tap.
73
75
  void ensureLanguages(highlighter, EAGER_LANGS);
74
76
  return highlighter;
75
77
  } catch (error) {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Run work when the main thread is next free.
8
+ *
9
+ * Tokenizing a tool card's code is the expensive half of expanding it — measured
10
+ * at 730–1484 ms on a throttled phone for a `read`/`grep` card — and running it
11
+ * inside the render that a tap scheduled put all of that in the tap's own task:
12
+ * the card appeared only after the grammar had walked every line. Deferring it
13
+ * costs one plain-text frame and moves the work out of the interaction.
14
+ *
15
+ * `requestIdleCallback` is the browser's own "nothing else is waiting" signal;
16
+ * the timeout is a FLOOR on latency rather than a budget, so a busy page still
17
+ * paints the colour within a quarter second. Runtimes without it (happy-dom,
18
+ * older Safari) fall back to the next macrotask.
19
+ */
20
+
21
+ const IDLE_TIMEOUT_MS = 250;
22
+
23
+ type IdleHost = {
24
+ requestIdleCallback?: (callback: () => void, options?: { timeout: number }) => number;
25
+ cancelIdleCallback?: (handle: number) => void;
26
+ };
27
+
28
+ /** Schedule `run`; the returned function cancels it. */
29
+ export function scheduleIdle(run: () => void, timeoutMs: number = IDLE_TIMEOUT_MS): () => void {
30
+ const host = globalThis as unknown as IdleHost;
31
+ if (typeof host.requestIdleCallback === 'function') {
32
+ const handle = host.requestIdleCallback(run, { timeout: timeoutMs });
33
+ return () => host.cancelIdleCallback?.(handle);
34
+ }
35
+ const handle = setTimeout(run, 0);
36
+ return () => clearTimeout(handle);
37
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The outline's two bounds.
8
+ *
9
+ * Both exist because a large file's outline is not a long list, it is a DOM:
10
+ * measured on a 10 MB source with 63 618 declarations, the panel mounted
11
+ * 446 308 nodes and a keystroke cost 953 ms (against the editor's own 34 ms).
12
+ * These cases pin the arithmetic that keeps that from happening, and the
13
+ * sentences that stop a truncated list from reading as a complete one.
14
+ */
15
+
16
+ import { describe, expect, test } from 'bun:test';
17
+ import {
18
+ MAX_OUTLINE_ROWS,
19
+ MAX_OUTLINE_SCAN_CHARS,
20
+ outlineTooLargeNotice,
21
+ outlineTruncatedNotice,
22
+ outlineWindow,
23
+ } from '@/shared/lib/code/outline/rows';
24
+ import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
25
+
26
+ function symbols(count: number): OutlineSymbol[] {
27
+ return Array.from({ length: count }, (_, i) => ({
28
+ name: `fn${i}`,
29
+ kind: 'function' as const,
30
+ line: i + 1,
31
+ column: 0,
32
+ depth: 0,
33
+ }));
34
+ }
35
+
36
+ describe('outlineWindow', () => {
37
+ test('a list under the cap is returned whole, in source order', () => {
38
+ const all = symbols(5);
39
+ const window = outlineWindow(all);
40
+
41
+ expect(window.rows).toEqual(all);
42
+ expect(window.total).toBe(5);
43
+ expect(window.truncated).toBe(false);
44
+ });
45
+
46
+ test('exactly the cap is not truncated', () => {
47
+ const window = outlineWindow(symbols(MAX_OUTLINE_ROWS));
48
+
49
+ expect(window.rows).toHaveLength(MAX_OUTLINE_ROWS);
50
+ expect(window.truncated).toBe(false);
51
+ });
52
+
53
+ test('past the cap the list is the FIRST rows, and it says how many there are', () => {
54
+ const window = outlineWindow(symbols(MAX_OUTLINE_ROWS + 120));
55
+
56
+ expect(window.rows).toHaveLength(MAX_OUTLINE_ROWS);
57
+ expect(window.total).toBe(MAX_OUTLINE_ROWS + 120);
58
+ expect(window.truncated).toBe(true);
59
+ // Source order, not a sample: the outline is a map of the document, so the
60
+ // top of the file is what a window shows.
61
+ expect(window.rows[0].name).toBe('fn0');
62
+ expect(window.rows[MAX_OUTLINE_ROWS - 1].name).toBe(`fn${MAX_OUTLINE_ROWS - 1}`);
63
+ });
64
+
65
+ test('an empty list is not truncated', () => {
66
+ const window = outlineWindow([]);
67
+
68
+ expect(window.rows).toEqual([]);
69
+ expect(window.total).toBe(0);
70
+ expect(window.truncated).toBe(false);
71
+ });
72
+ });
73
+
74
+ describe('outlineTooLargeNotice', () => {
75
+ test('a buffer at the scan ceiling is still outlined', () => {
76
+ expect(outlineTooLargeNotice(MAX_OUTLINE_SCAN_CHARS)).toBeNull();
77
+ expect(outlineTooLargeNotice(1024)).toBeNull();
78
+ });
79
+
80
+ test('past it the sentence names the size, so the empty panel is explained', () => {
81
+ const notice = outlineTooLargeNotice(MAX_OUTLINE_SCAN_CHARS + 1);
82
+
83
+ expect(notice).not.toBeNull();
84
+ expect(notice).toContain('8.0 MB');
85
+ expect(notice).toContain('too large to outline');
86
+ });
87
+ });
88
+
89
+ describe('outlineTruncatedNotice', () => {
90
+ test('it names both the shown count and the total, thousands separated', () => {
91
+ const notice = outlineTruncatedNotice(63618);
92
+
93
+ expect(notice).toContain('500');
94
+ expect(notice).toContain('63,618');
95
+ });
96
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * How much of an outline a panel is allowed to draw, and when it is not drawn
8
+ * at all.
9
+ *
10
+ * Both bounds exist because a large file's outline is not a long list — it is a
11
+ * DOM. Measured in the browser on a 10 MB source file with 63 618 declarations:
12
+ * **446 308 DOM nodes, 415 MB of JS heap, and 953 ms per keystroke** while the
13
+ * panel was on screen (the editor's own cost is 34 ms). Expanding the section
14
+ * took 2.7 s. The scan is not the problem — it is 366 ms in Bun, once per pause
15
+ * — the ROWS are.
16
+ *
17
+ * Two answers, and they are different questions:
18
+ *
19
+ * - **`MAX_OUTLINE_ROWS` caps the list.** An outline is a map you scan by eye or
20
+ * jump from; nobody reads 63 000 rows, and the panel says how many it is not
21
+ * showing rather than silently truncating. Same shape as the search panel's
22
+ * `MAX_SEARCH_RESULTS`.
23
+ * - **`MAX_OUTLINE_SCAN_CHARS` skips the scan above a size where it costs more
24
+ * than the list is worth.** At 10 MB a scan is ~366 ms of blocked main thread
25
+ * every time the buffer settles, which is a stutter per typing pause. The
26
+ * sentence says so instead of leaving the panel empty, which would read as
27
+ * "this file has no declarations".
28
+ *
29
+ * The size is counted in CODE UNITS (`content.length`), not bytes: measuring
30
+ * UTF-8 bytes would mean encoding the whole buffer to answer a guard, and for
31
+ * the ASCII that dominates source files the two numbers are the same. A file of
32
+ * multibyte characters therefore trips the guard slightly early — the safe
33
+ * direction.
34
+ */
35
+
36
+ import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
37
+
38
+ /**
39
+ * Rows a panel renders. Past this the list is truncated with a count, not
40
+ * silently shortened.
41
+ */
42
+ export const MAX_OUTLINE_ROWS = 500;
43
+
44
+ /** Buffer size above which the outline is not scanned at all. */
45
+ /**
46
+ * Buffer size above which the outline is not scanned at all.
47
+ *
48
+ * The scan is one blocked stretch of main thread after each typing pause, and it
49
+ * measured 38 ms at 1 MB, 160 ms at 5 MB, 262 ms at 10 MB and 519 ms at 20 MB —
50
+ * linear in the buffer. 8 MiB keeps that pause under ~210 ms, which is less than
51
+ * the debounce that schedules it and therefore reads as "the list settled"
52
+ * rather than as a stutter.
53
+ *
54
+ * Past it the panel says the file is too large to outline instead of scanning
55
+ * it: a declaration list for a 30 MB file is not worth a half-second freeze on
56
+ * every pause, and an empty list would read as "no declarations here".
57
+ */
58
+ export const MAX_OUTLINE_SCAN_CHARS = 8 * 1024 * 1024;
59
+
60
+ export interface OutlineWindow {
61
+ /** The rows to render — at most `MAX_OUTLINE_ROWS`. */
62
+ rows: OutlineSymbol[];
63
+ /** Symbols the scan actually found, including the ones not rendered. */
64
+ total: number;
65
+ /** True when `rows` is shorter than `total`. */
66
+ truncated: boolean;
67
+ }
68
+
69
+ /**
70
+ * The rows a panel should render, and whether it is showing all of them.
71
+ *
72
+ * Source order is preserved: an outline is a map of a document, so the first N
73
+ * declarations are the top of the file rather than an arbitrary sample.
74
+ */
75
+ export function outlineWindow(symbols: readonly OutlineSymbol[]): OutlineWindow {
76
+ const total = symbols.length;
77
+ if (total <= MAX_OUTLINE_ROWS) return { rows: symbols as OutlineSymbol[], total, truncated: false };
78
+ return { rows: symbols.slice(0, MAX_OUTLINE_ROWS), total, truncated: true };
79
+ }
80
+
81
+ /** The sentence printed under a truncated list. */
82
+ export function outlineTruncatedNotice(total: number): string {
83
+ return `Showing the first ${MAX_OUTLINE_ROWS.toLocaleString()} of ${total.toLocaleString()} symbols.`;
84
+ }
85
+
86
+ /**
87
+ * The sentence a panel prints instead of rows when the buffer is past the scan
88
+ * ceiling, or null when it is not.
89
+ */
90
+ export function outlineTooLargeNotice(contentLength: number): string | null {
91
+ if (contentLength <= MAX_OUTLINE_SCAN_CHARS) return null;
92
+ const mb = contentLength / (1024 * 1024);
93
+ return `This file is ${mb.toFixed(1)} MB — too large to outline.`;
94
+ }
@@ -6,10 +6,9 @@
6
6
  import { beforeAll, describe, expect, test } from 'bun:test';
7
7
 
8
8
  import { bootSyntax, onLanguageReady, requestLanguage } from '@/shared/lib/code/highlighter';
9
- import { findMatches } from '@/shared/lib/code/editor/find';
9
+ import { findMatches } from '@/shared/lib/code/editor/find-scan';
10
10
  import { getLanguageFromPath } from '@/shared/lib/code/language';
11
11
  import { highlightCode, highlightLines } from '@/shared/lib/code/syntax-highlight';
12
- import { highlightCodeWindow } from '@/shared/lib/code/windowed-highlight';
13
12
 
14
13
  /** Shiki emits `<span class="shiki">…` with dual-theme CSS variables per token. */
15
14
  const SHIKI_RE = /<span class="shiki">/;
@@ -115,54 +114,6 @@ describe('highlightCode', () => {
115
114
  });
116
115
  });
117
116
 
118
- describe('highlightCodeWindow', () => {
119
- const source = Array.from({ length: 120 }, (_, index) =>
120
- index === 0 ? 'export const first = 1;' : `export const value${index} = ${index}; // line ${index}`,
121
- ).join('\n');
122
-
123
- /** The wrapper the app's `.shiki span` colour rule depends on. */
124
- const unwrap = (html: string) => html.replace(/^<span class="shiki">/, '').replace(/<\/span>$/, '');
125
-
126
- test('wraps the window in the class the token colours are scoped to', () => {
127
- const window = highlightCodeWindow(source, 'typescript', { start: 0, end: 3 });
128
-
129
- // Without this class no `.shiki span` rule matches, so the tokens would
130
- // render in the plain ink colour.
131
- expect(window).toMatch(SHIKI_RE);
132
- expect(window.endsWith('</span>')).toBe(true);
133
- });
134
-
135
- test('returns only the requested lines, joined as the editor renders them', () => {
136
- const window = highlightCodeWindow(source, 'typescript', { start: 10, end: 13 });
137
- const rows = highlightLines(source, 'typescript');
138
-
139
- expect(unwrap(window).split('\n')).toEqual(rows.slice(10, 13));
140
- });
141
-
142
- test('keeps multi-line grammar state from the lines above the window', () => {
143
- const commented = ['/*', ' * a block comment', ' * spanning several lines', ' */', 'const after = 1;'].join('\n');
144
- const rows = highlightLines(commented, 'typescript');
145
- // Line 2 sits inside the comment, whose opening line is outside the window.
146
- const window = highlightCodeWindow(commented, 'typescript', { start: 2, end: 4 });
147
-
148
- expect(unwrap(window).split('\n')).toEqual(rows.slice(2, 4));
149
- });
150
-
151
- test('renders an over-long line as plain text instead of tokenizing it', () => {
152
- const minified = `const bundled = "${'x'.repeat(200)}";\nconst next = 1;`;
153
- const window = highlightCodeWindow(minified, 'javascript', { start: 0, end: 2 }, { maxLineLength: 50 });
154
-
155
- expect(window).toContain('const bundled');
156
- expect(unwrap(window).split('\n')[0]).not.toContain('--shiki-light:');
157
- expect(unwrap(window).split('\n')[1]).toContain('--shiki-light:');
158
- });
159
-
160
- test('returns nothing for a window outside the document', () => {
161
- expect(highlightCodeWindow('const a = 1;', 'typescript', { start: 5, end: 9 })).toBe('');
162
- expect(highlightCodeWindow('const a = 1;', 'typescript', { start: 0, end: 0 })).toBe('');
163
- });
164
- });
165
-
166
117
  describe('find marks', () => {
167
118
  const code = 'const alpha = 1;\nconst beta = alpha + 2;\n';
168
119
  const PLAIN = { matchCase: false, wholeWord: false, isRegex: false };
@@ -206,35 +157,6 @@ describe('find marks', () => {
206
157
  expect(plain).not.toContain('<mark');
207
158
  });
208
159
 
209
- test('keeps a windowed match on its own line, re-based to the slice', () => {
210
- const marks = findMatches(code, 'alpha', PLAIN).matches;
211
- // The window renders line 1 only. The match on line 0 must not reappear on
212
- // line 1's text, and the current index (line 0's match) must not mark
213
- // anything here.
214
- const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 2 }, { marks, currentMark: 0 });
215
-
216
- expect(marked(window).join('')).toBe('alpha');
217
- expect(window).not.toContain('data-find-current');
218
- });
219
-
220
- test('re-bases the current index onto the marks inside the window', () => {
221
- const marks = findMatches(code, 'alpha', PLAIN).matches;
222
- const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 2 }, { marks, currentMark: 1 });
223
-
224
- expect(window.match(/data-find-current/g)?.length).toBe(1);
225
- expect(marked(window).join('')).toBe('alpha');
226
- });
227
-
228
- test('a mark reaching past the window is clipped to it', () => {
229
- // The last line has no trailing newline, so a mark that runs to the end of
230
- // the slice would be clamped by the rebase.
231
- const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 3 }, {
232
- marks: [{ start: 30, end: 999 }],
233
- currentMark: 0,
234
- });
235
-
236
- expect(marked(window).join('')).toBe('alpha + 2;');
237
- });
238
160
  });
239
161
 
240
162
  describe('getLanguageFromPath', () => {
@@ -168,12 +168,11 @@ export function highlightLines(
168
168
  const rawLines = code.split('\n');
169
169
  const marks = options.marks;
170
170
  const currentMark = options.currentMark ?? -1;
171
- const occurrenceFrom = options.occurrenceFrom ?? -1;
172
171
  // A grammar that has not arrived yet, or a highlighter that failed to boot,
173
172
  // still gets the find marks: the widget's feedback must not depend on Shiki.
174
173
  const plain = (lineIndex: number): string => {
175
174
  if (!marks || marks.length === 0) return escapeCode(rawLines[lineIndex]);
176
- return tokenMarkup(rawLines[lineIndex], lineStartOffset(code, lineIndex), marks, currentMark, occurrenceFrom);
175
+ return tokenMarkup(rawLines[lineIndex], lineStartOffset(code, lineIndex), marks, currentMark);
177
176
  };
178
177
  if (!code) return rawLines;
179
178
  const hl = getHighlighterSync();
@@ -191,7 +190,7 @@ export function highlightLines(
191
190
  let offset = lineStartOffset(code, lineIndex);
192
191
  return lineTokens
193
192
  .map((tok) => {
194
- const content = tokenMarkup(tok.content, offset, marks, currentMark, occurrenceFrom);
193
+ const content = tokenMarkup(tok.content, offset, marks, currentMark);
195
194
  offset += tok.content.length;
196
195
  if (!tok.htmlStyle) return content;
197
196
  const style = Object.entries(tok.htmlStyle)