fix(ui): end the source pager at the file's real last line (K13)

Completeness check of K13: at the end of django/contrib/postgres/aggregates/
general.py the pager read "Lines 42 to 66 · end of file" and drew an empty
line 66, while the file has 65 lines and the title of the disabled "Next
lines" already said "Line 65 is the last line of this file." The Module
range counts the empty segment after the final line break. On the last page
that segment is no longer drawn and the range ends at the real last line,
so range text, drawn lines and title agree. A file that ends without a final
line break keeps its last line.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
Bernhard Jackiewicz
2026-10-04 00:38:29 +02:00
parent 80dd23d5a3
commit 2e6db07017
2 changed files with 20 additions and 8 deletions
@@ -149,15 +149,18 @@ it('does not invent line evidence for a malformed source body', async () => {
it('preserves the actual Module EOF line reported by the source API and highlights its call site', async () => {
// Recorded local project_lock.c response, start_line=53/end_line=190.
// Its trailing empty segment is the indexed EOF line, not a page separator.
// Its trailing empty segment is the indexed EOF line, not a page separator:
// the range matches. The file itself ends at 189 (review of K13), so the
// drawer neither draws an empty line 190 nor names it in the range.
const ui = await mount(vi.fn(async () => page(53, { ...projectLockEof, source_mode: 'full',
source_truncated: undefined, source_clipped: undefined, next_start_line: undefined })));
await ui.render({ filePath: path, line: 61 });
expect(ui.container.querySelector('[role=alert]')).toBeNull();
expect(ui.container.querySelectorAll('code > span')).toHaveLength(138);
expect(ui.container.querySelector('code > span:last-child .source-evidence-line-number')?.textContent).toBe('190');
expect(ui.container.querySelectorAll('code > span')).toHaveLength(137);
expect(ui.container.querySelector('code > span:last-child .source-evidence-line-number')?.textContent).toBe('189');
expect(ui.container.querySelector('[data-selected=true]')?.textContent).toContain('cbm_private_lock_directory_close(directory)');
expect(ui.container.textContent).toContain('Lines 53 to 190');
expect(ui.container.textContent).toContain('Lines 53 to 189');
expect(ui.container.textContent).toContain('end of file');
expect([...ui.container.querySelectorAll('button')].find(button => button.textContent === 'Next lines')?.disabled).toBe(true);
});
@@ -188,7 +191,11 @@ it('says "end of file" when the last page reaches the end of the Module, so a di
await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); });
const pager = ui.container.querySelector('nav[aria-label="Source pages"]')!;
expect(ui.container.querySelector('[role=alert]')).toBeNull();
expect(pager.textContent).toContain('Lines 42 to 66 · end of file');
// Review of K13: the range, the lines drawn and the title all end at the file's real last line, 65.
expect(pager.textContent).toContain('Lines 42 to 65 · end of file');
const numbers = [...ui.container.querySelectorAll('.source-evidence-line-number')].map(entry => entry.textContent);
expect(numbers).toHaveLength(24);
expect(numbers.at(-1)).toBe('65');
const next = [...pager.querySelectorAll('button')].find(button => button.textContent === 'Next lines')!;
expect(next.disabled).toBe(true);
// The title names the file's real last line, not the empty end segment the range counts.
@@ -202,4 +209,6 @@ it('names the last line itself when a file ends without a final line break (hand
await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); });
const next = [...ui.container.querySelectorAll('nav[aria-label="Source pages"] button')].find(button => button.textContent === 'Next lines')!;
expect(next.getAttribute('title')).toBe('Line 66 is the last line of this file.');
expect(ui.container.querySelector('nav[aria-label="Source pages"]')!.textContent).toContain('Lines 42 to 66 · end of file');
expect([...ui.container.querySelectorAll('.source-evidence-line-number')].at(-1)?.textContent).toBe('66');
});
@@ -105,7 +105,10 @@ export default function SourceEvidenceDrawer({ project, target, graph, client, o
const canNext = Boolean(more && validLine(next) && validLine(lastLine) && next === lastLine + 1 && rangeMatches);
const atEnd = Boolean(source && !more && rangeMatches && validLine(lastLine));
// The Module range counts the empty segment after the final line break; the file's last line is the one before it.
const lastFileLine = atEnd && lines.length > 1 && lines.at(-1) === '' ? lastLine! - 1 : lastLine;
const endSegment = atEnd && lines.length > 1 && lines.at(-1) === '';
const lastFileLine = endSegment ? lastLine! - 1 : lastLine;
// Review of K13: range text, drawn lines and title all stop at that line, not at the empty segment after it.
const shownLines = endSegment ? lines.slice(0, -1) : lines;
const locationVisible = requestedLine !== undefined && validLine(firstLine) && validLine(lastLine)
&& requestedLine >= firstLine && requestedLine <= lastLine && rangeMatches;
useEffect(() => {
@@ -142,7 +145,7 @@ export default function SourceEvidenceDrawer({ project, target, graph, client, o
{!rangeMatches && <p role="alert">Source text does not match the reported line range. Line numbers and highlighting are unavailable.</p>}
{requestedLine && !locationVisible && rangeMatches && <p role="status" className="source-evidence-notice">Requested line {requestedLine} is outside this returned window.
{start === initialStart ? ' The index or requested source window may be stale.' : <button onClick={() => moveTo(initialStart)}>Return to requested location</button>}</p>}
<pre ref={sourceRef} tabIndex={0} aria-label="Source lines" className="source-evidence-lines"><code>{lines.map((line, index) => {
<pre ref={sourceRef} tabIndex={0} aria-label="Source lines" className="source-evidence-lines"><code>{shownLines.map((line, index) => {
const number = (firstLine ?? 1) + index;
const selected = rangeMatches && number === requestedLine;
return <span key={number} data-selected={selected} ref={selected ? selectedRef : undefined}>
@@ -151,7 +154,7 @@ export default function SourceEvidenceDrawer({ project, target, graph, client, o
{lines.length === 0 && <p role="status">No source text was returned.</p>}
{more && !canNext && <p role="status">The source is truncated, but no valid next page was supplied. Refresh or open Explore to inspect the available source.</p>}
<nav aria-label="Source pages"><button disabled={!rangeMatches || !firstLine || firstLine <= 1} onClick={() => moveTo(Math.max(1, firstLine! - PAGE_LINES))}>Previous lines</button>
<span>{rangeMatches ? `Lines ${firstLine} to ${lastLine}` : 'Unverified line range'}{source.original_end_line ? ` · reported end ${source.original_end_line}` : ''}{atEnd ? END_OF_FILE : ''}</span>
<span>{rangeMatches ? `Lines ${firstLine} to ${lastFileLine}` : 'Unverified line range'}{source.original_end_line ? ` · reported end ${source.original_end_line}` : ''}{atEnd ? END_OF_FILE : ''}</span>
<button disabled={!canNext} title={atEnd ? endOfFileTitle(lastFileLine!) : undefined} onClick={() => moveTo(next!)}>Next lines</button></nav>
</>}
</aside>;