@peaske7/readit 0.2.0 → 0.2.1

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 (173) 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 +118 -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 +881 -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 +218 -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/RawModal.svelte +126 -0
  68. package/src/components/ReanchorConfirm.svelte +30 -0
  69. package/src/components/SettingsModal.svelte +220 -0
  70. package/src/components/ShortcutCapture.svelte +82 -0
  71. package/src/components/ShortcutList.svelte +145 -0
  72. package/src/components/TabBar.svelte +52 -0
  73. package/src/components/TableOfContents.svelte +125 -0
  74. package/src/components/ui/ActionLink.svelte +40 -0
  75. package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
  76. package/src/components/ui/Dialog.svelte +97 -0
  77. package/src/components/ui/DropdownMenu.svelte +85 -0
  78. package/src/components/ui/DropdownMenuItem.svelte +38 -0
  79. package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
  80. package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
  81. package/src/env.d.ts +6 -0
  82. package/src/index.css +36 -166
  83. package/src/lib/__fixtures__/bench-data.ts +0 -13
  84. package/src/lib/anchor.bench.ts +1 -12
  85. package/src/lib/anchor.test.ts +0 -8
  86. package/src/lib/anchor.ts +0 -4
  87. package/src/lib/comment-storage.bench.ts +49 -0
  88. package/src/lib/comment-storage.test.ts +41 -33
  89. package/src/lib/comment-storage.ts +21 -18
  90. package/src/lib/export.bench.ts +21 -0
  91. package/src/lib/export.ts +0 -1
  92. package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
  93. package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
  94. package/src/lib/highlight/core.test.ts +0 -5
  95. package/src/lib/highlight/dom.ts +52 -216
  96. package/src/lib/highlight/highlight-registry.ts +221 -0
  97. package/src/lib/highlight/highlight.bench.ts +92 -0
  98. package/src/lib/highlight/highlighter.ts +112 -132
  99. package/src/lib/highlight/resolver.ts +5 -79
  100. package/src/lib/highlight/types.ts +0 -5
  101. package/src/lib/html-text.test.ts +162 -0
  102. package/src/lib/html-text.ts +161 -0
  103. package/src/lib/i18n/en.ts +26 -0
  104. package/src/lib/i18n/ja.ts +26 -0
  105. package/src/lib/i18n/types.ts +25 -0
  106. package/src/lib/margin-layout.bench.ts +61 -0
  107. package/src/lib/margin-layout.ts +0 -7
  108. package/src/lib/markdown-renderer.test.ts +154 -0
  109. package/src/lib/markdown-renderer.ts +177 -0
  110. package/src/lib/mermaid-config.ts +38 -0
  111. package/src/lib/mermaid-renderer.ts +162 -0
  112. package/src/lib/mermaid-worker.ts +60 -0
  113. package/src/lib/positions.ts +31 -24
  114. package/src/lib/shortcut-registry.ts +244 -0
  115. package/src/lib/utils.ts +0 -29
  116. package/src/main.ts +16 -0
  117. package/src/schema.ts +16 -5
  118. package/src/server.ts +355 -91
  119. package/src/stores/app.svelte.ts +231 -0
  120. package/src/stores/locale.svelte.ts +46 -0
  121. package/src/stores/settings.svelte.ts +90 -0
  122. package/src/stores/shortcuts.svelte.ts +104 -0
  123. package/src/stores/ui.svelte.ts +12 -0
  124. package/src/template.ts +104 -0
  125. package/src/test-setup.ts +47 -0
  126. package/svelte.config.js +5 -0
  127. package/tsconfig.json +2 -2
  128. package/vite.config.ts +23 -3
  129. package/vscode-readit/.mcp.json +7 -0
  130. package/vscode-readit/.vscodeignore +7 -0
  131. package/vscode-readit/bun.lock +78 -0
  132. package/vscode-readit/icon.svg +10 -0
  133. package/vscode-readit/package.json +110 -0
  134. package/vscode-readit/src/extension.ts +117 -0
  135. package/vscode-readit/src/server-manager.ts +272 -0
  136. package/vscode-readit/src/webview-provider.ts +204 -0
  137. package/vscode-readit/tsconfig.json +20 -0
  138. package/e2e/fixtures/sample.html +0 -13
  139. package/src/App.tsx +0 -368
  140. package/src/components/ActionsMenu.tsx +0 -91
  141. package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
  142. package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
  143. package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
  144. package/src/components/Header.tsx +0 -54
  145. package/src/components/InlineEditor.tsx +0 -74
  146. package/src/components/MarginNote.tsx +0 -185
  147. package/src/components/MarginNotes.tsx +0 -23
  148. package/src/components/RawModal.tsx +0 -144
  149. package/src/components/ReanchorConfirm.tsx +0 -36
  150. package/src/components/SettingsModal.tsx +0 -232
  151. package/src/components/TabBar.tsx +0 -60
  152. package/src/components/TableOfContents.tsx +0 -108
  153. package/src/components/comments/CommentBadge.tsx +0 -49
  154. package/src/components/comments/CommentInput.tsx +0 -86
  155. package/src/components/comments/CommentListItem.tsx +0 -90
  156. package/src/components/comments/CommentManager.tsx +0 -129
  157. package/src/components/comments/CommentNav.tsx +0 -109
  158. package/src/components/ui/ActionLink.tsx +0 -28
  159. package/src/components/ui/Dialog.tsx +0 -116
  160. package/src/components/ui/DropdownMenu.tsx +0 -158
  161. package/src/contexts/CommentContext.tsx +0 -198
  162. package/src/contexts/LocaleContext.tsx +0 -76
  163. package/src/contexts/PositionsContext.tsx +0 -16
  164. package/src/contexts/SettingsContext.tsx +0 -133
  165. package/src/hooks/useClickOutside.ts +0 -31
  166. package/src/hooks/useCommentNavigation.ts +0 -107
  167. package/src/hooks/useComments.ts +0 -311
  168. package/src/hooks/useDocument.ts +0 -157
  169. package/src/hooks/useScrollSpy.ts +0 -77
  170. package/src/hooks/useTextSelection.ts +0 -86
  171. package/src/lib/highlight/worker.ts +0 -45
  172. package/src/main.tsx +0 -13
  173. package/src/store.ts +0 -222
