@peaske7/readit 0.2.0 → 0.3.0-rc.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 (179) hide show
  1. package/.claude/CLAUDE.md +118 -76
  2. package/.claude/commands/review.md +1 -1
  3. package/.claude/roadmap.md +32 -9
  4. package/.claude/user-stories.md +100 -15
  5. package/AGENTS.md +30 -26
  6. package/Makefile +32 -0
  7. package/README.md +90 -2
  8. package/biome.json +18 -8
  9. package/bun.lock +426 -568
  10. package/bunfig.toml +2 -0
  11. package/docs/perf-baseline.md +56 -1
  12. package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
  13. package/e2e/comments.spec.ts +14 -58
  14. package/e2e/document-load.spec.ts +1 -23
  15. package/e2e/export.spec.ts +4 -4
  16. package/e2e/perf/add-comment.spec.ts +9 -11
  17. package/e2e/perf/fixtures/generate.ts +1 -5
  18. package/e2e/perf/screenshot-final.png +0 -0
  19. package/e2e/perf/utils/metrics.ts +73 -9
  20. package/e2e/persistence-file.spec.ts +41 -26
  21. package/e2e/utils/selection.ts +17 -73
  22. package/go/cmd/readit/main.go +416 -0
  23. package/go/go.mod +20 -0
  24. package/go/go.sum +41 -0
  25. package/go/internal/server/anchor.go +302 -0
  26. package/go/internal/server/anchor_test.go +111 -0
  27. package/go/internal/server/comments.go +390 -0
  28. package/go/internal/server/documents.go +113 -0
  29. package/go/internal/server/embed.go +17 -0
  30. package/go/internal/server/headings.go +33 -0
  31. package/go/internal/server/headings_test.go +75 -0
  32. package/go/internal/server/htmltext.go +123 -0
  33. package/go/internal/server/markdown.go +157 -0
  34. package/go/internal/server/markdown_bench_test.go +42 -0
  35. package/go/internal/server/markdown_test.go +79 -0
  36. package/go/internal/server/server.go +453 -0
  37. package/go/internal/server/server_bench_test.go +122 -0
  38. package/go/internal/server/settings.go +110 -0
  39. package/go/internal/server/sse.go +140 -0
  40. package/go/internal/server/storage.go +275 -0
  41. package/go/internal/server/storage_test.go +152 -0
  42. package/go/internal/server/template.go +66 -0
  43. package/go/internal/server/types.go +101 -0
  44. package/go/internal/server/watcher.go +74 -0
  45. package/index.html +4 -14
  46. package/nvim-readit/lua/readit/health.lua +64 -0
  47. package/nvim-readit/lua/readit/init.lua +463 -0
  48. package/nvim-readit/plugin/readit.lua +19 -0
  49. package/package.json +20 -28
  50. package/shell/_readit +158 -0
  51. package/shell/readit.zsh +87 -0
  52. package/src/App.svelte +890 -0
  53. package/src/cli.ts +183 -21
  54. package/src/components/ActionsMenu.svelte +95 -0
  55. package/src/components/CommentBadge.svelte +67 -0
  56. package/src/components/CommentErrorBanner.svelte +33 -0
  57. package/src/components/CommentInput.svelte +75 -0
  58. package/src/components/CommentListItem.svelte +95 -0
  59. package/src/components/CommentManager.svelte +129 -0
  60. package/src/components/CommentNav.svelte +109 -0
  61. package/src/components/DocumentViewer.svelte +233 -0
  62. package/src/components/FloatingComment.svelte +107 -0
  63. package/src/components/Header.svelte +76 -0
  64. package/src/components/InlineEditor.svelte +72 -0
  65. package/src/components/MarginNote.svelte +167 -0
  66. package/src/components/MarginNotesContainer.svelte +33 -0
  67. package/src/components/MermaidEnhancer.svelte +218 -0
  68. package/src/components/MermaidModal.svelte +67 -0
  69. package/src/components/RawModal.svelte +126 -0
  70. package/src/components/ReanchorConfirm.svelte +30 -0
  71. package/src/components/SettingsModal.svelte +220 -0
  72. package/src/components/ShortcutCapture.svelte +82 -0
  73. package/src/components/ShortcutList.svelte +145 -0
  74. package/src/components/TabBar.svelte +52 -0
  75. package/src/components/TableOfContents.svelte +125 -0
  76. package/src/components/ui/ActionLink.svelte +40 -0
  77. package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
  78. package/src/components/ui/Dialog.svelte +97 -0
  79. package/src/components/ui/DropdownMenu.svelte +85 -0
  80. package/src/components/ui/DropdownMenuItem.svelte +38 -0
  81. package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
  82. package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
  83. package/src/env.d.ts +6 -0
  84. package/src/index.css +141 -166
  85. package/src/lib/__fixtures__/bench-data.ts +0 -13
  86. package/src/lib/anchor.bench.ts +1 -12
  87. package/src/lib/anchor.test.ts +0 -8
  88. package/src/lib/anchor.ts +0 -4
  89. package/src/lib/comment-storage.bench.ts +49 -0
  90. package/src/lib/comment-storage.test.ts +103 -33
  91. package/src/lib/comment-storage.ts +25 -18
  92. package/src/lib/export.bench.ts +21 -0
  93. package/src/lib/export.ts +0 -1
  94. package/src/lib/fetch-or-throw.test.ts +59 -0
  95. package/src/lib/fetch-or-throw.ts +12 -0
  96. package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
  97. package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
  98. package/src/lib/highlight/core.test.ts +0 -5
  99. package/src/lib/highlight/dom.ts +52 -216
  100. package/src/lib/highlight/highlight-registry.ts +221 -0
  101. package/src/lib/highlight/highlight.bench.ts +92 -0
  102. package/src/lib/highlight/highlighter.ts +112 -132
  103. package/src/lib/highlight/resolver.ts +5 -79
  104. package/src/lib/highlight/types.ts +0 -5
  105. package/src/lib/html-text.test.ts +162 -0
  106. package/src/lib/html-text.ts +161 -0
  107. package/src/lib/i18n/en.ts +34 -0
  108. package/src/lib/i18n/ja.ts +34 -0
  109. package/src/lib/i18n/types.ts +33 -0
  110. package/src/lib/key-lock.test.ts +104 -0
  111. package/src/lib/key-lock.ts +23 -0
  112. package/src/lib/margin-layout.bench.ts +61 -0
  113. package/src/lib/margin-layout.ts +0 -7
  114. package/src/lib/markdown-renderer.test.ts +154 -0
  115. package/src/lib/markdown-renderer.ts +178 -0
  116. package/src/lib/mermaid-config.ts +38 -0
  117. package/src/lib/mermaid-renderer.ts +162 -0
  118. package/src/lib/mermaid-worker.ts +60 -0
  119. package/src/lib/positions.ts +31 -24
  120. package/src/lib/shortcut-registry.ts +244 -0
  121. package/src/lib/utils.ts +0 -29
  122. package/src/main.ts +16 -0
  123. package/src/schema.ts +16 -5
  124. package/src/server.ts +355 -95
  125. package/src/stores/app.svelte.ts +231 -0
  126. package/src/stores/locale.svelte.ts +46 -0
  127. package/src/stores/settings.svelte.ts +90 -0
  128. package/src/stores/shortcuts.svelte.ts +104 -0
  129. package/src/stores/ui.svelte.ts +12 -0
  130. package/src/template.ts +104 -0
  131. package/src/test-setup.ts +47 -0
  132. package/svelte.config.js +5 -0
  133. package/tsconfig.json +2 -2
  134. package/vite.config.ts +23 -3
  135. package/vscode-readit/.mcp.json +7 -0
  136. package/vscode-readit/.vscodeignore +7 -0
  137. package/vscode-readit/bun.lock +78 -0
  138. package/vscode-readit/icon.svg +10 -0
  139. package/vscode-readit/package.json +110 -0
  140. package/vscode-readit/src/extension.ts +117 -0
  141. package/vscode-readit/src/server-manager.ts +272 -0
  142. package/vscode-readit/src/webview-provider.ts +204 -0
  143. package/vscode-readit/tsconfig.json +20 -0
  144. package/e2e/fixtures/sample.html +0 -13
  145. package/src/App.tsx +0 -368
  146. package/src/components/ActionsMenu.tsx +0 -91
  147. package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
  148. package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
  149. package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
  150. package/src/components/Header.tsx +0 -54
  151. package/src/components/InlineEditor.tsx +0 -74
  152. package/src/components/MarginNote.tsx +0 -185
  153. package/src/components/MarginNotes.tsx +0 -23
  154. package/src/components/RawModal.tsx +0 -144
  155. package/src/components/ReanchorConfirm.tsx +0 -36
  156. package/src/components/SettingsModal.tsx +0 -232
  157. package/src/components/TabBar.tsx +0 -60
  158. package/src/components/TableOfContents.tsx +0 -108
  159. package/src/components/comments/CommentBadge.tsx +0 -49
  160. package/src/components/comments/CommentInput.tsx +0 -86
  161. package/src/components/comments/CommentListItem.tsx +0 -90
  162. package/src/components/comments/CommentManager.tsx +0 -129
  163. package/src/components/comments/CommentNav.tsx +0 -109
  164. package/src/components/ui/ActionLink.tsx +0 -28
  165. package/src/components/ui/Dialog.tsx +0 -116
  166. package/src/components/ui/DropdownMenu.tsx +0 -158
  167. package/src/contexts/CommentContext.tsx +0 -198
  168. package/src/contexts/LocaleContext.tsx +0 -76
  169. package/src/contexts/PositionsContext.tsx +0 -16
  170. package/src/contexts/SettingsContext.tsx +0 -133
  171. package/src/hooks/useClickOutside.ts +0 -31
  172. package/src/hooks/useCommentNavigation.ts +0 -107
  173. package/src/hooks/useComments.ts +0 -311
  174. package/src/hooks/useDocument.ts +0 -157
  175. package/src/hooks/useScrollSpy.ts +0 -77
  176. package/src/hooks/useTextSelection.ts +0 -86
  177. package/src/lib/highlight/worker.ts +0 -45
  178. package/src/main.tsx +0 -13
  179. package/src/store.ts +0 -222
