mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 14:30:13 +00:00
fix(links): mention code-span check walks the spans once per call (TASK-3917 r4)
isInRanges scanned every code range for every match, so many inline code spans followed by many mentions was quadratic (360 KB took ~1.1 s). A cursor over the sorted, disjoint ranges advances with the increasing match position. ExtractWikiLinks has the same shape and is left for its own item. Claude-Session: https://claude.ai/code/session_01JRXHP9ZVfEN3YEuzmucxP3
This commit is contained in:
@@ -92,6 +92,22 @@ func (u *urlScan) inURL(content string, at int) bool {
|
||||
strings.HasPrefix(content[u.runStart:at], "www.")
|
||||
}
|
||||
|
||||
// rangeCursor answers "is pos inside one of these ranges" for positions that
|
||||
// arrive in increasing order, walking the sorted, disjoint ranges once per
|
||||
// call rather than once per match (TASK-3917 r4: isInRanges per match made a
|
||||
// body of many code spans and then many mentions quadratic).
|
||||
type rangeCursor struct {
|
||||
ranges [][2]int
|
||||
i int
|
||||
}
|
||||
|
||||
func (c *rangeCursor) contains(pos int) bool {
|
||||
for c.i < len(c.ranges) && c.ranges[c.i][1] <= pos {
|
||||
c.i++
|
||||
}
|
||||
return c.i < len(c.ranges) && pos >= c.ranges[c.i][0]
|
||||
}
|
||||
|
||||
// badMentionPrefix reports whether the byte before the "@" at content[at]
|
||||
// makes it a non-mention (an email local part, a path, a URL).
|
||||
func badMentionPrefix(content string, at int, u *urlScan) bool {
|
||||
@@ -161,10 +177,11 @@ func ExtractMentions(content string) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
u := urlScan{scheme: -1}
|
||||
fc, ic := rangeCursor{ranges: fenced}, rangeCursor{ranges: inline}
|
||||
for _, m := range mentionPattern.FindAllStringSubmatchIndex(content, -1) {
|
||||
at := m[0]
|
||||
u.advance(content, at)
|
||||
if isInRanges(at, fenced) || isInRanges(at, inline) {
|
||||
if fc.contains(at) || ic.contains(at) {
|
||||
continue
|
||||
}
|
||||
if at > 0 && badMentionPrefix(content, at, &u) {
|
||||
|
||||
@@ -57,6 +57,8 @@ func TestParseMentions(t *testing.T) {
|
||||
{"url then space then mention", "see https://x.social/a @alice", []string{"alice"}},
|
||||
{"www in an earlier run does not taint", "www.x.social z/@alice", []string{"alice"}},
|
||||
{"scheme earlier in the same run", "go:x://h/p/@alice", nil},
|
||||
{"mentions between and after code spans", "`a` @alice `@bob` @carol\n```\n@dave\n```\n@erin", []string{"alice", "carol", "erin"}},
|
||||
{"mention at a code span edge", "`x`@alice", []string{"alice"}},
|
||||
{"hyphen prefix rejected (documented limit)", "-@alice", nil},
|
||||
{"period at end", "thanks @alice.", []string{"alice"}},
|
||||
{"apostrophe", "@alice's turn", []string{"alice"}},
|
||||
@@ -83,3 +85,15 @@ func TestExtractMentionsLinearOnSlashRuns(t *testing.T) {
|
||||
t.Fatalf("ExtractMentions on %d bytes took %v; a per-match backward scan is back", len(body), d)
|
||||
}
|
||||
}
|
||||
|
||||
// The code-range check walks the spans once per call, not once per match: a
|
||||
// body of many inline code spans followed by many mentions was quadratic
|
||||
// (TASK-3917 r4; 360 KB took ~1.1 s, so a 2 MiB comment body ~40 s).
|
||||
func TestExtractMentionsLinearAfterCodeSpans(t *testing.T) {
|
||||
body := strings.Repeat("`a` ", 160000) + strings.Repeat("@aaa ", 160000)
|
||||
start := time.Now()
|
||||
ExtractMentions(body)
|
||||
if d := time.Since(start); d > 3*time.Second {
|
||||
t.Fatalf("ExtractMentions on %d bytes took %v; a per-match range scan is back", len(body), d)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user