Skip to content

Commit e89ba4f

Browse files
upd: extend mentions for additional context (#1430)
1 parent f338556 commit e89ba4f

File tree

2 files changed

+189
-2
lines changed

2 files changed

+189
-2
lines changed

plugs/index/snippet_extractor.test.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,52 @@ Deno.test("SnippetExtractor", () => {
77
`;
88
assertEquals(
99
extractSnippetAroundIndex(testText, testText.indexOf("[[Diplomas]]")),
10-
"This is all about [[Diplomas]], and stuff like that.",
10+
"# Ongoing things This is all about [[Diplomas]], and stuff like that. More stuff.",
1111
);
1212

1313
const testText2 =
1414
`A much much much much much much much much much much much longer sentence [[Diplomas]], that just keeps and keeps and keeps and keeps and keeps going.
1515
`;
1616
assertEquals(
1717
extractSnippetAroundIndex(testText2, testText2.indexOf("[[Diplomas]]")),
18-
"...much much much much much much much longer sentence [[Diplomas]], that just keeps and keeps and keeps and...",
18+
"A much much much much much much much much much much much longer sentence [[Diplomas]], that just keeps and keeps and keeps and keeps and keeps going.",
1919
);
20+
21+
// Multi-line behavior
22+
const testText3 = `Line 1
23+
Line 2 with [[Reference]]
24+
Line 3
25+
Line 4
26+
Line 5`;
27+
assertEquals(
28+
extractSnippetAroundIndex(testText3, testText3.indexOf("[[Reference]]")),
29+
"Line 1 Line 2 with [[Reference]] Line 3",
30+
);
31+
32+
// Long line with reference - ensure reference stays visible and centered
33+
const longText = "Type the name of a non-existent page to create it.".repeat(
34+
10,
35+
);
36+
const testText4 =
37+
`Click on the page picker (book icon) icon at the top right, or hit \`Cmd-k\` (Mac) or \`Ctrl-k\` (Linux and Windows) to open the **page picker**.
38+
* ${longText} Don't worry about folders existing, [[SilverBullet]] will automatically create them if they don't.
39+
* Another line here`;
40+
const result = extractSnippetAroundIndex(
41+
testText4,
42+
testText4.indexOf("[[SilverBullet]]"),
43+
);
44+
45+
// Reference should always be visible in the result
46+
assertEquals(result.includes("[[SilverBullet]]"), true);
47+
// ... should also contain some context around it
48+
assertEquals(result.includes("create them"), true);
49+
// ... should not start with the very beginning of the long repeated text
50+
assertEquals(result.startsWith("..."), true);
51+
52+
// Edge case: index beyond text bounds (triggers fallback)
53+
const testText5 = "Hello\nWorld\nTest";
54+
const beyondBoundsIndex = testText5.length + 10;
55+
const fallbackResult = extractSnippetAroundIndex(testText5, beyondBoundsIndex);
56+
// Fallback should return something, not break
57+
assertEquals(typeof fallbackResult, "string");
2058
});

plugs/index/snippet_extractor.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,153 @@
11
export function extractSnippetAroundIndex(
2+
text: string,
3+
index: number,
4+
maxSnippetLength: number = 300,
5+
maxLines: number = 3,
6+
): string {
7+
if (index < 0 || index >= text.length) {
8+
return "";
9+
}
10+
const lines = text.split("\n");
11+
let currentPos = 0;
12+
let targetLineIndex = -1;
13+
14+
// Find which line contains the index
15+
for (let i = 0; i < lines.length; i++) {
16+
const lineLength = lines[i].length + (i < lines.length - 1 ? 1 : 0); // +1 for newline except last line
17+
if (index >= currentPos && index < currentPos + lineLength) {
18+
targetLineIndex = i;
19+
break;
20+
}
21+
currentPos += lineLength;
22+
}
23+
24+
if (targetLineIndex === -1) {
25+
// Fallback to original sentence-based logic if line detection fails
26+
return extractSnippetAroundIndexLegacy(
27+
text,
28+
index,
29+
Math.min(maxSnippetLength, 100),
30+
);
31+
}
32+
33+
// Calculate range of lines to include (up to maxLines, centered around target)
34+
const linesToInclude = Math.min(maxLines, lines.length);
35+
const linesAbove = Math.floor((linesToInclude - 1) / 2);
36+
const linesBelow = linesToInclude - 1 - linesAbove;
37+
38+
const startLine = Math.max(0, targetLineIndex - linesAbove);
39+
const endLine = Math.min(lines.length - 1, targetLineIndex + linesBelow);
40+
41+
const selectedLines = lines.slice(startLine, endLine + 1);
42+
43+
// Stop at markdown list boundaries to avoid including separate bullet points
44+
const processedLines: string[] = [];
45+
let foundTargetLine = false;
46+
47+
for (let i = 0; i < selectedLines.length; i++) {
48+
const line = selectedLines[i];
49+
const isCurrentTargetLine = (startLine + i) === targetLineIndex;
50+
51+
if (isCurrentTargetLine) {
52+
foundTargetLine = true;
53+
processedLines.push(line);
54+
} else if (!foundTargetLine) {
55+
// Before target line - include it
56+
processedLines.push(line);
57+
} else {
58+
// After target line - check if it's a new bullet point or list item
59+
const trimmedLine = line.trim();
60+
if (trimmedLine.match(/^[*+-]\s/) || trimmedLine.match(/^\d+\.\s/)) {
61+
// New bullt point or numbered list item
62+
break;
63+
}
64+
processedLines.push(line);
65+
}
66+
}
67+
68+
let snippet = processedLines.join(" ").replace(/\s+/g, " ").trim();
69+
70+
// If snippet is still too long, truncate while centering around the reference
71+
if (snippet.length > maxSnippetLength) {
72+
// Calculate the position of the index within the multi-line snippet
73+
let snippetStartPos = 0;
74+
for (let i = 0; i < startLine; i++) {
75+
snippetStartPos += lines[i].length + (i < lines.length - 1 ? 1 : 0); // +1 for newline except last line
76+
}
77+
const indexInSnippet = index - snippetStartPos;
78+
79+
// Center the truncation around the reference
80+
const halfLength = Math.floor(maxSnippetLength / 2);
81+
const ellipsisLength = 3;
82+
83+
// Calculate ideal start and end positions centered on the reference
84+
let idealStart = Math.max(0, indexInSnippet - halfLength);
85+
let idealEnd = Math.min(snippet.length, indexInSnippet + halfLength);
86+
87+
// Adjust if at beginning/end, use available space
88+
if (idealStart === 0) {
89+
idealEnd = Math.min(snippet.length, maxSnippetLength - ellipsisLength);
90+
} else if (idealEnd === snippet.length) {
91+
idealStart = Math.max(
92+
0,
93+
snippet.length - maxSnippetLength + ellipsisLength,
94+
);
95+
}
96+
97+
// Find word boundaries for cleaner truncation
98+
let truncateStart = idealStart;
99+
let truncateEnd = idealEnd;
100+
101+
// Adjust start to word boundary if not at the beginning
102+
if (truncateStart > 0) {
103+
const nearbySpace = snippet.lastIndexOf(" ", truncateStart + 20);
104+
if (nearbySpace !== -1 && nearbySpace >= truncateStart - 20) {
105+
truncateStart = nearbySpace + 1;
106+
}
107+
}
108+
109+
// Adjust end to word boundary if not at the end
110+
if (truncateEnd < snippet.length) {
111+
const nearbySpace = snippet.indexOf(" ", truncateEnd - 20);
112+
if (nearbySpace !== -1 && nearbySpace <= truncateEnd + 20) {
113+
truncateEnd = nearbySpace;
114+
}
115+
}
116+
117+
// Don't exceed maxSnippetLength with ellipsis
118+
const needsStartEllipsis = truncateStart > 0;
119+
const needsEndEllipsis = truncateEnd < snippet.length;
120+
const availableLength = maxSnippetLength -
121+
(needsStartEllipsis ? ellipsisLength : 0) -
122+
(needsEndEllipsis ? ellipsisLength : 0);
123+
124+
if (truncateEnd - truncateStart > availableLength) {
125+
// If still too long, prioritize keeping the reference centered
126+
const excess = (truncateEnd - truncateStart) - availableLength;
127+
const reduceStart = Math.floor(excess / 2);
128+
const reduceEnd = excess - reduceStart;
129+
130+
truncateStart += reduceStart;
131+
truncateEnd -= reduceEnd;
132+
}
133+
134+
let truncated = snippet.substring(truncateStart, truncateEnd).trim();
135+
136+
if (needsStartEllipsis) {
137+
truncated = "..." + truncated;
138+
}
139+
if (needsEndEllipsis) {
140+
truncated = truncated + "...";
141+
}
142+
143+
snippet = truncated;
144+
}
145+
146+
return snippet;
147+
}
148+
149+
// Legacy function for fallback compatibility
150+
function extractSnippetAroundIndexLegacy(
2151
text: string,
3152
index: number,
4153
maxSnippetLength: number = 100,

0 commit comments

Comments
 (0)