package/src/index.css CHANGED
@@ -101,8 +101,8 @@ html {
101
101
  --comment-color-3-bg-focused: rgba(205, 145, 155, 0.65);
102
102
  --comment-color-3-border: #b86b78;
103
103
 
104
- /* Pending - neutral gray */
105
- --pending-bg: rgba(180, 180, 180, 0.3);
104
+ /* Pending - warm amber tint (matches comment highlight palette) */
105
+ --pending-bg: rgba(245, 222, 160, 0.45);
106
106
  }
107
107
 
108
108
  .dark {
@@ -151,127 +151,38 @@ html {
151
151
  --comment-color-3-bg-focused: rgba(184, 107, 120, 0.55);
152
152
  --comment-color-3-border: #d08a98;
153
153
 
154
- --pending-bg: rgba(100, 100, 100, 0.3);
154
+ --pending-bg: rgba(202, 168, 74, 0.35);
155
155
  }
156
156
 
157
157
  /* ========================================
158
- Comment Highlight Styles
158
+ Comment Highlight Styles (CSS Custom Highlight API)
159
159
  ======================================== */
160
160
 
161
- /* Base highlight - subtle by default */
162
- mark[data-comment-id] {
163
- background-color: var(--highlight-bg, var(--comment-color-0-bg));
164
- cursor: pointer;
165
- transition:
166
- background-color 150ms ease,
167
- opacity 150ms ease;
168
- }
169
-
170
- /* Color variants based on data-color-index */
171
- mark[data-comment-id][data-color-index="0"] {
172
- --highlight-bg: var(--comment-color-0-bg);
173
- --highlight-bg-focused: var(--comment-color-0-bg-focused);
174
- --highlight-border: var(--comment-color-0-border);
175
- }
176
-
177
- mark[data-comment-id][data-color-index="1"] {
178
- --highlight-bg: var(--comment-color-1-bg);
179
- --highlight-bg-focused: var(--comment-color-1-bg-focused);
180
- --highlight-border: var(--comment-color-1-border);
161
+ /* Color variants — paint-time only, zero DOM mutations */
162
+ ::highlight(comment-color-0) {
163
+ background-color: var(--comment-color-0-bg);
181
164
  }
182
165
 
183
- mark[data-comment-id][data-color-index="2"] {
184
- --highlight-bg: var(--comment-color-2-bg);
185
- --highlight-bg-focused: var(--comment-color-2-bg-focused);
186
- --highlight-border: var(--comment-color-2-border);
166
+ ::highlight(comment-color-1) {
167
+ background-color: var(--comment-color-1-bg);
187
168
  }
188
169
 
189
- mark[data-comment-id][data-color-index="3"] {
190
- --highlight-bg: var(--comment-color-3-bg);
191
- --highlight-bg-focused: var(--comment-color-3-bg-focused);
192
- --highlight-border: var(--comment-color-3-border);
170
+ ::highlight(comment-color-2) {
171
+ background-color: var(--comment-color-2-bg);
193
172
  }
194
173
 
195
- /* Focused state - this highlight is hovered */
196
- mark[data-comment-id][data-focused="true"] {
197
- background-color: var(
198
- --highlight-bg-focused,
199
- var(--comment-color-0-bg-focused)
200
- );
174
+ ::highlight(comment-color-3) {
175
+ background-color: var(--comment-color-3-bg);
201
176
  }
202
177
 
203
- /* Hover on highlight itself */
204
- mark[data-comment-id]:hover {
205
- background-color: var(
206
- --highlight-bg-focused,
207
- var(--comment-color-0-bg-focused)
208
- );
209
- }
210
-
211
- /* ========================================
212
- Bracket Mode for Long Selections
213
- ======================================== */
214
-
215
- /* Container for bracket-mode highlights */
216
- mark[data-comment-id][data-bracket-mode="true"] {
217
- background-color: transparent;
218
- position: relative;
219
- }
220
-
221
- /* Only show background on first segment in bracket mode */
222
- mark[data-comment-id][data-bracket-start="true"] {
223
- background-color: var(--highlight-bg, var(--comment-color-0-bg));
224
- border-radius: 2px 2px 0 0;
225
- }
226
-
227
- /* Focused bracket mode shows full highlight */
228
- mark[data-comment-id][data-bracket-mode="true"][data-focused="true"],
229
- mark[data-comment-id][data-bracket-mode="true"]:hover {
230
- background-color: var(
231
- --highlight-bg-focused,
232
- var(--comment-color-0-bg-focused)
233
- );
234
- }
235
-
236
- /* Gutter line for bracket mode */
237
- .bracket-gutter-line {
238
- position: absolute;
239
- left: -20px;
240
- width: 2px;
241
- background-color: var(--bracket-color, var(--comment-color-0-border));
242
- border-radius: 1px;
243
- opacity: 0.6;
244
- transition: opacity 150ms ease;
245
- pointer-events: none;
246
- }
247
-
248
- .bracket-gutter-line[data-focused="true"],
249
- .bracket-gutter-line:hover {
250
- opacity: 1;
178
+ /* Focused state higher priority, overrides color group */
179
+ ::highlight(comment-focused) {
180
+ background-color: var(--comment-color-0-bg-focused);
251
181
  }
252
182
 
253
- /* Bracket markers */
254
- .bracket-marker {
255
- position: absolute;
256
- left: -24px;
257
- font-family: monospace;
258
- font-size: 10px;
259
- color: var(--bracket-color, var(--comment-color-0-border));
260
- opacity: 0.6;
261
- pointer-events: none;
262
- transition: opacity 150ms ease;
263
- }
264
-
265
- .bracket-marker-start {
266
- top: -2px;
267
- }
268
-
269
- .bracket-marker-end {
270
- bottom: -2px;
271
- }
272
-
273
- .bracket-marker[data-focused="true"] {
274
- opacity: 1;
183
+ /* Pending selection highlight — neutral gray */
184
+ ::highlight(pending-selection) {
185
+ background-color: var(--pending-bg);
275
186
  }
276
187
 
277
188
  /* ========================================
@@ -301,64 +212,6 @@ mark[data-comment-id][data-bracket-mode="true"]:hover {
301
212
  opacity: 0.7;
302
213
  }
303
214
 
304
- /* Pending selection highlight - neutral gray */
305
- mark[data-pending] {
306
- background-color: var(--pending-bg);
307
- cursor: text;
308
- }
309
-
310
- /* Ensure highlights are visible in all contexts including headings */
311
- .prose mark[data-pending],
312
- .prose h1 mark[data-pending],
313
- .prose h2 mark[data-pending],
314
- .prose h3 mark[data-pending],
315
- .prose h4 mark[data-pending],
316
- .prose h5 mark[data-pending],
317
- .prose h6 mark[data-pending] {
318
- background-color: var(--pending-bg) !important;
319
- display: inline !important;
320
- }
321
-
322
- /* Ensure comment highlights are visible inside headings (prose overrides) */
323
- .prose h1 mark[data-comment-id],
324
- .prose h2 mark[data-comment-id],
325
- .prose h3 mark[data-comment-id],
326
- .prose h4 mark[data-comment-id],
327
- .prose h5 mark[data-comment-id],
328
- .prose h6 mark[data-comment-id] {
329
- background-color: var(--highlight-bg, var(--comment-color-0-bg)) !important;
330
- display: inline !important;
331
- }
332
-
333
- /* Bracket mode in headings - override transparent background */
334
- .prose h1 mark[data-comment-id][data-bracket-mode="true"],
335
- .prose h2 mark[data-comment-id][data-bracket-mode="true"],
336
- .prose h3 mark[data-comment-id][data-bracket-mode="true"],
337
- .prose h4 mark[data-comment-id][data-bracket-mode="true"],
338
- .prose h5 mark[data-comment-id][data-bracket-mode="true"],
339
- .prose h6 mark[data-comment-id][data-bracket-mode="true"] {
340
- background-color: var(--highlight-bg, var(--comment-color-0-bg)) !important;
341
- }
342
-
343
- /* Hover state for comment highlights in headings */
344
- .prose h1 mark[data-comment-id]:hover,
345
- .prose h2 mark[data-comment-id]:hover,
346
- .prose h3 mark[data-comment-id]:hover,
347
- .prose h4 mark[data-comment-id]:hover,
348
- .prose h5 mark[data-comment-id]:hover,
349
- .prose h6 mark[data-comment-id]:hover,
350
- .prose h1 mark[data-comment-id][data-focused="true"],
351
- .prose h2 mark[data-comment-id][data-focused="true"],
352
- .prose h3 mark[data-comment-id][data-focused="true"],
353
- .prose h4 mark[data-comment-id][data-focused="true"],
354
- .prose h5 mark[data-comment-id][data-focused="true"],
355
- .prose h6 mark[data-comment-id][data-focused="true"] {
356
- background-color: var(
357
- --highlight-bg-focused,
358
- var(--comment-color-0-bg-focused)
359
- ) !important;
360
- }
361
-
362
215
  /* Toast animation */
363
216
  [data-sonner-toast] {
364
217
  animation: toast-in 0.2s ease-out;
@@ -579,6 +432,23 @@ mark[data-pending] {
579
432
  margin: 1.75em 0;
580
433
  }
581
434
 
435
+ /* Shiki syntax-highlighted code blocks (server-rendered) */
436
+ .prose pre.shiki {
437
+ margin: 1.5em 0;
438
+ border-radius: 0.5em;
439
+ font-size: 0.875em;
440
+ padding: 1em;
441
+ overflow-x: auto;
442
+ }
443
+
444
+ .prose pre.shiki code {
445
+ background: transparent;
446
+ padding: 0;
447
+ font-size: inherit;
448
+ font-weight: 400;
449
+ color: inherit;
450
+ }
451
+
582
452
  .prose pre code {
583
453
  background: transparent;
584
454
  padding: 0;
@@ -864,6 +734,111 @@ mark[data-pending] {
864
734
  }
865
735
  }
866
736
 
737
+ /* Interactive toolbar (hover-revealed) */
738
+ .mermaid-toolbar {
739
+ position: absolute;
740
+ top: 0.5rem;
741
+ right: 0.5rem;
742
+ display: flex;
743
+ gap: 0.25rem;
744
+ padding: 0.25rem;
745
+ border-radius: 0.5rem;
746
+ background: rgba(255, 255, 255, 0.85);
747
+ backdrop-filter: blur(4px);
748
+ border: 1px solid rgba(228, 228, 231, 0.6);
749
+ opacity: 0;
750
+ transition: opacity 120ms ease-out;
751
+ pointer-events: none;
752
+ z-index: 5;
753
+ }
754
+
755
+ .dark .mermaid-toolbar {
756
+ background: rgba(24, 24, 27, 0.85);
757
+ border-color: rgba(63, 63, 70, 0.6);
758
+ }
759
+
760
+ .mermaid-container:hover .mermaid-toolbar,
761
+ .mermaid-toolbar:focus-within {
762
+ opacity: 1;
763
+ pointer-events: auto;
764
+ }
765
+
766
+ .mermaid-toolbar button {
767
+ display: inline-flex;
768
+ align-items: center;
769
+ justify-content: center;
770
+ width: 1.75rem;
771
+ height: 1.75rem;
772
+ border-radius: 0.375rem;
773
+ color: rgb(82, 82, 91);
774
+ background: transparent;
775
+ border: 0;
776
+ cursor: pointer;
777
+ transition:
778
+ background 120ms ease-out,
779
+ color 120ms ease-out;
780
+ }
781
+
782
+ .mermaid-toolbar button:hover {
783
+ background: rgba(228, 228, 231, 0.8);
784
+ color: rgb(24, 24, 27);
785
+ }
786
+
787
+ .dark .mermaid-toolbar button {
788
+ color: rgb(161, 161, 170);
789
+ }
790
+
791
+ .dark .mermaid-toolbar button:hover {
792
+ background: rgba(63, 63, 70, 0.8);
793
+ color: rgb(244, 244, 245);
794
+ }
795
+
796
+ /* Inline source view shown when toggled to code mode */
797
+ .mermaid-source-view {
798
+ margin: 0;
799
+ padding: 1rem;
800
+ border-radius: 0.5rem;
801
+ background: rgb(244, 244, 245);
802
+ color: rgb(39, 39, 42);
803
+ font-size: 0.8125rem;
804
+ line-height: 1.5;
805
+ font-family:
806
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
807
+ "Courier New", monospace;
808
+ overflow: auto;
809
+ white-space: pre;
810
+ }
811
+
812
+ .dark .mermaid-source-view {
813
+ background: rgb(24, 24, 27);
814
+ color: rgb(228, 228, 231);
815
+ }
816
+
817
+ .mermaid-source-view code {
818
+ background: transparent;
819
+ padding: 0;
820
+ font-family: inherit;
821
+ font-size: inherit;
822
+ color: inherit;
823
+ }
824
+
825
+ /* Modal-mode mermaid graph — let it scale up */
826
+ .mermaid-modal-graph {
827
+ margin: 0;
828
+ padding: 0;
829
+ width: 100%;
830
+ display: flex;
831
+ align-items: center;
832
+ justify-content: center;
833
+ }
834
+
835
+ .mermaid-modal-graph > svg {
836
+ max-width: 100%;
837
+ max-height: 80vh;
838
+ width: auto;
839
+ height: auto;
840
+ }
841
+
867
842
  /* ========================================
868
843
  Floating TOC Panel
869
844
  ======================================== */
@@ -1,8 +1,6 @@
1
1
  import type { Comment, CommentFile } from "../../schema";
2
2
  import { serializeComments } from "../comment-storage";
3
3
 
4
- // --- Document fixtures ---
5
-
6
4
  function generateMarkdownDoc(lineCount: number): string {
7
5
  const lines: string[] = [];
8
6
  lines.push("# Performance Test Document");
@@ -21,13 +19,11 @@ function generateMarkdownDoc(lineCount: number): string {
21
19
  );
22
20
  lines.push("");
23
21
 
24
- // Add a list
25
22
  for (let j = 1; j <= 3; j++) {
26
23
  lines.push(`- Item ${j} in section ${sectionNum}`);
27
24
  }
28
25
  lines.push("");
29
26
 
30
- // Add a code block
31
27
  lines.push("```typescript");
32
28
  lines.push(`function section${sectionNum}() {`);
33
29
  lines.push(` const value = ${sectionNum} * 42;`);
@@ -36,13 +32,11 @@ function generateMarkdownDoc(lineCount: number): string {
36
32
  lines.push("```");
37
33
  lines.push("");
38
34
 
39
- // Add a paragraph
40
35
  lines.push(
41
36
  `The conclusion of section ${sectionNum} summarizes the key findings and provides actionable recommendations for the reader to follow.`,
42
37
  );
43
38
  lines.push("");
44
39
 
45
- // Add a table every few sections
46
40
  if (sectionNum % 3 === 0) {
47
41
  lines.push("| Column A | Column B | Column C |");
48
42
  lines.push("|----------|----------|----------|");
@@ -65,11 +59,8 @@ export const SMALL_DOC = generateMarkdownDoc(30);
65
59
  export const MEDIUM_DOC = generateMarkdownDoc(150);
66
60
  export const LARGE_DOC = generateMarkdownDoc(300);
67
61
 
68
- // --- Comment fixtures ---
69
-
70
62
  function makeComment(index: number, doc: string): Comment {
71
63
  const lines = doc.split("\n");
72
- // Distribute comments across the document
73
64
  const targetLine = Math.min(
74
65
  Math.floor((index + 1) * (lines.length / 55)),
75
66
  lines.length - 1,
@@ -81,7 +72,6 @@ function makeComment(index: number, doc: string): Comment {
81
72
  ? lineText.slice(0, Math.min(30, lineText.length))
82
73
  : lineText || "default text";
83
74
 
84
- // Calculate actual character offset
85
75
  let startOffset = 0;
86
76
  for (let i = 0; i < targetLine; i++) {
87
77
  startOffset += lines[i].length + 1; // +1 for \n
@@ -92,7 +82,6 @@ function makeComment(index: number, doc: string): Comment {
92
82
  id: `bench${String(index).padStart(3, "0")}`,
93
83
  selectedText,
94
84
  comment: `Benchmark comment ${index}: This text needs to be reviewed and updated.`,
95
- createdAt: "2025-01-01T00:00:00.000Z",
96
85
  startOffset,
97
86
  endOffset,
98
87
  lineHint: `L${targetLine + 1}`,
@@ -107,8 +96,6 @@ export const COMMENTS_1 = makeComments(1);
107
96
  export const COMMENTS_10 = makeComments(10);
108
97
  export const COMMENTS_50 = makeComments(50);
109
98
 
110
- // --- Serialized comment file fixtures ---
111
-
112
99
  function makeCommentFile(comments: Comment[]): CommentFile {
113
100
  return {
114
101
  source: "/bench/test-doc.md",
@@ -13,8 +13,6 @@ import {
13
13
  findAnchorWithFallback,
14
14
  } from "./anchor";
15
15
 
16
- // --- Exact match (best case) ---
17
-
18
16
  describe("findAnchor — exact match", () => {
19
17
  const comment = COMMENTS_10[5];
20
18
 
@@ -35,10 +33,7 @@ describe("findAnchor — exact match", () => {
35
33
  });
36
34
  });
37
35
 
38
- // --- Normalized match ---
39
-
40
36
  describe("findAnchorNormalized", () => {
41
- // Create text with extra whitespace to force normalization path
42
37
  const comment = COMMENTS_10[5];
43
38
  const normalizedText = comment.selectedText.replace(/ /g, " ");
44
39
 
@@ -51,13 +46,9 @@ describe("findAnchorNormalized", () => {
51
46
  });
52
47
  });
53
48
 
54
- // --- Fuzzy match (worst case) ---
55
-
56
49
  describe("findAnchorFuzzy", () => {
57
- // Slightly mutate text to force Levenshtein search
58
50
  const comment = COMMENTS_10[5];
59
- const mutated =
60
- "X" + comment.selectedText.slice(1, -1) + "Z";
51
+ const mutated = `X${comment.selectedText.slice(1, -1)}Z`;
61
52
 
62
53
  bench("large doc — fuzzy (mutated text)", () => {
63
54
  findAnchorFuzzy({
@@ -68,8 +59,6 @@ describe("findAnchorFuzzy", () => {
68
59
  });
69
60
  });
70
61
 
71
- // --- Full fallback chain ---
72
-
73
62
  describe("findAnchorWithFallback", () => {
74
63
  bench("1 comment — exact hit", () => {
75
64
  const c = COMMENTS_1[0];
@@ -64,12 +64,10 @@ describe("levenshteinDistance", () => {
64
64
  });
65
65
 
66
66
  it("returns Infinity when exceeding maxDistance threshold", () => {
67
- // Length difference alone exceeds threshold
68
67
  expect(levenshteinDistance("hello", "hi", 1)).toBe(
69
68
  Number.POSITIVE_INFINITY,
70
69
  );
71
70
 
72
- // Content difference exceeds threshold during computation
73
71
  expect(levenshteinDistance("hello", "world", 2)).toBe(
74
72
  Number.POSITIVE_INFINITY,
75
73
  );
@@ -252,7 +250,6 @@ line eight`;
252
250
 
253
251
  describe("findAnchorNormalized", () => {
254
252
  it("finds text with collapsed whitespace", () => {
255
- // Original had "hello world" but source was reformatted
256
253
  const source = "hello\n world";
257
254
  const result = findAnchorNormalized({
258
255
  source,
@@ -275,7 +272,6 @@ describe("findAnchorNormalized", () => {
275
272
  });
276
273
 
277
274
  it("returns null when text has no collapsible whitespace", () => {
278
- // If original text has no extra whitespace, exact match would have worked
279
275
  const result = findAnchorNormalized({
280
276
  source: "hello world",
281
277
  selectedText: "hello world",
@@ -411,7 +407,6 @@ describe("findAnchorWithFallback", () => {
411
407
  });
412
408
 
413
409
  it("falls back to normalized match when exact fails", () => {
414
- // Source was reformatted (newlines instead of spaces)
415
410
  const result = findAnchorWithFallback({
416
411
  source: "hello\nworld",
417
412
  selectedText: "hello world",
@@ -444,7 +439,6 @@ describe("findAnchorWithFallback", () => {
444
439
  describe("findClosestOccurrence", () => {
445
440
  it("finds the occurrence closest to hint", () => {
446
441
  const content = "the cat sat on the mat and the rat";
447
- // "the" appears at positions 0, 15, and 27
448
442
 
449
443
  const result = findClosestOccurrence({
450
444
  source: content,
@@ -452,7 +446,6 @@ describe("findClosestOccurrence", () => {
452
446
  lineHint: "L1",
453
447
  });
454
448
  expect(result).not.toBeUndefined();
455
- // Should find the first "the" at position 0 since hint is L1
456
449
  expect(result?.start).toBe(0);
457
450
  });
458
451
 
@@ -463,7 +456,6 @@ line three the
463
456
  line four
464
457
  line five the`;
465
458
 
466
- // Test finding closest to line 3
467
459
  const result = findClosestOccurrence({
468
460
  source: content,
469
461
  selectedText: "the",
package/src/lib/anchor.ts CHANGED
@@ -31,7 +31,6 @@ export function normalizeWhitespace(text: string): string {
31
31
  return text.replace(/\s+/g, " ").trim();
32
32
  }
33
33
 
34
- /** Wagner-Fischer with O(min(m,n)) space. Returns Infinity when > maxDistance. */
35
34
  export function levenshteinDistance(
36
35
  a: string,
37
36
  b: string,
@@ -100,7 +99,6 @@ export function getLineOffset(content: string, lineNumber: number): number {
100
99
  return content.length;
101
100
  }
102
101
 
103
- /** Supports "L42", "L42-L55", and legacy "L42-45" format. */
104
102
  export function parseLineHint(lineHint: string): {
105
103
  start: number;
106
104
  end: number;
@@ -222,7 +220,6 @@ export function findAnchorNormalized({
222
220
  const originalStart = windowStart + toOriginal[normalizedIndex];
223
221
  const endNormIndex = normalizedIndex + normalizedText.length - 1;
224
222
  let originalEnd = windowStart + toOriginal[endNormIndex] + 1;
225
- // Extend past trailing whitespace that was collapsed during normalization
226
223
  while (originalEnd < source.length && /\s/.test(source[originalEnd])) {
227
224
  originalEnd++;
228
225
  }
@@ -318,7 +315,6 @@ export function findAnchorFuzzy({
318
315
  return bestMatch;
319
316
  }
320
317
 
321
- /** Fallback chain: exact → normalized → fuzzy. */
322
318
  export function findAnchorWithFallback({
323
319
  source,
324
320
  selectedText,
@@ -0,0 +1,49 @@
1
+ import { bench, describe } from "vitest";
2
+ import {
3
+ COMMENT_FILE_LARGE,
4
+ COMMENT_FILE_MEDIUM,
5
+ COMMENT_FILE_OBJ_LARGE,
6
+ COMMENT_FILE_OBJ_MEDIUM,
7
+ COMMENT_FILE_OBJ_SMALL,
8
+ COMMENT_FILE_SMALL,
9
+ LARGE_DOC,
10
+ } from "./__fixtures__/bench-data";
11
+ import {
12
+ computeHash,
13
+ parseCommentFile,
14
+ serializeComments,
15
+ } from "./comment-storage";
16
+
17
+ describe("parseCommentFile", () => {
18
+ bench("1 comment", () => {
19
+ parseCommentFile(COMMENT_FILE_SMALL);
20
+ });
21
+
22
+ bench("10 comments", () => {
23
+ parseCommentFile(COMMENT_FILE_MEDIUM);
24
+ });
25
+
26
+ bench("50 comments", () => {
27
+ parseCommentFile(COMMENT_FILE_LARGE);
28
+ });
29
+ });
30
+
31
+ describe("serializeComments", () => {
32
+ bench("1 comment", () => {
33
+ serializeComments(COMMENT_FILE_OBJ_SMALL);
34
+ });
35
+
36
+ bench("10 comments", () => {
37
+ serializeComments(COMMENT_FILE_OBJ_MEDIUM);
38
+ });
39
+
40
+ bench("50 comments", () => {
41
+ serializeComments(COMMENT_FILE_OBJ_LARGE);
42
+ });
43
+ });
44
+
45
+ describe("computeHash", () => {
46
+ bench("300-line document", () => {
47
+ computeHash(LARGE_DOC);
48
+ });
49
+ });