From 2e6db070176ebef98e418ac29142d43fd132091d Mon Sep 17 00:00:00 2001 From: Bernhard Jackiewicz Date: Sun, 4 Oct 2026 00:38:29 +0200 Subject: [PATCH] fix(ui): end the source pager at the file's real last line (K13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../SourceEvidenceDrawer.test.tsx | 19 ++++++++++++++----- .../src/architecture/SourceEvidenceDrawer.tsx | 9 ++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/graph-ui/src/architecture/SourceEvidenceDrawer.test.tsx b/graph-ui/src/architecture/SourceEvidenceDrawer.test.tsx index fa859c26e..d039fa5db 100644 --- a/graph-ui/src/architecture/SourceEvidenceDrawer.test.tsx +++ b/graph-ui/src/architecture/SourceEvidenceDrawer.test.tsx @@ -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'); }); diff --git a/graph-ui/src/architecture/SourceEvidenceDrawer.tsx b/graph-ui/src/architecture/SourceEvidenceDrawer.tsx index e56b2da1d..8edf6502c 100644 --- a/graph-ui/src/architecture/SourceEvidenceDrawer.tsx +++ b/graph-ui/src/architecture/SourceEvidenceDrawer.tsx @@ -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 &&

Source text does not match the reported line range. Line numbers and highlighting are unavailable.

} {requestedLine && !locationVisible && rangeMatches &&

Requested line {requestedLine} is outside this returned window. {start === initialStart ? ' The index or requested source window may be stale.' : }

} -
{lines.map((line, index) => {
+            
{shownLines.map((line, index) => {
                 const number = (firstLine ?? 1) + index;
                 const selected = rangeMatches && number === requestedLine;
                 return 
@@ -151,7 +154,7 @@ export default function SourceEvidenceDrawer({ project, target, graph, client, o
             {lines.length === 0 && 

No source text was returned.

} {more && !canNext &&

The source is truncated, but no valid next page was supplied. Refresh or open Explore to inspect the available source.

} } ;