Skip to content

Commit 6cc286a

Browse files
author
merge-queue-bot
committed
Merge PR #690: refactor(rename): export NormalizedLabel/RefDefBracketBytes, remove lsp duplicates
2 parents 11b905e + 30fdf68 commit 6cc286a

11 files changed

Lines changed: 68 additions & 84 deletions

PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,5 +227,5 @@ footer: |
227227
| 2606240211 || sonnet | [Add dedicated unit tests for locate.go helpers](plan/2606240211_arch-fix-locate-helper-tests.md) |
228228
| 2606240212 | 🔲 | sonnet | [Add dedicated unit tests for lsp/rename.go helpers](plan/2606240212_arch-fix-lsp-rename-helper-tests.md) |
229229
| 2606240213 | 🔲 | sonnet | [Add dedicated unit tests for export.go helpers and two small rename helpers](plan/2606240213_arch-fix-export-helper-tests.md) |
230-
| 2606240214 | 🔲 | sonnet | [Remove duplicated helpers between lsp/rename.go and rename/rename.go](plan/2606240214_arch-fix-rename-dedup.md) |
230+
| 2606240214 | | sonnet | [Remove duplicated helpers between lsp/rename.go and rename/rename.go](plan/2606240214_arch-fix-rename-dedup.md) |
231231
<?/catalog?>

cmd/mdsmith/rename.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func computeRenameChanges(
244244
}
245245
return changes, -1
246246
}
247-
edits, err := rename.LinkRef(src, rename.NormalizeLabel(oldName), newName)
247+
edits, err := rename.LinkRef(src, oldName, newName)
248248
if err != nil {
249249
fmt.Fprintf(os.Stderr, "mdsmith: %v\n", err)
250250
return nil, 2

internal/lsp/rename.go

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"github.com/jeduden/mdsmith/internal/index"
99
"github.com/jeduden/mdsmith/internal/mdtext"
1010
"github.com/jeduden/mdsmith/internal/rename"
11-
"github.com/jeduden/mdsmith/pkg/goldmark/util"
1211
)
1312

1413
// handlePrepareRename answers textDocument/prepareRename. The
@@ -225,7 +224,7 @@ func refDefPrepareRange(source []byte, line int, _ string) (prepareRenameResult,
225224
return prepareRenameResult{}, false
226225
}
227226
row := lines[line-1]
228-
m := refDefBracketBytes(row)
227+
m := rename.RefDefBracketBytes(row)
229228
if m == nil {
230229
return prepareRenameResult{}, false
231230
}
@@ -240,35 +239,6 @@ func refDefPrepareRange(source []byte, line int, _ string) (prepareRenameResult,
240239
}, true
241240
}
242241

