@peaske7/readit 0.1.8 → 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.
- package/.claude/CLAUDE.md +118 -76
- package/.claude/commands/review.md +1 -1
- package/.claude/roadmap.md +32 -9
- package/.claude/user-stories.md +100 -15
- package/AGENTS.md +30 -26
- package/Makefile +32 -0
- package/README.md +90 -5
- package/biome.json +18 -8
- package/bun.lock +426 -710
- package/bunfig.toml +2 -0
- package/docs/perf-baseline.md +130 -0
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
- package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
- package/e2e/comments.spec.ts +14 -58
- package/e2e/document-load.spec.ts +1 -23
- package/e2e/export.spec.ts +4 -4
- package/e2e/perf/add-comment.spec.ts +116 -0
- package/e2e/perf/fixtures/generate.ts +327 -0
- package/e2e/perf/initial-load.spec.ts +49 -0
- package/e2e/perf/perf.setup.ts +23 -0
- package/e2e/perf/perf.teardown.ts +9 -0
- package/e2e/perf/screenshot-final.png +0 -0
- package/e2e/perf/scroll.spec.ts +39 -0
- package/e2e/perf/tab-switch.spec.ts +69 -0
- package/e2e/perf/text-selection.spec.ts +119 -0
- package/e2e/perf/utils/metrics.ts +350 -0
- package/e2e/perf/utils/perf-cli.ts +86 -0
- package/e2e/persistence-file.spec.ts +41 -26
- package/e2e/utils/selection.ts +17 -73
- package/go/cmd/readit/main.go +416 -0
- package/go/go.mod +20 -0
- package/go/go.sum +41 -0
- package/go/internal/server/anchor.go +302 -0
- package/go/internal/server/anchor_test.go +111 -0
- package/go/internal/server/comments.go +390 -0
- package/go/internal/server/documents.go +113 -0
- package/go/internal/server/embed.go +17 -0
- package/go/internal/server/headings.go +33 -0
- package/go/internal/server/headings_test.go +75 -0
- package/go/internal/server/htmltext.go +123 -0
- package/go/internal/server/markdown.go +157 -0
- package/go/internal/server/markdown_bench_test.go +42 -0
- package/go/internal/server/markdown_test.go +79 -0
- package/go/internal/server/server.go +453 -0
- package/go/internal/server/server_bench_test.go +122 -0
- package/go/internal/server/settings.go +110 -0
- package/go/internal/server/sse.go +140 -0
- package/go/internal/server/storage.go +275 -0
- package/go/internal/server/storage_test.go +118 -0
- package/go/internal/server/template.go +66 -0
- package/go/internal/server/types.go +101 -0
- package/go/internal/server/watcher.go +74 -0
- package/index.html +4 -14
- package/nvim-readit/lua/readit/health.lua +64 -0
- package/nvim-readit/lua/readit/init.lua +463 -0
- package/nvim-readit/plugin/readit.lua +19 -0
- package/package.json +24 -41
- package/playwright.config.ts +12 -0
- package/shell/_readit +158 -0
- package/shell/readit.zsh +87 -0
- package/src/App.svelte +881 -0
- package/src/{cli/index.ts → cli.ts} +216 -70
- package/src/components/ActionsMenu.svelte +95 -0
- package/src/components/CommentBadge.svelte +67 -0
- package/src/components/CommentErrorBanner.svelte +33 -0
- package/src/components/CommentInput.svelte +75 -0
- package/src/components/CommentListItem.svelte +95 -0
- package/src/components/CommentManager.svelte +129 -0
- package/src/components/CommentNav.svelte +109 -0
- package/src/components/DocumentViewer.svelte +218 -0
- package/src/components/FloatingComment.svelte +107 -0
- package/src/components/Header.svelte +76 -0
- package/src/components/InlineEditor.svelte +72 -0
- package/src/components/MarginNote.svelte +167 -0
- package/src/components/MarginNotesContainer.svelte +33 -0
- package/src/components/RawModal.svelte +126 -0
- package/src/components/ReanchorConfirm.svelte +30 -0
- package/src/components/SettingsModal.svelte +220 -0
- package/src/components/ShortcutCapture.svelte +82 -0
- package/src/components/ShortcutList.svelte +145 -0
- package/src/components/TabBar.svelte +52 -0
- package/src/components/TableOfContents.svelte +125 -0
- package/src/components/ui/ActionLink.svelte +40 -0
- package/src/components/ui/Button.svelte +53 -0
- package/src/components/ui/Dialog.svelte +97 -0
- package/src/components/ui/DropdownMenu.svelte +85 -0
- package/src/components/ui/DropdownMenuItem.svelte +38 -0
- package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
- package/src/components/ui/Text.svelte +42 -0
- package/src/env.d.ts +6 -0
- package/src/index.css +36 -166
- package/src/lib/__fixtures__/bench-data.ts +1 -54
- package/src/lib/anchor.bench.ts +47 -68
- package/src/lib/anchor.test.ts +5 -9
- package/src/lib/anchor.ts +9 -93
- package/src/lib/comment-storage.bench.ts +6 -20
- package/src/lib/comment-storage.test.ts +45 -37
- package/src/lib/comment-storage.ts +23 -64
- package/src/lib/export.bench.ts +9 -23
- package/src/lib/export.ts +7 -14
- package/src/lib/headings.test.ts +103 -0
- package/src/lib/headings.ts +44 -0
- package/src/lib/highlight/core.test.ts +1 -6
- package/src/lib/highlight/dom.ts +53 -280
- package/src/lib/highlight/highlight-registry.ts +221 -0
- package/src/lib/highlight/highlight.bench.ts +92 -0
- package/src/lib/highlight/highlighter.ts +122 -302
- package/src/lib/highlight/{core.ts → resolver.ts} +3 -19
- package/src/lib/highlight/types.ts +0 -40
- package/src/lib/html-text.test.ts +162 -0
- package/src/lib/html-text.ts +161 -0
- package/src/lib/i18n/en.ts +13 -36
- package/src/lib/i18n/ja.ts +14 -37
- package/src/lib/i18n/types.ts +13 -36
- package/src/lib/margin-layout.bench.ts +48 -15
- package/src/lib/margin-layout.ts +2 -31
- package/src/lib/markdown-renderer.test.ts +154 -0
- package/src/lib/markdown-renderer.ts +177 -0
- package/src/lib/mermaid-config.ts +38 -0
- package/src/lib/mermaid-renderer.ts +162 -0
- package/src/lib/mermaid-worker.ts +60 -0
- package/src/lib/positions.ts +157 -0
- package/src/lib/shortcut-registry.ts +138 -103
- package/src/lib/utils.ts +2 -48
- package/src/main.ts +16 -0
- package/src/schema.ts +92 -0
- package/src/{server/index.ts → server.ts} +427 -163
- package/src/stores/app.svelte.ts +231 -0
- package/src/stores/locale.svelte.ts +46 -0
- package/src/stores/settings.svelte.ts +90 -0
- package/src/stores/shortcuts.svelte.ts +104 -0
- package/src/stores/ui.svelte.ts +12 -0
- package/src/template.ts +104 -0
- package/src/test-setup.ts +47 -0
- package/svelte.config.js +5 -0
- package/tsconfig.json +2 -2
- package/vite.config.ts +31 -3
- package/vscode-readit/.mcp.json +7 -0
- package/vscode-readit/.vscodeignore +7 -0
- package/vscode-readit/bun.lock +78 -0
- package/vscode-readit/icon.svg +10 -0
- package/vscode-readit/package.json +110 -0
- package/vscode-readit/src/extension.ts +117 -0
- package/vscode-readit/src/server-manager.ts +272 -0
- package/vscode-readit/src/webview-provider.ts +204 -0
- package/vscode-readit/tsconfig.json +20 -0
- package/e2e/fixtures/sample.html +0 -13
- package/src/App.tsx +0 -416
- package/src/components/ActionsMenu.tsx +0 -112
- package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
- package/src/components/DocumentViewer/DocumentViewer.tsx +0 -259
- package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
- package/src/components/DocumentViewer/InlineCode.tsx +0 -60
- package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -137
- package/src/components/DocumentViewer/index.ts +0 -1
- package/src/components/FloatingTOC.tsx +0 -61
- package/src/components/Header.tsx +0 -65
- package/src/components/InlineEditor.tsx +0 -74
- package/src/components/MarginNote.tsx +0 -207
- package/src/components/MarginNotes.tsx +0 -50
- package/src/components/RawModal.tsx +0 -143
- package/src/components/ReanchorConfirm.tsx +0 -36
- package/src/components/SettingsModal.tsx +0 -310
- package/src/components/ShortcutCapture.tsx +0 -48
- package/src/components/ShortcutList.tsx +0 -198
- package/src/components/TabBar.tsx +0 -60
- package/src/components/TableOfContents.tsx +0 -108
- package/src/components/comments/CommentBadge.tsx +0 -49
- package/src/components/comments/CommentInput.tsx +0 -114
- package/src/components/comments/CommentListItem.tsx +0 -92
- package/src/components/comments/CommentManager.tsx +0 -113
- package/src/components/comments/CommentMinimap.tsx +0 -62
- package/src/components/comments/CommentNav.tsx +0 -109
- package/src/components/ui/ActionBar.tsx +0 -16
- package/src/components/ui/ActionLink.tsx +0 -32
- package/src/components/ui/Button.tsx +0 -55
- package/src/components/ui/Dialog.tsx +0 -156
- package/src/components/ui/DropdownMenu.tsx +0 -114
- package/src/components/ui/SeparatorDot.tsx +0 -9
- package/src/components/ui/Text.tsx +0 -54
- package/src/contexts/CommentContext.tsx +0 -229
- package/src/contexts/LayoutContext.tsx +0 -88
- package/src/contexts/LocaleContext.tsx +0 -35
- package/src/hooks/useClickOutside.ts +0 -35
- package/src/hooks/useClipboard.ts +0 -82
- package/src/hooks/useCommentNavigation.ts +0 -130
- package/src/hooks/useComments.ts +0 -323
- package/src/hooks/useDocument.ts +0 -156
- package/src/hooks/useEditorScheme.ts +0 -51
- package/src/hooks/useFontPreference.ts +0 -59
- package/src/hooks/useHeadings.test.ts +0 -159
- package/src/hooks/useHeadings.ts +0 -129
- package/src/hooks/useKeybindings.ts +0 -108
- package/src/hooks/useKeyboardShortcuts.ts +0 -63
- package/src/hooks/useLayoutMode.ts +0 -44
- package/src/hooks/useLocalePreference.ts +0 -42
- package/src/hooks/useReanchorMode.ts +0 -33
- package/src/hooks/useScrollMetrics.ts +0 -56
- package/src/hooks/useScrollSpy.ts +0 -81
- package/src/hooks/useTextSelection.ts +0 -123
- package/src/hooks/useThemePreference.ts +0 -66
- package/src/lib/context.bench.ts +0 -41
- package/src/lib/context.test.ts +0 -224
- package/src/lib/context.ts +0 -193
- package/src/lib/editor-links.ts +0 -59
- package/src/lib/highlight/colors.ts +0 -37
- package/src/lib/highlight/index.ts +0 -23
- package/src/lib/highlight/script-builder.ts +0 -485
- package/src/lib/html-processor.test.tsx +0 -170
- package/src/lib/html-processor.tsx +0 -95
- package/src/lib/i18n/completeness.test.ts +0 -51
- package/src/lib/i18n/translations.test.ts +0 -39
- package/src/lib/layout-constants.ts +0 -12
- package/src/lib/scroll.test.ts +0 -118
- package/src/lib/scroll.ts +0 -47
- package/src/lib/shortcut-registry.test.ts +0 -173
- package/src/lib/utils.test.ts +0 -110
- package/src/main.tsx +0 -13
- package/src/store/index.test.ts +0 -242
- package/src/store/index.ts +0 -254
- package/src/types/index.ts +0 -127
package/src/lib/anchor.bench.ts
CHANGED
|
@@ -1,112 +1,91 @@
|
|
|
1
1
|
import { bench, describe } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
COMMENTS_1,
|
|
4
|
+
COMMENTS_10,
|
|
5
|
+
COMMENTS_50,
|
|
6
|
+
LARGE_DOC,
|
|
7
|
+
MEDIUM_DOC,
|
|
8
|
+
} from "./__fixtures__/bench-data";
|
|
3
9
|
import {
|
|
4
10
|
findAnchor,
|
|
5
11
|
findAnchorFuzzy,
|
|
6
12
|
findAnchorNormalized,
|
|
7
13
|
findAnchorWithFallback,
|
|
8
|
-
levenshteinDistance,
|
|
9
14
|
} from "./anchor";
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
"The conclusion of section 8 summarizes the key findings";
|
|
14
|
-
const EXACT_TEXT_L250 = "- Item 1 in section 14";
|
|
15
|
-
const WHITESPACE_TEXT = "Item 1 in\n section 14";
|
|
16
|
-
const FUZZY_TEXT = "- Item 1 in section 1x"; // 1-char typo
|
|
17
|
-
|
|
18
|
-
describe("levenshteinDistance", () => {
|
|
19
|
-
const str20 = "hello world testing!";
|
|
20
|
-
|
|
21
|
-
bench("identical strings (20 chars)", () => {
|
|
22
|
-
levenshteinDistance(str20, str20);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
bench("1 edit distance (20 chars)", () => {
|
|
26
|
-
levenshteinDistance("hello world testing!", "hello world testinx!");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
bench("maxDistance early exit", () => {
|
|
30
|
-
levenshteinDistance("completely different", "nothing alike here!", 3);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const longA = "a".repeat(50) + "b".repeat(50);
|
|
34
|
-
const longB = "a".repeat(50) + "c".repeat(50);
|
|
35
|
-
|
|
36
|
-
bench("longer strings (100 chars)", () => {
|
|
37
|
-
levenshteinDistance(longA, longB);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
16
|
+
describe("findAnchor — exact match", () => {
|
|
17
|
+
const comment = COMMENTS_10[5];
|
|
40
18
|
|
|
41
|
-
|
|
42
|
-
bench("300-line doc, correct hint", () => {
|
|
19
|
+
bench("medium doc (150 lines)", () => {
|
|
43
20
|
findAnchor({
|
|
44
|
-
source:
|
|
45
|
-
selectedText:
|
|
46
|
-
lineHint: "
|
|
21
|
+
source: MEDIUM_DOC,
|
|
22
|
+
selectedText: comment.selectedText,
|
|
23
|
+
lineHint: comment.lineHint ?? "L1",
|
|
47
24
|
});
|
|
48
25
|
});
|
|
49
26
|
|
|
50
|
-
bench("
|
|
27
|
+
bench("large doc (300 lines)", () => {
|
|
51
28
|
findAnchor({
|
|
52
29
|
source: LARGE_DOC,
|
|
53
|
-
selectedText:
|
|
54
|
-
lineHint: "
|
|
30
|
+
selectedText: comment.selectedText,
|
|
31
|
+
lineHint: comment.lineHint ?? "L1",
|
|
55
32
|
});
|
|
56
33
|
});
|
|
57
34
|
});
|
|
58
35
|
|
|
59
36
|
describe("findAnchorNormalized", () => {
|
|
60
|
-
|
|
37
|
+
const comment = COMMENTS_10[5];
|
|
38
|
+
const normalizedText = comment.selectedText.replace(/ /g, " ");
|
|
39
|
+
|
|
40
|
+
bench("large doc — normalized whitespace", () => {
|
|
61
41
|
findAnchorNormalized({
|
|
62
42
|
source: LARGE_DOC,
|
|
63
|
-
selectedText:
|
|
64
|
-
lineHint: "
|
|
43
|
+
selectedText: normalizedText,
|
|
44
|
+
lineHint: comment.lineHint ?? "L1",
|
|
65
45
|
});
|
|
66
46
|
});
|
|
67
47
|
});
|
|
68
48
|
|
|
69
49
|
describe("findAnchorFuzzy", () => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
source: LARGE_DOC,
|
|
73
|
-
selectedText: FUZZY_TEXT,
|
|
74
|
-
lineHint: "L250",
|
|
75
|
-
threshold: 3,
|
|
76
|
-
});
|
|
77
|
-
});
|
|
50
|
+
const comment = COMMENTS_10[5];
|
|
51
|
+
const mutated = `X${comment.selectedText.slice(1, -1)}Z`;
|
|
78
52
|
|
|
79
|
-
bench("
|
|
53
|
+
bench("large doc — fuzzy (mutated text)", () => {
|
|
80
54
|
findAnchorFuzzy({
|
|
81
55
|
source: LARGE_DOC,
|
|
82
|
-
selectedText:
|
|
83
|
-
|
|
56
|
+
selectedText: mutated.slice(0, 50), // Keep within MAX_FUZZY_TEXT_LENGTH
|
|
57
|
+
lineHint: comment.lineHint ?? "L1",
|
|
84
58
|
});
|
|
85
59
|
});
|
|
86
60
|
});
|
|
87
61
|
|
|
88
62
|
describe("findAnchorWithFallback", () => {
|
|
89
|
-
bench("
|
|
63
|
+
bench("1 comment — exact hit", () => {
|
|
64
|
+
const c = COMMENTS_1[0];
|
|
90
65
|
findAnchorWithFallback({
|
|
91
66
|
source: LARGE_DOC,
|
|
92
|
-
selectedText:
|
|
93
|
-
lineHint: "
|
|
67
|
+
selectedText: c.selectedText,
|
|
68
|
+
lineHint: c.lineHint ?? "L1",
|
|
94
69
|
});
|
|
95
70
|
});
|
|
96
71
|
|
|
97
|
-
bench("
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
72
|
+
bench("10 comments — exact hits", () => {
|
|
73
|
+
for (const c of COMMENTS_10) {
|
|
74
|
+
findAnchorWithFallback({
|
|
75
|
+
source: LARGE_DOC,
|
|
76
|
+
selectedText: c.selectedText,
|
|
77
|
+
lineHint: c.lineHint ?? "L1",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
103
80
|
});
|
|
104
81
|
|
|
105
|
-
bench("
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
82
|
+
bench("50 comments — exact hits", () => {
|
|
83
|
+
for (const c of COMMENTS_50) {
|
|
84
|
+
findAnchorWithFallback({
|
|
85
|
+
source: LARGE_DOC,
|
|
86
|
+
selectedText: c.selectedText,
|
|
87
|
+
lineHint: c.lineHint ?? "L1",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
111
90
|
});
|
|
112
91
|
});
|
package/src/lib/anchor.test.ts
CHANGED
|
@@ -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
|
);
|
|
@@ -135,8 +133,12 @@ describe("parseLineHint", () => {
|
|
|
135
133
|
});
|
|
136
134
|
|
|
137
135
|
it("parses line range hint", () => {
|
|
136
|
+
expect(parseLineHint("L42-L45")).toEqual({ start: 42, end: 45 });
|
|
137
|
+
expect(parseLineHint("L10-L20")).toEqual({ start: 10, end: 20 });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("parses legacy format without L prefix on end line", () => {
|
|
138
141
|
expect(parseLineHint("L42-45")).toEqual({ start: 42, end: 45 });
|
|
139
|
-
expect(parseLineHint("L10-20")).toEqual({ start: 10, end: 20 });
|
|
140
142
|
});
|
|
141
143
|
|
|
142
144
|
it("returns default for invalid hint", () => {
|
|
@@ -248,7 +250,6 @@ line eight`;
|
|
|
248
250
|
|
|
249
251
|
describe("findAnchorNormalized", () => {
|
|
250
252
|
it("finds text with collapsed whitespace", () => {
|
|
251
|
-
// Original had "hello world" but source was reformatted
|
|
252
253
|
const source = "hello\n world";
|
|
253
254
|
const result = findAnchorNormalized({
|
|
254
255
|
source,
|
|
@@ -271,7 +272,6 @@ describe("findAnchorNormalized", () => {
|
|
|
271
272
|
});
|
|
272
273
|
|
|
273
274
|
it("returns null when text has no collapsible whitespace", () => {
|
|
274
|
-
// If original text has no extra whitespace, exact match would have worked
|
|
275
275
|
const result = findAnchorNormalized({
|
|
276
276
|
source: "hello world",
|
|
277
277
|
selectedText: "hello world",
|
|
@@ -407,7 +407,6 @@ describe("findAnchorWithFallback", () => {
|
|
|
407
407
|
});
|
|
408
408
|
|
|
409
409
|
it("falls back to normalized match when exact fails", () => {
|
|
410
|
-
// Source was reformatted (newlines instead of spaces)
|
|
411
410
|
const result = findAnchorWithFallback({
|
|
412
411
|
source: "hello\nworld",
|
|
413
412
|
selectedText: "hello world",
|
|
@@ -440,7 +439,6 @@ describe("findAnchorWithFallback", () => {
|
|
|
440
439
|
describe("findClosestOccurrence", () => {
|
|
441
440
|
it("finds the occurrence closest to hint", () => {
|
|
442
441
|
const content = "the cat sat on the mat and the rat";
|
|
443
|
-
// "the" appears at positions 0, 15, and 27
|
|
444
442
|
|
|
445
443
|
const result = findClosestOccurrence({
|
|
446
444
|
source: content,
|
|
@@ -448,7 +446,6 @@ describe("findClosestOccurrence", () => {
|
|
|
448
446
|
lineHint: "L1",
|
|
449
447
|
});
|
|
450
448
|
expect(result).not.toBeUndefined();
|
|
451
|
-
// Should find the first "the" at position 0 since hint is L1
|
|
452
449
|
expect(result?.start).toBe(0);
|
|
453
450
|
});
|
|
454
451
|
|
|
@@ -459,7 +456,6 @@ line three the
|
|
|
459
456
|
line four
|
|
460
457
|
line five the`;
|
|
461
458
|
|
|
462
|
-
// Test finding closest to line 3
|
|
463
459
|
const result = findClosestOccurrence({
|
|
464
460
|
source: content,
|
|
465
461
|
selectedText: "the",
|
package/src/lib/anchor.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import { type Anchor, AnchorConfidences } from "../
|
|
1
|
+
import { type Anchor, AnchorConfidences } from "../schema";
|
|
2
2
|
import { getLineNumber } from "./comment-storage";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const FUZZY_SEARCH_WINDOW = 2000; // larger window for fuzzy search near line hint
|
|
4
|
+
const DEFAULT_SEARCH_WINDOW = 500;
|
|
5
|
+
const DEFAULT_FUZZY_THRESHOLD = 5;
|
|
6
|
+
const MAX_FUZZY_TEXT_LENGTH = 200;
|
|
7
|
+
const FUZZY_SEARCH_WINDOW = 2000;
|
|
9
8
|
|
|
10
|
-
/**
|
|
11
|
-
* Common parameters for anchor finding functions.
|
|
12
|
-
* Using object destructuring per style guide §3.5 for multiple string parameters.
|
|
13
|
-
*/
|
|
14
9
|
export interface FindAnchorParams {
|
|
15
10
|
source: string;
|
|
16
11
|
selectedText: string;
|
|
@@ -32,27 +27,15 @@ export interface FindAnchorWithFallbackParams {
|
|
|
32
27
|
fuzzyThreshold?: number;
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
/**
|
|
36
|
-
* Normalize whitespace for comparison: collapse runs of whitespace to single space.
|
|
37
|
-
* This allows matching text that was reformatted (line breaks, indentation changes).
|
|
38
|
-
*/
|
|
39
30
|
export function normalizeWhitespace(text: string): string {
|
|
40
31
|
return text.replace(/\s+/g, " ").trim();
|
|
41
32
|
}
|
|
42
33
|
|
|
43
|
-
/**
|
|
44
|
-
* Calculate Levenshtein distance between two strings.
|
|
45
|
-
* Uses Wagner-Fischer algorithm with O(min(m,n)) space.
|
|
46
|
-
*
|
|
47
|
-
* @param maxDistance Optional early exit threshold. If set, returns Infinity
|
|
48
|
-
* when distance is guaranteed to exceed this value.
|
|
49
|
-
*/
|
|
50
34
|
export function levenshteinDistance(
|
|
51
35
|
a: string,
|
|
52
36
|
b: string,
|
|
53
37
|
maxDistance?: number,
|
|
54
38
|
): number {
|
|
55
|
-
// Ensure a is the shorter string for space optimization
|
|
56
39
|
if (a.length > b.length) {
|
|
57
40
|
[a, b] = [b, a];
|
|
58
41
|
}
|
|
@@ -60,20 +43,16 @@ export function levenshteinDistance(
|
|
|
60
43
|
const m = a.length;
|
|
61
44
|
const n = b.length;
|
|
62
45
|
|
|
63
|
-
// Early termination for empty strings
|
|
64
46
|
if (m === 0) return n;
|
|
65
47
|
if (n === 0) return m;
|
|
66
48
|
|
|
67
|
-
// Early exit: length difference alone exceeds threshold
|
|
68
49
|
if (maxDistance !== undefined && Math.abs(m - n) > maxDistance) {
|
|
69
50
|
return Number.POSITIVE_INFINITY;
|
|
70
51
|
}
|
|
71
52
|
|
|
72
|
-
// Use single row for space optimization
|
|
73
53
|
let prevRow = new Array(m + 1);
|
|
74
54
|
let currRow = new Array(m + 1);
|
|
75
55
|
|
|
76
|
-
// Initialize first row
|
|
77
56
|
for (let i = 0; i <= m; i++) {
|
|
78
57
|
prevRow[i] = i;
|
|
79
58
|
}
|
|
@@ -81,20 +60,18 @@ export function levenshteinDistance(
|
|
|
81
60
|
for (let j = 1; j <= n; j++) {
|
|
82
61
|
currRow[0] = j;
|
|
83
62
|
|
|
84
|
-
// Track minimum value in this row for early exit
|
|
85
63
|
let rowMin = currRow[0];
|
|
86
64
|
|
|
87
65
|
for (let i = 1; i <= m; i++) {
|
|
88
66
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
89
67
|
currRow[i] = Math.min(
|
|
90
|
-
prevRow[i] + 1,
|
|
91
|
-
currRow[i - 1] + 1,
|
|
92
|
-
prevRow[i - 1] + cost,
|
|
68
|
+
prevRow[i] + 1,
|
|
69
|
+
currRow[i - 1] + 1,
|
|
70
|
+
prevRow[i - 1] + cost,
|
|
93
71
|
);
|
|
94
72
|
rowMin = Math.min(rowMin, currRow[i]);
|
|
95
73
|
}
|
|
96
74
|
|
|
97
|
-
// Early exit: minimum possible distance exceeds threshold
|
|
98
75
|
if (maxDistance !== undefined && rowMin > maxDistance) {
|
|
99
76
|
return Number.POSITIVE_INFINITY;
|
|
100
77
|
}
|
|
@@ -105,9 +82,6 @@ export function levenshteinDistance(
|
|
|
105
82
|
return prevRow[m];
|
|
106
83
|
}
|
|
107
84
|
|
|
108
|
-
/**
|
|
109
|
-
* Get character offset for the start of a line number (1-indexed).
|
|
110
|
-
*/
|
|
111
85
|
export function getLineOffset(content: string, lineNumber: number): number {
|
|
112
86
|
if (lineNumber <= 1) return 0;
|
|
113
87
|
|
|
@@ -125,15 +99,11 @@ export function getLineOffset(content: string, lineNumber: number): number {
|
|
|
125
99
|
return content.length;
|
|
126
100
|
}
|
|
127
101
|
|
|
128
|
-
/**
|
|
129
|
-
* Parse line hint string to get line number(s).
|
|
130
|
-
* Supports "L42" and "L42-45" formats.
|
|
131
|
-
*/
|
|
132
102
|
export function parseLineHint(lineHint: string): {
|
|
133
103
|
start: number;
|
|
134
104
|
end: number;
|
|
135
105
|
} {
|
|
136
|
-
const match = lineHint.match(/^L(\d+)(?:-(\d+))?$/);
|
|
106
|
+
const match = lineHint.match(/^L(\d+)(?:-L?(\d+))?$/);
|
|
137
107
|
if (!match) {
|
|
138
108
|
return { start: 1, end: 1 };
|
|
139
109
|
}
|
|
@@ -143,10 +113,6 @@ export function parseLineHint(lineHint: string): {
|
|
|
143
113
|
return { start, end };
|
|
144
114
|
}
|
|
145
115
|
|
|
146
|
-
/**
|
|
147
|
-
* Find anchor position for selected text in source content.
|
|
148
|
-
* Uses line hint for fast lookup, falls back to global search.
|
|
149
|
-
*/
|
|
150
116
|
export function findAnchor({
|
|
151
117
|
source,
|
|
152
118
|
selectedText,
|
|
@@ -158,7 +124,6 @@ export function findAnchor({
|
|
|
158
124
|
}
|
|
159
125
|
const { start: hintLine } = parseLineHint(lineHint);
|
|
160
126
|
|
|
161
|
-
// Fast path: search near line hint
|
|
162
127
|
const lineOffset = getLineOffset(source, hintLine);
|
|
163
128
|
const windowStart = Math.max(0, lineOffset - searchWindow);
|
|
164
129
|
const windowEnd = Math.min(source.length, lineOffset + searchWindow);
|
|
@@ -176,7 +141,6 @@ export function findAnchor({
|
|
|
176
141
|
};
|
|
177
142
|
}
|
|
178
143
|
|
|
179
|
-
// Fallback: global search
|
|
180
144
|
const globalIndex = source.indexOf(selectedText);
|
|
181
145
|
if (globalIndex !== -1) {
|
|
182
146
|
return {
|
|
@@ -190,10 +154,6 @@ export function findAnchor({
|
|
|
190
154
|
return undefined;
|
|
191
155
|
}
|
|
192
156
|
|
|
193
|
-
/**
|
|
194
|
-
* Build a position map from normalized string positions back to original positions.
|
|
195
|
-
* Returns array where normalizedToOriginal[i] = original position for normalized char i.
|
|
196
|
-
*/
|
|
197
157
|
function buildNormalizedPositionMap(text: string): {
|
|
198
158
|
normalized: string;
|
|
199
159
|
toOriginal: number[];
|
|
@@ -208,7 +168,6 @@ function buildNormalizedPositionMap(text: string): {
|
|
|
208
168
|
|
|
209
169
|
if (isSpace) {
|
|
210
170
|
if (!inWhitespace && normalized.length > 0) {
|
|
211
|
-
// First whitespace after content - emit single space
|
|
212
171
|
normalized += " ";
|
|
213
172
|
toOriginal.push(i);
|
|
214
173
|
}
|
|
@@ -220,7 +179,6 @@ function buildNormalizedPositionMap(text: string): {
|
|
|
220
179
|
}
|
|
221
180
|
}
|
|
222
181
|
|
|
223
|
-
// Trim trailing space
|
|
224
182
|
if (normalized.endsWith(" ")) {
|
|
225
183
|
normalized = normalized.slice(0, -1);
|
|
226
184
|
toOriginal.pop();
|
|
@@ -229,16 +187,6 @@ function buildNormalizedPositionMap(text: string): {
|
|
|
229
187
|
return { normalized, toOriginal };
|
|
230
188
|
}
|
|
231
189
|
|
|
232
|
-
/**
|
|
233
|
-
* Find anchor using whitespace-normalized matching.
|
|
234
|
-
* Useful when document was reformatted but content is unchanged.
|
|
235
|
-
* Returns "normalized" confidence level.
|
|
236
|
-
*
|
|
237
|
-
* Algorithm:
|
|
238
|
-
* 1. Normalize source and build position map
|
|
239
|
-
* 2. Find normalized text in normalized source (fast substring search)
|
|
240
|
-
* 3. Map positions back to original source
|
|
241
|
-
*/
|
|
242
190
|
export function findAnchorNormalized({
|
|
243
191
|
source,
|
|
244
192
|
selectedText,
|
|
@@ -254,31 +202,24 @@ export function findAnchorNormalized({
|
|
|
254
202
|
return undefined;
|
|
255
203
|
}
|
|
256
204
|
|
|
257
|
-
// Skip if text has no collapsible whitespace (exact match would have worked)
|
|
258
205
|
if (normalizedText === selectedText) {
|
|
259
206
|
return undefined;
|
|
260
207
|
}
|
|
261
208
|
const { start: hintLine } = parseLineHint(lineHint);
|
|
262
209
|
const lineOffset = getLineOffset(source, hintLine);
|
|
263
210
|
|
|
264
|
-
// Define search window
|
|
265
211
|
const windowStart = Math.max(0, lineOffset - searchWindow);
|
|
266
212
|
const windowEnd = Math.min(source.length, lineOffset + searchWindow);
|
|
267
213
|
const window = source.slice(windowStart, windowEnd);
|
|
268
214
|
|
|
269
|
-
// Build normalized version with position mapping
|
|
270
215
|
const { normalized: normalizedWindow, toOriginal } =
|
|
271
216
|
buildNormalizedPositionMap(window);
|
|
272
217
|
|
|
273
|
-
// Fast substring search on normalized text
|
|
274
218
|
const normalizedIndex = normalizedWindow.indexOf(normalizedText);
|
|
275
219
|
if (normalizedIndex !== -1) {
|
|
276
|
-
// Map back to original positions
|
|
277
220
|
const originalStart = windowStart + toOriginal[normalizedIndex];
|
|
278
221
|
const endNormIndex = normalizedIndex + normalizedText.length - 1;
|
|
279
|
-
// Find original end: scan forward from mapped position to include trailing whitespace
|
|
280
222
|
let originalEnd = windowStart + toOriginal[endNormIndex] + 1;
|
|
281
|
-
// Extend to include any trailing whitespace that was collapsed
|
|
282
223
|
while (originalEnd < source.length && /\s/.test(source[originalEnd])) {
|
|
283
224
|
originalEnd++;
|
|
284
225
|
}
|
|
@@ -291,7 +232,6 @@ export function findAnchorNormalized({
|
|
|
291
232
|
};
|
|
292
233
|
}
|
|
293
234
|
|
|
294
|
-
// Global fallback (outside hint window)
|
|
295
235
|
const { normalized: fullNormalized, toOriginal: fullToOriginal } =
|
|
296
236
|
buildNormalizedPositionMap(source);
|
|
297
237
|
const globalIndex = fullNormalized.indexOf(normalizedText);
|
|
@@ -314,10 +254,6 @@ export function findAnchorNormalized({
|
|
|
314
254
|
return undefined;
|
|
315
255
|
}
|
|
316
256
|
|
|
317
|
-
/**
|
|
318
|
-
* Find anchor using fuzzy matching with Levenshtein distance.
|
|
319
|
-
* Scans the source for substrings similar to the selected text.
|
|
320
|
-
*/
|
|
321
257
|
export function findAnchorFuzzy({
|
|
322
258
|
source,
|
|
323
259
|
selectedText,
|
|
@@ -330,7 +266,6 @@ export function findAnchorFuzzy({
|
|
|
330
266
|
|
|
331
267
|
const textLen = selectedText.length;
|
|
332
268
|
|
|
333
|
-
// For very long texts, skip fuzzy matching (too expensive)
|
|
334
269
|
if (textLen > MAX_FUZZY_TEXT_LENGTH) {
|
|
335
270
|
return undefined;
|
|
336
271
|
}
|
|
@@ -338,26 +273,22 @@ export function findAnchorFuzzy({
|
|
|
338
273
|
let bestMatch: Anchor | undefined;
|
|
339
274
|
let bestDistance = threshold + 1;
|
|
340
275
|
|
|
341
|
-
// Determine search range based on line hint
|
|
342
276
|
let searchStart = 0;
|
|
343
277
|
let searchEnd = source.length;
|
|
344
278
|
|
|
345
279
|
if (lineHint) {
|
|
346
280
|
const { start: hintLine } = parseLineHint(lineHint);
|
|
347
281
|
const lineOffset = getLineOffset(source, hintLine);
|
|
348
|
-
// Search in a larger window for fuzzy matching
|
|
349
282
|
searchStart = Math.max(0, lineOffset - FUZZY_SEARCH_WINDOW);
|
|
350
283
|
searchEnd = Math.min(source.length, lineOffset + FUZZY_SEARCH_WINDOW);
|
|
351
284
|
}
|
|
352
285
|
|
|
353
|
-
// Slide window of similar length through the search range
|
|
354
286
|
const minLen = Math.max(1, textLen - threshold);
|
|
355
287
|
const maxLen = textLen + threshold;
|
|
356
288
|
|
|
357
289
|
for (let len = minLen; len <= maxLen; len++) {
|
|
358
290
|
for (let i = searchStart; i <= searchEnd - len; i++) {
|
|
359
291
|
const candidate = source.slice(i, i + len);
|
|
360
|
-
// Use early exit: only compute if distance could improve on current best
|
|
361
292
|
const distance = levenshteinDistance(
|
|
362
293
|
selectedText,
|
|
363
294
|
candidate,
|
|
@@ -374,7 +305,6 @@ export function findAnchorFuzzy({
|
|
|
374
305
|
distance,
|
|
375
306
|
};
|
|
376
307
|
|
|
377
|
-
// Early exit if we found an exact match
|
|
378
308
|
if (distance === 0) {
|
|
379
309
|
return bestMatch;
|
|
380
310
|
}
|
|
@@ -385,27 +315,17 @@ export function findAnchorFuzzy({
|
|
|
385
315
|
return bestMatch;
|
|
386
316
|
}
|
|
387
317
|
|
|
388
|
-
/**
|
|
389
|
-
* Find anchor with fallback chain: exact → normalized → fuzzy.
|
|
390
|
-
*
|
|
391
|
-
* Matching strategies in order of preference:
|
|
392
|
-
* 1. Exact: Fast substring match (O(n))
|
|
393
|
-
* 2. Normalized: Whitespace-collapsed match for reformatted text (O(n))
|
|
394
|
-
* 3. Fuzzy: Levenshtein distance for small edits (O(n × m × threshold))
|
|
395
|
-
*/
|
|
396
318
|
export function findAnchorWithFallback({
|
|
397
319
|
source,
|
|
398
320
|
selectedText,
|
|
399
321
|
lineHint,
|
|
400
322
|
fuzzyThreshold,
|
|
401
323
|
}: FindAnchorWithFallbackParams): Anchor | undefined {
|
|
402
|
-
// Try exact match first (fastest)
|
|
403
324
|
const exactMatch = findAnchor({ source, selectedText, lineHint });
|
|
404
325
|
if (exactMatch) {
|
|
405
326
|
return exactMatch;
|
|
406
327
|
}
|
|
407
328
|
|
|
408
|
-
// Try normalized match (handles reformatting)
|
|
409
329
|
const normalizedMatch = findAnchorNormalized({
|
|
410
330
|
source,
|
|
411
331
|
selectedText,
|
|
@@ -415,7 +335,6 @@ export function findAnchorWithFallback({
|
|
|
415
335
|
return normalizedMatch;
|
|
416
336
|
}
|
|
417
337
|
|
|
418
|
-
// Fall back to fuzzy matching (handles small edits)
|
|
419
338
|
return findAnchorFuzzy({
|
|
420
339
|
source,
|
|
421
340
|
selectedText,
|
|
@@ -424,9 +343,6 @@ export function findAnchorWithFallback({
|
|
|
424
343
|
});
|
|
425
344
|
}
|
|
426
345
|
|
|
427
|
-
/**
|
|
428
|
-
* Find the closest match when multiple occurrences exist.
|
|
429
|
-
*/
|
|
430
346
|
export function findClosestOccurrence({
|
|
431
347
|
source,
|
|
432
348
|
selectedText,
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
COMMENT_FILE_MEDIUM,
|
|
5
5
|
COMMENT_FILE_OBJ_LARGE,
|
|
6
6
|
COMMENT_FILE_OBJ_MEDIUM,
|
|
7
|
+
COMMENT_FILE_OBJ_SMALL,
|
|
7
8
|
COMMENT_FILE_SMALL,
|
|
8
9
|
LARGE_DOC,
|
|
9
10
|
} from "./__fixtures__/bench-data";
|
|
10
11
|
import {
|
|
11
12
|
computeHash,
|
|
12
|
-
createComment,
|
|
13
13
|
parseCommentFile,
|
|
14
14
|
serializeComments,
|
|
15
15
|
} from "./comment-storage";
|
|
@@ -29,6 +29,10 @@ describe("parseCommentFile", () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
describe("serializeComments", () => {
|
|
32
|
+
bench("1 comment", () => {
|
|
33
|
+
serializeComments(COMMENT_FILE_OBJ_SMALL);
|
|
34
|
+
});
|
|
35
|
+
|
|
32
36
|
bench("10 comments", () => {
|
|
33
37
|
serializeComments(COMMENT_FILE_OBJ_MEDIUM);
|
|
34
38
|
});
|
|
@@ -39,25 +43,7 @@ describe("serializeComments", () => {
|
|
|
39
43
|
});
|
|
40
44
|
|
|
41
45
|
describe("computeHash", () => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
bench("short string (100 chars)", () => {
|
|
45
|
-
computeHash(shortString);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
bench("large doc (~10k chars)", () => {
|
|
46
|
+
bench("300-line document", () => {
|
|
49
47
|
computeHash(LARGE_DOC);
|
|
50
48
|
});
|
|
51
49
|
});
|
|
52
|
-
|
|
53
|
-
describe("createComment", () => {
|
|
54
|
-
bench("short selection", () => {
|
|
55
|
-
createComment("selected text here", "my comment", 100, 118, LARGE_DOC);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const longSelection = "a".repeat(2000);
|
|
59
|
-
|
|
60
|
-
bench("long selection (triggers truncation)", () => {
|
|
61
|
-
createComment(longSelection, "my comment", 0, 2000, LARGE_DOC);
|
|
62
|
-
});
|
|
63
|
-
});
|