@@ -1,7 +1,8 @@
1
- import { use } from "react";
2
- import { SettingsContext } from "../../contexts/SettingsContext";
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
3
  import { cn } from "../../lib/utils";
4
4
  import { FontFamilies } from "../../schema";
5
+ import { settings } from "../../stores/settings.svelte";
5
6
 
6
7
  const variantStyles = {
7
8
  title:
@@ -17,31 +18,25 @@ const variantStyles = {
17
18
 
18
19
  type TextVariant = keyof typeof variantStyles;
19
20
 
20
- interface TextProps extends React.HTMLAttributes<HTMLElement> {
21
+ interface Props {
21
22
  variant?: TextVariant;
22
23
  as?: "p" | "span" | "div" | "h1" | "h2" | "h3" | "label" | "pre";
24
+ class?: string;
25
+ children: Snippet;
23
26
  }
24
27
 
25
- function Text({
26
- className,
28
+ let {
27
29
  variant = "body",
28
- as: Tag = "p",
29
- ...props
30
- }: TextProps) {
31
- const settings = use(SettingsContext);
32
- const fontClass = settings
33
- ? settings.fontFamily === FontFamilies.SANS_SERIF
34
- ? "font-sans"
35
- : "font-serif"
36
- : undefined;
30
+ as = "p",
31
+ class: className,
32
+ children,
33
+ }: Props = $props();
37
34
 
38
- return (
39
- <Tag
40
- className={cn(fontClass, variantStyles[variant], className)}
41
- {...props}
42
- />
43
- );
44
- }
35
+ let fontClass = $derived(
36
+ settings.fontFamily === FontFamilies.SANS_SERIF ? "font-sans" : "font-serif",
37
+ );
38
+ </script>
45
39
 
46
- export type { TextVariant };
47
- export { Text, variantStyles };
40
+ <svelte:element this={as} class={cn(fontClass, variantStyles[variant], className)}>
41
+ {@render children()}
42
+ </svelte:element>
package/src/env.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /// <reference types="bun" />
2
+
3
+ declare module "*.css" {
4
+ const content: string;
5
+ export default content;
6
+ }
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);
181
- }
182
-
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);
187
- }
188
-
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);
193
- }
194
-
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
- );
201
- }
202
-
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
- );
161
+ /* Color variants — paint-time only, zero DOM mutations */
162
+ ::highlight(comment-color-0) {
163
+ background-color: var(--comment-color-0-bg);
234
164
  }
235
165
 
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;
166
+ ::highlight(comment-color-1) {
167
+ background-color: var(--comment-color-1-bg);
246
168
  }
247
169
 
248
- .bracket-gutter-line[data-focused="true"],
249
- .bracket-gutter-line:hover {
250
- opacity: 1;
170
+ ::highlight(comment-color-2) {
171
+ background-color: var(--comment-color-2-bg);
251
172
  }
252
173
 
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;
174
+ ::highlight(comment-color-3) {
175
+ background-color: var(--comment-color-3-bg);
263
176
  }
264
177
 
265
- .bracket-marker-start {
266
- top: -2px;
178
+ /* Focused state — higher priority, overrides color group */
179
+ ::highlight(comment-focused) {
180
+ background-color: var(--comment-color-0-bg-focused);
267
181
  }
268
182
 
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;
@@ -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
+ });