fix(read): enforced exact line selector bounds

- Removed implicit padding and syntactic boundary expansion from explicit ranges.
- Covered local, converted, multi-range, and artifact reads.

Fixes #5802
This commit is contained in:
roboomp
2026-07-17 07:45:39 +00:00
parent 0f9fceeea4
commit e5d5aec189
7 changed files with 93 additions and 186 deletions
@@ -266,27 +266,24 @@ export function findBlockContextLines(
return nativeBlockContext(fullLines, visible, source) ?? lexicalBracketContext(fullLines, visible);
}
/**
* Build display entries for `visibleSpans` plus any off-window block-boundary
* lines, in source order, with `{ kind: "ellipsis" }` markers inserted across
* non-contiguous gaps. `options.lineText` lets callers substitute display text
* (e.g. column-truncated lines) for a given line number.
*/
export function buildLineEntriesWithBlockContext(
fullLines: readonly string[],
visibleSpans: readonly LineSpan[],
source: BlockContextSource = {},
options: {
lineText?: (lineNumber: number, sourceText: string, context: boolean) => string;
} = {},
): LineEntry[] {
const spans = normalizeLineSpans(visibleSpans, fullLines.length);
const visible = visibleLineNumbers(spans);
const context = findBlockContextLines(fullLines, visible, source);
const allLines = new Set<number>(visible);
for (const lineNumber of context.keys()) allLines.add(lineNumber);
interface LineEntryOptions {
lineText?: (lineNumber: number, sourceText: string, context: boolean) => string;
}
function buildEntries(
fullLines: readonly string[],
visible: ReadonlySet<number>,
context: ReadonlyMap<number, string> | undefined,
options: LineEntryOptions,
): LineEntry[] {
const sorted = [...visible];
if (context) {
for (const lineNumber of context.keys()) {
if (!visible.has(lineNumber)) sorted.push(lineNumber);
}
}
sorted.sort((left, right) => left - right);
const sorted = [...allLines].sort((left, right) => left - right);
const entries: LineEntry[] = [];
let previousLine: number | undefined;
for (const lineNumber of sorted) {
@@ -294,7 +291,7 @@ export function buildLineEntriesWithBlockContext(
entries.push({ kind: "ellipsis" });
}
const sourceText = fullLines[lineNumber - 1] ?? "";
const isContext = context.has(lineNumber);
const isContext = context?.has(lineNumber) === true;
entries.push({
kind: "line",
lineNumber,
@@ -307,6 +304,33 @@ export function buildLineEntriesWithBlockContext(
return entries;
}
/** Build display entries for exactly the requested spans, separated by ellipses. */
export function buildLineEntries(
fullLines: readonly string[],
visibleSpans: readonly LineSpan[],
options: LineEntryOptions = {},
): LineEntry[] {
const spans = normalizeLineSpans(visibleSpans, fullLines.length);
return buildEntries(fullLines, visibleLineNumbers(spans), undefined, options);
}
/**
* Build display entries for `visibleSpans` plus any off-window block-boundary
* lines, in source order, with `{ kind: "ellipsis" }` markers inserted across
* non-contiguous gaps. `options.lineText` lets callers substitute display text
* (e.g. column-truncated lines) for a given line number.
*/
export function buildLineEntriesWithBlockContext(
fullLines: readonly string[],
visibleSpans: readonly LineSpan[],
source: BlockContextSource = {},
options: LineEntryOptions = {},
): LineEntry[] {
const spans = normalizeLineSpans(visibleSpans, fullLines.length);
const visible = visibleLineNumbers(spans);
return buildEntries(fullLines, visible, findBlockContextLines(fullLines, visible, source), options);
}
export function lineEntriesToPlainText(entries: readonly LineEntry[], ellipsis = "…"): string {
return entries.map(entry => (entry.kind === "ellipsis" ? ellipsis : entry.text)).join("\n");
}