@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
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
package server
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"regexp"
|
|
6
|
+
"strings"
|
|
7
|
+
"unicode/utf8"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
var lineHintRe = regexp.MustCompile(`^L(\d+)(?:-L?(\d+))?$`)
|
|
11
|
+
|
|
12
|
+
type AnchorResult struct {
|
|
13
|
+
StartOffset int
|
|
14
|
+
EndOffset int
|
|
15
|
+
Confidence string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func ParseLineHint(hint string) (start, end int) {
|
|
19
|
+
m := lineHintRe.FindStringSubmatch(hint)
|
|
20
|
+
if m == nil {
|
|
21
|
+
return 1, 1
|
|
22
|
+
}
|
|
23
|
+
_, _ = fmt.Sscanf(m[1], "%d", &start)
|
|
24
|
+
if m[2] != "" {
|
|
25
|
+
_, _ = fmt.Sscanf(m[2], "%d", &end)
|
|
26
|
+
} else {
|
|
27
|
+
end = start
|
|
28
|
+
}
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// lineOffset returns the byte offset of the start of the given 1-indexed line.
|
|
33
|
+
func lineOffset(content string, line int) int {
|
|
34
|
+
if line <= 1 {
|
|
35
|
+
return 0
|
|
36
|
+
}
|
|
37
|
+
n := 0
|
|
38
|
+
for i, c := range content {
|
|
39
|
+
if c == '\n' {
|
|
40
|
+
n++
|
|
41
|
+
if n == line-1 {
|
|
42
|
+
return i + 1
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return len(content)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func FindAnchor(source, selectedText, lineHint string) *AnchorResult {
|
|
50
|
+
if selectedText == "" {
|
|
51
|
+
return nil
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Try exact match near hint first
|
|
55
|
+
hintLine, _ := ParseLineHint(lineHint)
|
|
56
|
+
offset := lineOffset(source, hintLine)
|
|
57
|
+
|
|
58
|
+
windowStart := max(0, offset-DefaultSearchWindow)
|
|
59
|
+
windowEnd := min(len(source), offset+DefaultSearchWindow)
|
|
60
|
+
window := source[windowStart:windowEnd]
|
|
61
|
+
|
|
62
|
+
if idx := strings.Index(window, selectedText); idx >= 0 {
|
|
63
|
+
start := windowStart + idx
|
|
64
|
+
return &AnchorResult{
|
|
65
|
+
StartOffset: start,
|
|
66
|
+
EndOffset: start + len(selectedText),
|
|
67
|
+
Confidence: AnchorExact,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fall back to full source
|
|
72
|
+
if idx := strings.Index(source, selectedText); idx >= 0 {
|
|
73
|
+
return &AnchorResult{
|
|
74
|
+
StartOffset: idx,
|
|
75
|
+
EndOffset: idx + len(selectedText),
|
|
76
|
+
Confidence: AnchorExact,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return nil
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func normalizeWhitespace(s string) string {
|
|
84
|
+
var b strings.Builder
|
|
85
|
+
inSpace := false
|
|
86
|
+
for _, r := range s {
|
|
87
|
+
if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
|
|
88
|
+
if !inSpace {
|
|
89
|
+
b.WriteByte(' ')
|
|
90
|
+
inSpace = true
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
b.WriteRune(r)
|
|
94
|
+
inSpace = false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return strings.TrimSpace(b.String())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func FindAnchorNormalized(source, selectedText, lineHint string) *AnchorResult {
|
|
101
|
+
normText := normalizeWhitespace(selectedText)
|
|
102
|
+
normSource := normalizeWhitespace(source)
|
|
103
|
+
if normText == selectedText && normSource == source {
|
|
104
|
+
return nil // no normalization needed
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Try windowed search near the hint first
|
|
108
|
+
hintLine, _ := ParseLineHint(lineHint)
|
|
109
|
+
offset := lineOffset(source, hintLine)
|
|
110
|
+
normCharOffset := mapSourceToNormCharOffset(source, offset)
|
|
111
|
+
|
|
112
|
+
normRunes := []rune(normSource)
|
|
113
|
+
normTextRunes := []rune(normText)
|
|
114
|
+
normTextRuneLen := len(normTextRunes)
|
|
115
|
+
|
|
116
|
+
windowStart := max(0, normCharOffset-DefaultSearchWindow)
|
|
117
|
+
windowEnd := min(len(normRunes), normCharOffset+DefaultSearchWindow)
|
|
118
|
+
window := string(normRunes[windowStart:windowEnd])
|
|
119
|
+
|
|
120
|
+
if before, _, ok := strings.Cut(window, normText); ok {
|
|
121
|
+
charIdx := utf8.RuneCountInString(before)
|
|
122
|
+
return resolveNormalizedMatch(source, windowStart+charIdx, normTextRuneLen)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Fall back to full source
|
|
126
|
+
if before, _, ok := strings.Cut(normSource, normText); ok {
|
|
127
|
+
charIdx := utf8.RuneCountInString(before)
|
|
128
|
+
return resolveNormalizedMatch(source, charIdx, normTextRuneLen)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return nil
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func resolveNormalizedMatch(source string, normCharIdx int, normTextRuneLen int) *AnchorResult {
|
|
135
|
+
origStart := mapNormalizedCharOffset(source, normCharIdx)
|
|
136
|
+
origEnd := mapNormalizedCharOffset(source, normCharIdx+normTextRuneLen)
|
|
137
|
+
|
|
138
|
+
// Trim trailing whitespace at end position
|
|
139
|
+
for origEnd > origStart && origEnd < len(source) {
|
|
140
|
+
if c := source[origEnd-1]; c == ' ' || c == '\t' || c == '\n' || c == '\r' {
|
|
141
|
+
origEnd--
|
|
142
|
+
} else {
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return &AnchorResult{
|
|
148
|
+
StartOffset: origStart,
|
|
149
|
+
EndOffset: origEnd,
|
|
150
|
+
Confidence: AnchorNormalized,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func mapSourceToNormCharOffset(original string, sourceOffset int) int {
|
|
155
|
+
normPos := 0
|
|
156
|
+
inSpace := false
|
|
157
|
+
for i, r := range original {
|
|
158
|
+
if i >= sourceOffset {
|
|
159
|
+
return normPos
|
|
160
|
+
}
|
|
161
|
+
if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
|
|
162
|
+
if !inSpace {
|
|
163
|
+
normPos++
|
|
164
|
+
inSpace = true
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
normPos++
|
|
168
|
+
inSpace = false
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return normPos
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func mapNormalizedCharOffset(original string, normCharOffset int) int {
|
|
175
|
+
normPos := 0
|
|
176
|
+
inSpace := false
|
|
177
|
+
for i, r := range original {
|
|
178
|
+
if normPos >= normCharOffset {
|
|
179
|
+
return i
|
|
180
|
+
}
|
|
181
|
+
if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
|
|
182
|
+
if !inSpace {
|
|
183
|
+
normPos++
|
|
184
|
+
inSpace = true
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
normPos++
|
|
188
|
+
inSpace = false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return len(original)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
func FindAnchorFuzzy(source, selectedText, lineHint string) *AnchorResult {
|
|
195
|
+
selectedRunes := []rune(selectedText)
|
|
196
|
+
if len(selectedRunes) > MaxFuzzyTextLength {
|
|
197
|
+
return nil
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
hintLine, _ := ParseLineHint(lineHint)
|
|
201
|
+
offset := lineOffset(source, hintLine)
|
|
202
|
+
|
|
203
|
+
windowStart := max(0, offset-FuzzySearchWindow)
|
|
204
|
+
windowEnd := min(len(source), offset+FuzzySearchWindow)
|
|
205
|
+
window := source[windowStart:windowEnd]
|
|
206
|
+
|
|
207
|
+
windowRunes := []rune(window)
|
|
208
|
+
textRuneLen := len(selectedRunes)
|
|
209
|
+
threshold := DefaultFuzzyThreshold
|
|
210
|
+
|
|
211
|
+
bestDist := threshold + 1
|
|
212
|
+
bestStart := -1
|
|
213
|
+
bestEnd := -1
|
|
214
|
+
|
|
215
|
+
for i := 0; i <= len(windowRunes)-textRuneLen+threshold; i++ {
|
|
216
|
+
for candLen := max(1, textRuneLen-threshold); candLen <= min(len(windowRunes)-i, textRuneLen+threshold); candLen++ {
|
|
217
|
+
candidate := windowRunes[i : i+candLen]
|
|
218
|
+
dist := levenshteinRunes(selectedRunes, candidate, threshold)
|
|
219
|
+
if dist <= threshold && dist < bestDist {
|
|
220
|
+
bestDist = dist
|
|
221
|
+
// Convert rune indices back to byte offsets
|
|
222
|
+
byteStart := len(string(windowRunes[:i]))
|
|
223
|
+
byteEnd := len(string(windowRunes[:i+candLen]))
|
|
224
|
+
bestStart = windowStart + byteStart
|
|
225
|
+
bestEnd = windowStart + byteEnd
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if bestStart < 0 {
|
|
231
|
+
return nil
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return &AnchorResult{
|
|
235
|
+
StartOffset: bestStart,
|
|
236
|
+
EndOffset: bestEnd,
|
|
237
|
+
Confidence: AnchorFuzzy,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
func levenshteinRunes(a, b []rune, maxDist int) int {
|
|
242
|
+
la, lb := len(a), len(b)
|
|
243
|
+
if abs(la-lb) > maxDist {
|
|
244
|
+
return maxDist + 1
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if la > lb {
|
|
248
|
+
a, b = b, a
|
|
249
|
+
la, lb = lb, la
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
prev := make([]int, la+1)
|
|
253
|
+
curr := make([]int, la+1)
|
|
254
|
+
|
|
255
|
+
for i := range prev {
|
|
256
|
+
prev[i] = i
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for j := 1; j <= lb; j++ {
|
|
260
|
+
curr[0] = j
|
|
261
|
+
minVal := curr[0]
|
|
262
|
+
for i := 1; i <= la; i++ {
|
|
263
|
+
cost := 1
|
|
264
|
+
if a[i-1] == b[j-1] {
|
|
265
|
+
cost = 0
|
|
266
|
+
}
|
|
267
|
+
curr[i] = min(
|
|
268
|
+
prev[i]+1,
|
|
269
|
+
min(curr[i-1]+1, prev[i-1]+cost),
|
|
270
|
+
)
|
|
271
|
+
if curr[i] < minVal {
|
|
272
|
+
minVal = curr[i]
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if minVal > maxDist {
|
|
276
|
+
return maxDist + 1
|
|
277
|
+
}
|
|
278
|
+
prev, curr = curr, prev
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return prev[la]
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
func abs(x int) int {
|
|
285
|
+
if x < 0 {
|
|
286
|
+
return -x
|
|
287
|
+
}
|
|
288
|
+
return x
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
func FindAnchorWithFallback(source, selectedText, lineHint string) *AnchorResult {
|
|
292
|
+
if r := FindAnchor(source, selectedText, lineHint); r != nil {
|
|
293
|
+
return r
|
|
294
|
+
}
|
|
295
|
+
if r := FindAnchorNormalized(source, selectedText, lineHint); r != nil {
|
|
296
|
+
return r
|
|
297
|
+
}
|
|
298
|
+
if r := FindAnchorFuzzy(source, selectedText, lineHint); r != nil {
|
|
299
|
+
return r
|
|
300
|
+
}
|
|
301
|
+
return nil
|
|
302
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package server
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
func TestParseLineHint(t *testing.T) {
|
|
6
|
+
tests := []struct {
|
|
7
|
+
input string
|
|
8
|
+
wantStart int
|
|
9
|
+
wantEnd int
|
|
10
|
+
}{
|
|
11
|
+
{"L42", 42, 42},
|
|
12
|
+
{"L42-L55", 42, 55},
|
|
13
|
+
{"L42-55", 42, 55}, // legacy format
|
|
14
|
+
{"invalid", 1, 1},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for _, tt := range tests {
|
|
18
|
+
s, e := ParseLineHint(tt.input)
|
|
19
|
+
if s != tt.wantStart || e != tt.wantEnd {
|
|
20
|
+
t.Errorf("ParseLineHint(%q) = (%d, %d), want (%d, %d)", tt.input, s, e, tt.wantStart, tt.wantEnd)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func TestFindAnchorExact(t *testing.T) {
|
|
26
|
+
source := "line1\nline2\nthe target text\nline4"
|
|
27
|
+
result := FindAnchor(source, "the target text", "L3")
|
|
28
|
+
if result == nil {
|
|
29
|
+
t.Fatal("expected match")
|
|
30
|
+
}
|
|
31
|
+
if result.Confidence != AnchorExact {
|
|
32
|
+
t.Errorf("confidence = %q, want exact", result.Confidence)
|
|
33
|
+
}
|
|
34
|
+
if source[result.StartOffset:result.EndOffset] != "the target text" {
|
|
35
|
+
t.Errorf("matched text = %q", source[result.StartOffset:result.EndOffset])
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func TestFindAnchorExactFullScan(t *testing.T) {
|
|
40
|
+
source := "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nfar away target"
|
|
41
|
+
result := FindAnchor(source, "far away target", "L1")
|
|
42
|
+
if result == nil {
|
|
43
|
+
t.Fatal("expected match via full scan")
|
|
44
|
+
}
|
|
45
|
+
if result.Confidence != AnchorExact {
|
|
46
|
+
t.Errorf("confidence = %q, want exact", result.Confidence)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func TestFindAnchorNormalized(t *testing.T) {
|
|
51
|
+
source := "hello world\nfoo"
|
|
52
|
+
result := FindAnchorNormalized(source, "hello world", "L1")
|
|
53
|
+
if result == nil {
|
|
54
|
+
t.Fatal("expected normalized match")
|
|
55
|
+
}
|
|
56
|
+
if result.Confidence != AnchorNormalized {
|
|
57
|
+
t.Errorf("confidence = %q, want normalized", result.Confidence)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func TestFindAnchorNormalizedSkipsSame(t *testing.T) {
|
|
62
|
+
source := "hello world"
|
|
63
|
+
result := FindAnchorNormalized(source, "hello world", "L1")
|
|
64
|
+
if result != nil {
|
|
65
|
+
t.Error("should return nil when both source and text already normalized")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func TestFindAnchorFuzzy(t *testing.T) {
|
|
70
|
+
source := "the quick brwon fox jumps"
|
|
71
|
+
result := FindAnchorFuzzy(source, "the quick brown fox jumps", "L1")
|
|
72
|
+
if result == nil {
|
|
73
|
+
t.Fatal("expected fuzzy match")
|
|
74
|
+
}
|
|
75
|
+
if result.Confidence != AnchorFuzzy {
|
|
76
|
+
t.Errorf("confidence = %q, want fuzzy", result.Confidence)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func TestFindAnchorFuzzyTooLong(t *testing.T) {
|
|
81
|
+
long := make([]byte, MaxFuzzyTextLength+1)
|
|
82
|
+
for i := range long {
|
|
83
|
+
long[i] = 'a'
|
|
84
|
+
}
|
|
85
|
+
result := FindAnchorFuzzy("aaaa", string(long), "L1")
|
|
86
|
+
if result != nil {
|
|
87
|
+
t.Error("should return nil for text exceeding MaxFuzzyTextLength")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func TestFindAnchorWithFallback(t *testing.T) {
|
|
92
|
+
source := "line1\nhello world\nline3"
|
|
93
|
+
|
|
94
|
+
// Exact match
|
|
95
|
+
r := FindAnchorWithFallback(source, "hello world", "L2")
|
|
96
|
+
if r == nil || r.Confidence != AnchorExact {
|
|
97
|
+
t.Error("expected exact match")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Normalized fallback
|
|
101
|
+
r = FindAnchorWithFallback(source, "hello world", "L2")
|
|
102
|
+
if r == nil || r.Confidence != AnchorNormalized {
|
|
103
|
+
t.Errorf("expected normalized match, got %v", r)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// No match
|
|
107
|
+
r = FindAnchorWithFallback(source, "nonexistent text that does not appear anywhere at all", "L1")
|
|
108
|
+
if r != nil {
|
|
109
|
+
t.Error("expected nil for non-matching text")
|
|
110
|
+
}
|
|
111
|
+
}
|