Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ server/out/
.env
tests/_*
docs/
.vscode/
.vscode/
.claude/
graphify-out/
tests/manuals/
.vscode/launch.json
2 changes: 2 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ package-lock.json
.env
.env.*
docs/
graphify-out/
tests/manuals/
34 changes: 5 additions & 29 deletions client/src/lineDirectiveNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,11 @@ import * as fs from 'fs';
import * as path from 'path';
import { window, workspace, Uri, Position, Range, ViewColumn } from 'vscode';

// ── Detection ─────────────────────────────────────────────────────────────────

/** Returns true if the file header looks like a Bison or Flex generated C file. */
export function isGeneratedFile(text: string): boolean {
const header = text.slice(0, 600);
return (
header.includes('/* A Bison parser, made by GNU Bison') ||
header.includes('/* Generated by GNU Bison') ||
header.includes('/* Generated by flex') ||
header.includes('/* A lexical scanner generated by flex')
);
}
export { isGeneratedFile } from './lineDirectiveUtils';
import { isGeneratedFile, findNearestLineDirective } from './lineDirectiveUtils';

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

interface LineDirective {
sourceLine: number; // 1-based line number in the source file
sourceFile: string;
}

/** Scan backwards from cursorLine to find the nearest #line N "file" directive. */
function findNearestLineDirective(lines: string[], cursorLine: number): LineDirective | null {
for (let i = cursorLine; i >= 0; i--) {
const m = lines[i].match(/^#line\s+(\d+)\s+"([^"]+)"/);
if (m) {
return { sourceLine: parseInt(m[1], 10), sourceFile: m[2] };
}
}
return null;
}

/** Navigate from the generated C file at cursorLine to the original grammar source. */
export async function showInSource(): Promise<void> {
const editor = window.activeTextEditor;
Expand Down Expand Up @@ -70,7 +44,8 @@ export async function showInSource(): Promise<void> {
sourcePath = found;
}

const targetLine = Math.max(0, directive.sourceLine - 1); // convert to 0-based
const offset = cursorLine - directive.directiveLine; // lines between directive and cursor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that correct? I think yes, but the later use of the offset should possibly have another + 1:

Image

goes to

Image

note that #line 6287 "../../cobc/parser.y" says that the next line is line 6287;

cursor line (6290) - directive line (6287) = 3 (+1 --> 4 if considered from the #line itself), but the position is only 3 lines below the #line .

Apart from that the feature works fine.

const targetLine = Math.max(0, directive.sourceLine - 1 + offset);
const doc = await workspace.openTextDocument(Uri.file(sourcePath));
const pos = new Position(targetLine, 0);
await window.showTextDocument(doc, { selection: new Range(pos, pos), viewColumn: ViewColumn.Active });
Expand Down Expand Up @@ -197,6 +172,7 @@ function findLineInGenerated(generatedLines: string[], sourceFilePath: string, s
let bestSrcLine = -1;

for (let i = 0; i < generatedLines.length; i++) {
// NOTE: only handles quoted filenames; unquoted form not yet supported (see showInSource / lineDirectiveUtils.ts)
const m = generatedLines[i].match(/^#line\s+(\d+)\s+"([^"]+)"/);
if (!m) continue;
const dirSrcLine = parseInt(m[1], 10);
Expand Down
43 changes: 43 additions & 0 deletions client/src/lineDirectiveUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Pure helper functions for line directive navigation.
* No VS Code dependencies — designed for testability and reusability.
*/

// ── Detection ─────────────────────────────────────────────────────────────────

/** Returns true if the file header looks like a Bison or Flex generated C file. */
export function isGeneratedFile(text: string): boolean {
const header = text.slice(0, 600);
return (
header.includes('/* A Bison parser, made by GNU Bison') ||
header.includes('/* Generated by GNU Bison') ||
header.includes('/* Generated by flex') ||
header.includes('/* A lexical scanner generated by flex')
);
}

// ── Line directive helpers ────────────────────────────────────────────────────

export interface LineDirective {
sourceLine: number; // 1-based line number in the source file
sourceFile: string;
directiveLine: number; // 0-based line index in the generated file where directive was found
}

/**
* Scan backwards from cursorLine to find the nearest #line N "file" or #line N file directive.
* Returns the directive data including the line index where it was found.
*/
export function findNearestLineDirective(lines: string[], cursorLine: number): LineDirective | null {
for (let i = cursorLine; i >= 0; i--) {
const m = lines[i].match(/^#line\s+(\d+)\s+"?([^"\s]+)"?/);
if (m) {
return {
sourceLine: parseInt(m[1], 10),
sourceFile: m[2],
directiveLine: i,
};
}
}
return null;
}
133 changes: 133 additions & 0 deletions tests/test-line-directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* Tests for lineDirectiveUtils pure helpers.
* Run: TS_NODE_PROJECT=tsconfig.base.json npx ts-node tests/test-line-directive.ts
*/
import { isGeneratedFile, findNearestLineDirective } from '../client/src/lineDirectiveUtils';

let passed = 0, failed = 0;
function assert(cond: boolean, msg: string, extra?: unknown) {
if (cond) { console.log(` [PASS] ${msg}`); passed++; }
else { console.error(` [FAIL] ${msg}${extra !== undefined ? ` → ${JSON.stringify(extra)}` : ''}`); failed++; }
}

// ── isGeneratedFile ────────────────────────────────────────────────────────────
console.log('\nisGeneratedFile');

assert(
isGeneratedFile('/* A Bison parser, made by GNU Bison 3.8.2. */\n...rest...'),
'detects Bison generated file (made by)'
);
assert(
isGeneratedFile('/* Generated by GNU Bison 3.8 */\n...rest...'),
'detects Bison generated file (generated by)'
);
assert(
isGeneratedFile('/* Generated by flex 2.6.4 */\n...rest...'),
'detects Flex generated file (generated by flex)'
);
assert(
isGeneratedFile('/* A lexical scanner generated by flex */\n'),
'detects Flex generated file (lexical scanner)'
);
assert(
!isGeneratedFile('// My hand-written parser\n'),
'rejects non-generated file'
);
assert(
!isGeneratedFile(''),
'rejects empty file'
);

// ── findNearestLineDirective ───────────────────────────────────────────────────
console.log('\nfindNearestLineDirective — quoted filenames');

{
// Cursor ON the directive line → offset = 0
const lines = ['#line 28 "parser.y"', 'some_func();'];
const d = findNearestLineDirective(lines, 0);
assert(d !== null, 'quoted: finds directive on cursor line');
if (d) {
assert(d.sourceLine === 28, 'quoted: sourceLine = 28', d.sourceLine);
assert(d.sourceFile === 'parser.y', 'quoted: sourceFile = parser.y', d.sourceFile);
assert(d.directiveLine === 0, 'quoted: directiveLine = 0', d.directiveLine);
}
}

{
// Cursor 4 lines below the directive → offset = 4
// The caller should compute: targetLine = sourceLine - 1 + (cursorLine - directiveLine) = 27 + 4 = 31
const lines = [
'#line 28 "parser.y"',
'#include "config.h"',
'#include <stdlib.h>',
'#include <string.h>',
'void some_func(int mode) {',
];
const d = findNearestLineDirective(lines, 4);
assert(d !== null, 'quoted: finds directive 4 lines above cursor');
if (d) {
assert(d.sourceLine === 28, 'quoted: sourceLine = 28', d.sourceLine);
assert(d.sourceFile === 'parser.y', 'quoted: sourceFile (cursor 4 lines below)', d.sourceFile);
assert(d.directiveLine === 0, 'quoted: directiveLine = 0', d.directiveLine);
}
}

console.log('\nfindNearestLineDirective — unquoted filenames');

{
// GitMensch case: no quotes
const lines = ['#line 28 parser.y', 'some_func();'];
const d = findNearestLineDirective(lines, 1);
assert(d !== null, 'unquoted: finds directive');
if (d) {
assert(d.sourceLine === 28, 'unquoted: sourceLine = 28', d.sourceLine);
assert(d.sourceFile === 'parser.y', 'unquoted: sourceFile = parser.y', d.sourceFile);
assert(d.directiveLine === 0, 'unquoted: directiveLine = 0', d.directiveLine);
}
}

{
// Unquoted relative path
const lines = ['#line 5 src/parser.y', 'x = 1;'];
const d = findNearestLineDirective(lines, 1);
assert(d !== null, 'unquoted path: finds directive');
if (d) {
assert(d.sourceFile === 'src/parser.y', 'unquoted path: sourceFile', d.sourceFile);
}
}

console.log('\nfindNearestLineDirective — edge cases');

{
// No directive anywhere → null
const lines = ['int x = 0;', 'return x;'];
const d = findNearestLineDirective(lines, 1);
assert(d === null, 'returns null when no directive exists');
}

{
// Multiple directives — picks the nearest one above cursor
const lines = [
'#line 10 "a.y"',
'foo();',
'#line 20 "b.y"',
'bar();',
];
const d = findNearestLineDirective(lines, 3);
assert(d !== null, 'picks nearest directive above cursor');
if (d) {
assert(d.sourceLine === 20, 'picks nearest directive above cursor (line)', d.sourceLine);
assert(d.sourceFile === 'b.y', 'picks nearest directive above cursor (file)', d.sourceFile);
assert(d.directiveLine === 2, 'directiveLine of nearest directive', d.directiveLine);
}
}

{
// Cursor on line 0 with no directive → null
const lines = ['int x = 0;'];
const d = findNearestLineDirective(lines, 0);
assert(d === null, 'no directive on line 0 → null');
}

console.log(`\nResults: ${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
Loading