243-
// refDefBracketBytes returns the [start, end) byte offsets of the
244-
// label inside a CommonMark reference-definition line, or nil when
245-
// row is not a reference definition.
246-
func refDefBracketBytes(row []byte) []int {
247-
i := 0
248-
for i < len(row) && i < 3 && row[i] == ' ' {
249-
i++
250-
}
251-
if i >= len(row) || row[i] != '[' {
252-
return nil
253-
}
254-
open := i + 1
255-
closeIdx := -1
256-
for j := open; j < len(row); j++ {
257-
if row[j] == ']' {
258-
closeIdx = j
259-
break
260-
}
261-
}
262-
if closeIdx < 0 || closeIdx == open {
263-
return nil
264-
}
265-
// After `]` we need `:` to qualify as a definition.
266-
if closeIdx+1 >= len(row) || row[closeIdx+1] != ':' {
267-
return nil
268-
}
269-
return []int{open, closeIdx}
270-
}
271-
272242
// refUsePrepareRange builds the rename range for a reference-style
273243
// link use (`[text][label]`, `[label][]`, or `[label]`). The cursor
274244
// position determines whether the user is editing the label or the
@@ -319,7 +289,7 @@ func refUseLabelBytes(row []byte, cursorByte int, label string) (int, int, bool)
319289
return start, end, true
320290
}
321291
// Shortcut `[label]`: this pair's content normalizes to label.
322-
if normalizedLabel(row[pr.open+1:pr.close]) == label {
292+
if rename.NormalizedLabel(row[pr.open+1:pr.close]) == label {
323293
return pr.open + 1, pr.close, true
324294
}
325295
}
@@ -340,10 +310,10 @@ func matchLeadingPair(row []byte, pairs []bracketPair, i int, label string) (int
340310
if next.open != pr.close+1 {
341311
return 0, 0, false
342312
}
343-
if normalizedLabel(row[next.open+1:next.close]) == label {
313+
if rename.NormalizedLabel(row[next.open+1:next.close]) == label {
344314
return next.open + 1, next.close, true
345315
}
346-
if next.close == next.open+1 && normalizedLabel(row[pr.open+1:pr.close]) == label {
316+
if next.close == next.open+1 && rename.NormalizedLabel(row[pr.open+1:pr.close]) == label {
347317
return pr.open + 1, pr.close, true
348318
}
349319
return 0, 0, false
@@ -363,19 +333,15 @@ func matchTrailingPair(row []byte, pairs []bracketPair, i int, label string) (in
363333
if prev.close+1 != pr.open {
364334
return 0, 0, false
365335
}
366-
if pr.close == pr.open+1 && normalizedLabel(row[prev.open+1:prev.close]) == label {
336+
if pr.close == pr.open+1 && rename.NormalizedLabel(row[prev.open+1:prev.close]) == label {
367337
return prev.open + 1, prev.close, true
368338
}
369-
if normalizedLabel(row[pr.open+1:pr.close]) == label {
339+
if rename.NormalizedLabel(row[pr.open+1:pr.close]) == label {
370340
return pr.open + 1, pr.close, true
371341
}
372342
return 0, 0, false
373343
}
374344

375-
func normalizedLabel(b []byte) string {
376-
return string(util.ToLinkReference(b))
377-
}
378-
379345
// bracketPairs returns every top-level `[` / `]` pair on row, in
380346
// left-to-right order. The walker is depth-aware: a `[` opens a new
381347
// nesting level and a `]` closes the innermost open `[`, so a

internal/lsp/rename_coverage_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66
"time"
77

8+
"github.com/jeduden/mdsmith/internal/rename"
89
"github.com/stretchr/testify/assert"
910
"github.com/stretchr/testify/require"
1011
)
@@ -199,7 +200,7 @@ func TestRefDefBracketBytesEdgeCases(t *testing.T) {
199200
{" [over]: x", nil}, // 4 leading spaces is over the limit
200201
}
201202
for _, tc := range cases {
202-
got := refDefBracketBytes([]byte(tc.row))
203+
got := rename.RefDefBracketBytes([]byte(tc.row))
203204
assert.Equal(t, tc.want, got, "row=%q", tc.row)
204205
}
205206
}

internal/rename/heading.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,6 @@ func FindHeadingLine(source []byte, headingText string) (int, bool) {
135135
return 0, false
136136
}
137137

138-
// NormalizeLabel collapses a link-reference label to its canonical
139-
// matching form (lowercased, whitespace-collapsed), the same
140-
// normalization LinkRef expects its oldLabel argument in. The CLI
141-
// normalizes `--link-ref oldlabel` through this before calling
142-
// LinkRef.
143-
func NormalizeLabel(s string) string {
144-
return normalizedLabel([]byte(s))
145-
}
146-
147138
// firstControlRune returns the first newline / carriage return in s,
148139
// or 0 when s is a single line. Heading text and link-ref labels are
149140
// single-line surfaces; a control rune would rewrite them into

internal/rename/heading_test.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,6 @@ func TestFindHeadingLine(t *testing.T) {
218218
assert.False(t, ok)
219219
}
220220

221-
func TestNormalizeLabel(t *testing.T) {
222-
assert.Equal(t, "docs api", NormalizeLabel("Docs API"))
223-
assert.Equal(t, "x", NormalizeLabel("X"))
224-
}
225-
226221
func TestFirstControlRune(t *testing.T) {
227222
assert.Equal(t, '\r', firstControlRune("a\rb"))
228223
assert.Equal(t, rune(0), firstControlRune("clean"))

internal/rename/helpers_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestRefDefBracketBytes(t *testing.T) {
3232
}
3333
for _, tc := range cases {
3434
t.Run(tc.name, func(t *testing.T) {
35-
assert.Equal(t, tc.want, refDefBracketBytes([]byte(tc.row)))
35+
assert.Equal(t, tc.want, RefDefBracketBytes([]byte(tc.row)))
3636
})
3737
}
3838
}

internal/rename/rename.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,17 @@ func (e LabelConflictError) Error() string {
6262
}
6363

6464
// LinkRef computes the in-file edits that rename a link-reference
65-
// label from oldLabel (already normalized via util.ToLinkReference)
66-
// to newName. The rewrite is file-local: the `[label]: url`
67-
// definition plus every `[text][label]` and shortcut `[label]` use.
65+
// label from oldLabel to newName. The rewrite is file-local: the
66+
// `[label]: url` definition plus every `[text][label]` and shortcut
67+
// `[label]` use. oldLabel is normalized internally (CommonMark
68+
// link-label normalization: lowercase, whitespace collapsed) so
69+
// callers may pass raw label text or a pre-normalized form.
6870
//
6971
// Returns ErrEmptyLabel, an InvalidLabelRuneError, or a
7072
// LabelConflictError without producing any edit when the rename is
7173
// unsafe, so callers can surface the failure before applying.
7274
func LinkRef(source []byte, oldLabel, newName string) ([]Edit, error) {
75+
oldLabel = NormalizedLabel([]byte(oldLabel))
7376
if strings.TrimSpace(newName) == "" {
7477
return nil, ErrEmptyLabel
7578
}
@@ -80,7 +83,7 @@ func LinkRef(source []byte, oldLabel, newName string) ([]Edit, error) {
8083
// → "Docs API") is allowed — it refreshes casing/spacing across
8184
// the def and every use. labelConflict matches on the normalized
8285
// form so such a rename never collides with itself.
83-
newLabel := normalizedLabel([]byte(newName))
86+
newLabel := NormalizedLabel([]byte(newName))
8487
if conflict := labelConflict(source, oldLabel, newLabel); conflict != "" {
8588
return nil, LabelConflictError{Conflict: conflict}
8689
}
@@ -124,7 +127,10 @@ func invalidLinkRefRune(s string) rune {
124127
return 0
125128
}
126129

127-
func normalizedLabel(b []byte) string {
130+
// NormalizedLabel returns the CommonMark-normalized form of a link
131+
// label — lowercase with internal whitespace collapsed — by delegating
132+
// to goldmark's util.ToLinkReference.
133+
func NormalizedLabel(b []byte) string {
128134
return string(util.ToLinkReference(b))
129135
}
130136

@@ -173,7 +179,7 @@ func validRefDefMatches(body []byte) []validRefDefMatch {
173179
continue
174180
}
175181
raw := body[m[2]:m[3]]
176-
norm := normalizedLabel(raw)
182+
norm := NormalizedLabel(raw)
177183
out = append(out, validRefDefMatch{
178184
bodyLine: bodyLine,
179185
rawLabel: string(raw),
@@ -246,7 +252,7 @@ func refDefEditsInBody(
246252
continue
247253
}
248254
row := lines[fileLine-1]
249-
bracket := refDefBracketBytes(row)
255+
bracket := RefDefBracketBytes(row)
250256
startCh := mdtext.UTF16FromByteOffset(row, bracket[0])
251257
endCh := mdtext.UTF16FromByteOffset(row, bracket[1])
252258
out = append(out, Edit{
@@ -276,7 +282,7 @@ func refUseEditsInBody(
276282
if !ok || l.Reference == nil {
277283
return ast.WalkContinue, nil
278284
}
279-
if normalizedLabel(l.Reference.Value) != oldLabel {
285+
if NormalizedLabel(l.Reference.Value) != oldLabel {
280286
return ast.WalkContinue, nil
281287
}
282288
edit, ok := refUseEdit(l, body, lines, fmOffset, newName, idx)
@@ -376,10 +382,10 @@ func linkTextBounds(l *ast.Link, body []byte) (int, int) {
376382
return start, end
377383
}
378384

379-
// refDefBracketBytes returns the [start, end) byte offsets of the
385+
// RefDefBracketBytes returns the [start, end) byte offsets of the
380386
// label inside a CommonMark reference-definition line, or nil when
381387
// row is not a reference definition.
382-
func refDefBracketBytes(row []byte) []int {
388+
func RefDefBracketBytes(row []byte) []int {
383389
i := 0
384390
for i < len(row) && i < 3 && row[i] == ' ' {
385391
i++

internal/rename/rename_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ func TestLinkRef_SameNormalizedFormRefreshesCasing(t *testing.T) {
6363
require.Len(t, edits, 2)
6464
}
6565

66+
func TestLinkRef_NormalizesOldLabel(t *testing.T) {
67+
// LinkRef normalizes oldLabel internally, so callers may pass
68+
// the raw label text without pre-normalizing it.
69+
src := []byte("See [spec].\n\n[spec]: u\n")
70+
edits, err := LinkRef(src, "Spec", "rfc")
71+
require.NoError(t, err)
72+
require.Len(t, edits, 2)
73+
}
74+
6675
func TestLinkRef_CodeFenceDefNotRewritten(t *testing.T) {
6776
src := []byte("Use [spec].\n\n```\n[spec]: fake\n```\n\n[spec]: real\n")
6877
edits, err := LinkRef(src, "spec", "rfc")
@@ -124,3 +133,21 @@ func TestLinkRef_NoMatchingLabel(t *testing.T) {
124133
require.NoError(t, err)
125134
assert.Empty(t, edits)
126135
}
136+
137+
func TestNormalizedLabel(t *testing.T) {
138+
cases := []struct {
139+
input string
140+
want string
141+
}{
142+
{"spec", "spec"},
143+
{"Spec", "spec"},
144+
{"API Docs", "api docs"},
145+
{"API Docs", "api docs"},
146+
{" leading ", "leading"},
147+
}
148+
for _, tc := range cases {
149+
t.Run(tc.input, func(t *testing.T) {
150+
assert.Equal(t, tc.want, NormalizedLabel([]byte(tc.input)))
151+
})
152+
}
153+
}

plan/2606240212_arch-fix-lsp-rename-helper-tests.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: Add dedicated unit tests for lsp/rename.go helpers
44
status: "🔲"
55
model: sonnet
66
summary: >-
7-
internal/lsp/rename.go has 15 unexported
7+
internal/lsp/rename.go has 13 unexported
88
helpers without dedicated unit tests. Add a
99
named test for each so the audit policy is
1010
satisfied.
@@ -13,7 +13,7 @@ summary: >-
1313

1414
## Goal
1515

16-
Add a named unit test for each of the 15 unexported
16+
Add a named unit test for each of the 13 unexported
1717
helpers in `internal/lsp/rename.go`. The 2026-06-24
1818
audit requires it.
1919

@@ -23,10 +23,11 @@ Go arch doc §"Tests" requires every production function
2323
to have a dedicated test by name. The 2026-06-24 audit
2424
(range: 1599c9f..09f22d3) flagged this file.
2525

26-
`atxHeadingTextByteRange` already has a test. The
27-
three trivial pass-through methods on the workspace
28-
adapter carry exemption comments. The 15 helpers
29-
below need dedicated tests:
26+
`atxHeadingTextByteRange` already has a test.
27+
Three pass-through adapter methods carry exemption
28+
comments. Two helpers were removed from this file.
29+
Both now live in `internal/rename` with tests.
30+
The 13 helpers below need tests:
3031

3132
- `isValidRefDefLine`
3233
- `headingPrepareRange`
@@ -36,13 +37,11 @@ below need dedicated tests:
3637
- `trimRightSpace`
3738
- `trimmedRange`
3839
- `refDefPrepareRange`
39-
- `refDefBracketBytes`
4040
- `refUsePrepareRange`
4141
- `refUseLabelBytes` (one partial test exists;
4242
add broader coverage)
4343
- `matchLeadingPair`
4444
- `matchTrailingPair`
45-
- `normalizedLabel`
4645
- `bracketPairs`
4746

4847
## Tasks
@@ -64,11 +63,10 @@ below need dedicated tests:
6463
`TestskipLeadingSpaces`,
6564
`TesttrimRightSpace`, `TesttrimmedRange`,
6665
`TestrefDefPrepareRange`,
67-
`TestrefDefBracketBytes`,
6866
`TestrefUsePrepareRange`,
6967
`TestrefUseLabelBytes`,
7068
`TestmatchLeadingPair`,
7169
`TestmatchTrailingPair`,
72-
`TestnormalizedLabel`, `TestbracketPairs`.
70+
`TestbracketPairs`.
7371
- [ ] `go test ./internal/lsp/...` is green.
7472
- [ ] `mdsmith check .` is green.

0 commit comments

Comments
 (0)