Skip to content

Commit d061a01

Browse files
committed
fix: correct #line offset and unquoted filename detection in Show in Source
1 parent fdb2b48 commit d061a01

3 files changed

Lines changed: 181 additions & 29 deletions

File tree

client/src/lineDirectiveNavigation.ts

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,11 @@ import * as fs from 'fs';
22
import * as path from 'path';
33
import { window, workspace, Uri, Position, Range, ViewColumn } from 'vscode';
44

5-
// ── Detection ─────────────────────────────────────────────────────────────────
6-
7-
/** Returns true if the file header looks like a Bison or Flex generated C file. */
8-
export function isGeneratedFile(text: string): boolean {
9-
const header = text.slice(0, 600);
10-
return (
11-
header.includes('/* A Bison parser, made by GNU Bison') ||
12-
header.includes('/* Generated by GNU Bison') ||
13-
header.includes('/* Generated by flex') ||
14-
header.includes('/* A lexical scanner generated by flex')
15-
);
16-
}
5+
export { isGeneratedFile } from './lineDirectiveUtils';
6+
import { isGeneratedFile, findNearestLineDirective } from './lineDirectiveUtils';
177

188
// ── Direction 1: generated → source ──────────────────────────────────────────
199

20-
interface LineDirective {
21-
sourceLine: number; // 1-based line number in the source file
22-
sourceFile: string;
23-
}
24-
25-
/** Scan backwards from cursorLine to find the nearest #line N "file" directive. */
26-
function findNearestLineDirective(lines: string[], cursorLine: number): LineDirective | null {
27-
for (let i = cursorLine; i >= 0; i--) {
28-
const m = lines[i].match(/^#line\s+(\d+)\s+"([^"]+)"/);
29-
if (m) {
30-
return { sourceLine: parseInt(m[1], 10), sourceFile: m[2] };
31-
}
32-
}
33-
return null;
34-
}
35-
3610
/** Navigate from the generated C file at cursorLine to the original grammar source. */
3711
export async function showInSource(): Promise<void> {
3812
const editor = window.activeTextEditor;
@@ -70,7 +44,8 @@ export async function showInSource(): Promise<void> {
7044
sourcePath = found;
7145
}
7246

73-
const targetLine = Math.max(0, directive.sourceLine - 1); // convert to 0-based
47+
const offset = cursorLine - directive.directiveLine; // lines between directive and cursor
48+
const targetLine = Math.max(0, directive.sourceLine - 1 + offset);
7449
const doc = await workspace.openTextDocument(Uri.file(sourcePath));
7550
const pos = new Position(targetLine, 0);
7651
await window.showTextDocument(doc, { selection: new Range(pos, pos), viewColumn: ViewColumn.Active });
@@ -197,6 +172,7 @@ function findLineInGenerated(generatedLines: string[], sourceFilePath: string, s
197172
let bestSrcLine = -1;
198173

199174
for (let i = 0; i < generatedLines.length; i++) {
175+
// NOTE: only handles quoted filenames; unquoted form not yet supported (see showInSource / lineDirectiveUtils.ts)
200176
const m = generatedLines[i].match(/^#line\s+(\d+)\s+"([^"]+)"/);
201177
if (!m) continue;
202178
const dirSrcLine = parseInt(m[1], 10);

client/src/lineDirectiveUtils.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Pure helper functions for line directive navigation.
3+
* No VS Code dependencies — designed for testability and reusability.
4+
*/
5+
6+
// ── Detection ─────────────────────────────────────────────────────────────────
7+
8+
/** Returns true if the file header looks like a Bison or Flex generated C file. */
9+
export function isGeneratedFile(text: string): boolean {
10+
const header = text.slice(0, 600);
11+
return (
12+
header.includes('/* A Bison parser, made by GNU Bison') ||
13+
header.includes('/* Generated by GNU Bison') ||
14+
header.includes('/* Generated by flex') ||
15+
header.includes('/* A lexical scanner generated by flex')
16+
);
17+
}
18+
19+
// ── Line directive helpers ────────────────────────────────────────────────────
20+
21+
export interface LineDirective {
22+
sourceLine: number; // 1-based line number in the source file
23+
sourceFile: string;
24+
directiveLine: number; // 0-based line index in the generated file where directive was found
25+
}
26+
27+
/**
28+
* Scan backwards from cursorLine to find the nearest #line N "file" or #line N file directive.
29+
* Returns the directive data including the line index where it was found.
30+
*/
31+
export function findNearestLineDirective(lines: string[], cursorLine: number): LineDirective | null {
32+
for (let i = cursorLine; i >= 0; i--) {
33+
const m = lines[i].match(/^#line\s+(\d+)\s+"?([^"\s]+)"?/);
34+
if (m) {
35+
return {
36+
sourceLine: parseInt(m[1], 10),
37+
sourceFile: m[2],
38+
directiveLine: i,
39+
};
40+
}
41+
}
42+
return null;
43+
}

tests/test-line-directive.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* Tests for lineDirectiveUtils pure helpers.
3+
* Run: TS_NODE_PROJECT=tsconfig.base.json npx ts-node tests/test-line-directive.ts
4+
*/
5+
import { isGeneratedFile, findNearestLineDirective } from '../client/src/lineDirectiveUtils';
6+
7+
let passed = 0, failed = 0;
8+
function assert(cond: boolean, msg: string, extra?: unknown) {
9+
if (cond) { console.log(` [PASS] ${msg}`); passed++; }
10+
else { console.error(` [FAIL] ${msg}${extra !== undefined ? ` → ${JSON.stringify(extra)}` : ''}`); failed++; }
11+
}
12+
13+
// ── isGeneratedFile ────────────────────────────────────────────────────────────
14+
console.log('\nisGeneratedFile');
15+
16+
assert(
17+
isGeneratedFile('/* A Bison parser, made by GNU Bison 3.8.2. */\n...rest...'),
18+
'detects Bison generated file (made by)'
19+
);
20+
assert(
21+
isGeneratedFile('/* Generated by GNU Bison 3.8 */\n...rest...'),
22+
'detects Bison generated file (generated by)'
23+
);
24+
assert(
25+
isGeneratedFile('/* Generated by flex 2.6.4 */\n...rest...'),
26+
'detects Flex generated file (generated by flex)'
27+
);
28+
assert(
29+
isGeneratedFile('/* A lexical scanner generated by flex */\n'),
30+
'detects Flex generated file (lexical scanner)'
31+
);
32+
assert(
33+
!isGeneratedFile('// My hand-written parser\n'),
34+
'rejects non-generated file'
35+
);
36+
assert(
37+
!isGeneratedFile(''),
38+
'rejects empty file'
39+
);
40+
41+
// ── findNearestLineDirective ───────────────────────────────────────────────────
42+
console.log('\nfindNearestLineDirective — quoted filenames');
43+
44+
{
45+
// Cursor ON the directive line → offset = 0
46+
const lines = ['#line 28 "parser.y"', 'some_func();'];
47+
const d = findNearestLineDirective(lines, 0);
48+
assert(d !== null, 'quoted: finds directive on cursor line');
49+
if (d) {
50+
assert(d.sourceLine === 28, 'quoted: sourceLine = 28', d.sourceLine);
51+
assert(d.sourceFile === 'parser.y', 'quoted: sourceFile = parser.y', d.sourceFile);
52+
assert(d.directiveLine === 0, 'quoted: directiveLine = 0', d.directiveLine);
53+
}
54+
}
55+
56+
{
57+
// Cursor 4 lines below the directive → offset = 4
58+
// The caller should compute: targetLine = sourceLine - 1 + (cursorLine - directiveLine) = 27 + 4 = 31
59+
const lines = [
60+
'#line 28 "parser.y"',
61+
'#include "config.h"',
62+
'#include <stdlib.h>',
63+
'#include <string.h>',
64+
'void some_func(int mode) {',
65+
];
66+
const d = findNearestLineDirective(lines, 4);
67+
assert(d !== null, 'quoted: finds directive 4 lines above cursor');
68+
if (d) {
69+
assert(d.sourceLine === 28, 'quoted: sourceLine = 28', d.sourceLine);
70+
assert(d.sourceFile === 'parser.y', 'quoted: sourceFile (cursor 4 lines below)', d.sourceFile);
71+
assert(d.directiveLine === 0, 'quoted: directiveLine = 0', d.directiveLine);
72+
}
73+
}
74+
75+
console.log('\nfindNearestLineDirective — unquoted filenames');
76+
77+
{
78+
// GitMensch case: no quotes
79+
const lines = ['#line 28 parser.y', 'some_func();'];
80+
const d = findNearestLineDirective(lines, 1);
81+
assert(d !== null, 'unquoted: finds directive');
82+
if (d) {
83+
assert(d.sourceLine === 28, 'unquoted: sourceLine = 28', d.sourceLine);
84+
assert(d.sourceFile === 'parser.y', 'unquoted: sourceFile = parser.y', d.sourceFile);
85+
assert(d.directiveLine === 0, 'unquoted: directiveLine = 0', d.directiveLine);
86+
}
87+
}
88+
89+
{
90+
// Unquoted relative path
91+
const lines = ['#line 5 src/parser.y', 'x = 1;'];
92+
const d = findNearestLineDirective(lines, 1);
93+
assert(d !== null, 'unquoted path: finds directive');
94+
if (d) {
95+
assert(d.sourceFile === 'src/parser.y', 'unquoted path: sourceFile', d.sourceFile);
96+
}
97+
}
98+
99+
console.log('\nfindNearestLineDirective — edge cases');
100+
101+
{
102+
// No directive anywhere → null
103+
const lines = ['int x = 0;', 'return x;'];
104+
const d = findNearestLineDirective(lines, 1);
105+
assert(d === null, 'returns null when no directive exists');
106+
}
107+
108+
{
109+
// Multiple directives — picks the nearest one above cursor
110+
const lines = [
111+
'#line 10 "a.y"',
112+
'foo();',
113+
'#line 20 "b.y"',
114+
'bar();',
115+
];
116+
const d = findNearestLineDirective(lines, 3);
117+
assert(d !== null, 'picks nearest directive above cursor');
118+
if (d) {
119+
assert(d.sourceLine === 20, 'picks nearest directive above cursor (line)', d.sourceLine);
120+
assert(d.sourceFile === 'b.y', 'picks nearest directive above cursor (file)', d.sourceFile);
121+
assert(d.directiveLine === 2, 'directiveLine of nearest directive', d.directiveLine);
122+
}
123+
}
124+
125+
{
126+
// Cursor on line 0 with no directive → null
127+
const lines = ['int x = 0;'];
128+
const d = findNearestLineDirective(lines, 0);
129+
assert(d === null, 'no directive on line 0 → null');
130+
}
131+
132+
console.log(`\nResults: ${passed} passed, ${failed} failed`);
133+
if (failed > 0) process.exit(1);

0 commit comments

Comments
 (0)