diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e136a3..169b31a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,8 @@ on: branches: ["main", "dev"] pull_request: branches: ["main", "dev"] + # manual run in actions tab - for all branches + workflow_dispatch: jobs: build-and-test: @@ -25,9 +27,24 @@ jobs: - name: Compile run: npm run compile - - name: Run parser tests + - name: Test — parsers run: npx ts-node --project server/tsconfig.json tests/test-parsers.ts + - name: Test — diagnostic codes + run: TS_NODE_PROJECT=tsconfig.base.json npx ts-node tests/test-diagnostic-codes.ts + + - name: Test — version settings + run: TS_NODE_PROJECT=tsconfig.base.json npx ts-node tests/test-version-settings.ts + + - name: Test — fix-it hints (code actions) + run: TS_NODE_PROJECT=tsconfig.base.json npx ts-node tests/test-fix-it-hints.ts + + - name: Test — line directive utils + run: TS_NODE_PROJECT=tsconfig.base.json npx ts-node tests/test-line-directive.ts + + - name: Test — new providers + run: TS_NODE_PROJECT=tsconfig.base.json npx ts-node tests/test-new-providers.ts + - name: Package VSIX run: npx vsce package --no-dependencies -o extension.vsix diff --git a/.github/workflows/publish-ovsx.yml b/.github/workflows/publish-ovsx.yml deleted file mode 100644 index 34b5588..0000000 --- a/.github/workflows/publish-ovsx.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Publish to Open VSX - -on: - push: - tags: - - "v*" - -jobs: - publish-ovsx: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - - - name: Install dependencies - run: npm ci - - - name: Build VSIX - run: npx vsce package - - - name: Publish to Open VSX - if: ${{ secrets.OVSX_PAT != '' }} - env: - OVSX_PAT: ${{ secrets.OVSX_PAT }} - run: npx ovsx publish *.vsix --pat "$OVSX_PAT" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b31fac..8d32c9f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,13 +6,23 @@ on: - "v*" jobs: - package: + release: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify tag is on main + run: | + git fetch origin main + if ! git merge-base --is-ancestor HEAD origin/main; then + echo "❌ Release tags must be created from the main branch." + exit 1 + fi - name: Setup Node.js uses: actions/setup-node@v4 @@ -24,12 +34,30 @@ jobs: run: npm ci - name: Build VSIX - run: | - npm run package - npx vsce package + run: npx vsce package - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: files: "*.vsix" - generate_release_notes: true \ No newline at end of file + generate_release_notes: true + + - name: Publish to Open VSX + env: + OVSX_PAT: ${{ secrets.OVSX_PAT }} + run: | + if [ -z "$OVSX_PAT" ]; then + echo "⚠️ OVSX_PAT not set — skipping Open VSX publish" + else + npx ovsx publish *.vsix --pat "$OVSX_PAT" + fi + + - name: Publish to VS Marketplace + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + run: | + if [ -z "$VSCE_PAT" ]; then + echo "⚠️ VSCE_PAT not set — skipping VS Marketplace publish" + else + npx vsce publish --pat "$VSCE_PAT" + fi diff --git a/.gitignore b/.gitignore index 97b7c17..1000672 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ server/out/ .env tests/_* docs/ -.vscode/ \ No newline at end of file +.vscode/ +.claude/ +graphify-out/ +tests/manuals/ +.vscode/launch.json diff --git a/.vscodeignore b/.vscodeignore index 9d47de6..d9bfaf5 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -25,3 +25,5 @@ package-lock.json .env .env.* docs/ +graphify-out/ +tests/manuals/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 58dd571..8a99e96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to the **Bison/Flex Language Support** extension will be documented in this file. +## [1.5.3] - 2026-04-20 + +### Added + +- **Show in Generated File — output channel** (#27): a dedicated *Bison/Flex Navigation* output channel now logs every directory and file path searched when locating the generated file. The channel is shown automatically when the search starts, making it easy to diagnose `buildDirectory` misconfigurations. +- **Show in Generated File — automake file names** (#45, contributed by [@GitMensch](https://github.com/GitMensch)): the generated-file search now also looks for the automake-style names used when Bison/Flex is called from an automake rule (`.tab.c`/`.tab.h`, `_tab.c`/`_tab.h`, `lex.yy.c`, `lex._.c`, and their `.cc`/`.cpp` variants). + +### Fixed + +- **Show in Source — `#line` offset and unquoted filenames** (#44): the command now correctly handles `#line` directives without quoted filenames (e.g. `#line 42 parser.y` in addition to `#line 42 "parser.y"`), and the line offset is now computed correctly so the editor opens at the exact grammar line. +- **Show in Generated File — cached lookup / multi-root workspaces** (#27): the last successfully found generated file is cached per source file, avoiding redundant filesystem scans on repeated calls. In multi-root workspaces `${workspaceFolder}` in `bisonFlex.buildDirectory` now resolves to the workspace folder that contains the source file, so a single setting works correctly across all roots. + +### Documentation + +- `bisonFlex.buildDirectory` is now documented in the README with supported formats (`${workspaceFolder}`, relative paths, absolute paths) and multi-root workspace behaviour. + +### CI / Internal + +- Workflows consolidated: CI now runs on `push`/`pull_request` for both `main` and `dev`; the VSIX artifact is built on every CI run. + +--- + ## [1.5.2] - 2026-04-05 ### Added diff --git a/README.md b/README.md index cc1e63d..711eb44 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,16 @@ Jump between Bison/Flex grammar sources and their generated C files using `#line - **Bison/Flex: Show in Source** — from a generated `.tab.c` / `lex.yy.c` file, reads the nearest `#line N "file.y"` directive above the cursor and opens the grammar source at the correct line. Appears in the context menu only when a generated file is detected. - **Bison/Flex: Show in Generated File** — from a `.y` / `.l` source, locates the generated file and navigates to the matching line. Searches `bisonFlex.buildDirectory`, then CMake/Makefile detection, then the same directory, then a workspace-wide scan. A QuickPick is shown when multiple candidates are found. +#### Configuring `bisonFlex.buildDirectory` + +Set this to the directory where Bison/Flex writes its generated files when they are not placed next to the source: + +- **`${workspaceFolder}`** — resolved to the workspace folder that contains the source file. In multi-root workspaces each folder resolves independently, so a single setting works across all roots. +- **Relative path** — resolved relative to that same workspace folder (e.g. `build` or `cmake-build-debug/parser`). +- **Absolute path** — used as-is. + +If the setting is empty or the generated file is not found there, the extension falls back to CMake/Makefile-detected paths, then the source file's own directory, then a workspace-wide scan. + ### Autocompletion Context-aware suggestions triggered as you type: @@ -226,7 +236,7 @@ Then press `F5` in VS Code to launch the Extension Development Host. | `bisonFlex.minVersionBison` | `string` | `""` | Suppress checks that require a newer Bison version (e.g. `"3.0"`). Fires `bison/feature-requires-version` when a `%define` feature exceeds this version. | | `bisonFlex.minVersionFlex` | `string` | `""` | Same as above for Flex. | | `bisonFlex.disabledChecks` | `array` | `[]` | Diagnostic code slugs to suppress entirely (e.g. `["bison/shift-reduce", "flex/missing-yywrap"]`). | -| `bisonFlex.buildDirectory` | `string` | `""` | Path to the build output directory. Used by **Show in Generated File** to locate `.tab.c` / `lex.yy.c` when they are not next to the source. | +| `bisonFlex.buildDirectory` | `string` | `""` | Directory where Bison/Flex writes its generated files (`.tab.c`, `lex.yy.c`). Supports `${workspaceFolder}` (resolved per workspace root in multi-root setups), paths relative to that root, and absolute paths. See [Configuring `bisonFlex.buildDirectory`](#configuring-bisonflexbuilddirectory) for details. | --- diff --git a/client/src/lineDirectiveNavigation.ts b/client/src/lineDirectiveNavigation.ts index 3dcc412..5ee62e5 100644 --- a/client/src/lineDirectiveNavigation.ts +++ b/client/src/lineDirectiveNavigation.ts @@ -1,37 +1,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') - ); -} - -// ── Direction 1: generated → source ────────────────────────────────────────── +import { window, workspace, Uri, Position, Range, ViewColumn, OutputChannel } from 'vscode'; -interface LineDirective { - sourceLine: number; // 1-based line number in the source file - sourceFile: string; -} +export { isGeneratedFile } from './lineDirectiveUtils'; +import { isGeneratedFile, findNearestLineDirective } from './lineDirectiveUtils'; -/** 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; -} +// ── Direction 1: generated → source ────────────────────────────────────────── /** Navigate from the generated C file at cursorLine to the original grammar source. */ export async function showInSource(): Promise { @@ -70,18 +44,45 @@ export async function showInSource(): Promise { sourcePath = found; } - const targetLine = Math.max(0, directive.sourceLine - 1); // convert to 0-based + const offset = cursorLine - directive.directiveLine; // lines between directive and cursor + 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 }); } +// ── Navigation output channel ──────────────────────────────────────────────── + +let navChannel: OutputChannel | undefined; +function getNavChannel(): OutputChannel { + if (!navChannel) navChannel = window.createOutputChannel('Bison/Flex Navigation'); + return navChannel; +} + +/** Resolve `bisonFlex.buildDirectory`: substitutes ${workspaceFolder} and resolves relative paths. + * In multi-root workspaces, folders[0] is used as the root for relative paths. + * workspaceFile is intentionally not used: a .code-workspace file can be stored anywhere + * outside the workspace, making it unsuitable as a base for resolving grammar paths. */ +function resolveSettingBuildDir(rawDir: string, sourceFilePath: string): string { + const wsFolder = workspace.workspaceFolders?.[0]?.uri.fsPath ?? path.dirname(sourceFilePath); + let resolved = rawDir.replace(/\$\{workspaceFolder\}/g, wsFolder); + if (!path.isAbsolute(resolved)) { + resolved = path.resolve(wsFolder, resolved); + } + return resolved; +} + // ── Direction 2: source → generated ────────────────────────────────────────── const BISON_CANDIDATES = (base: string, dir: string): string[] => [ path.join(dir, base + '.tab.c'), path.join(dir, base + '.tab.cpp'), path.join(dir, base + '.tab.cc'), + path.join(dir, base + '.c'), + path.join(dir, base + '.cc'), + path.join(dir, base + '.c++'), + path.join(dir, base + '.cxx'), + path.join(dir, base + '.cpp'), ]; const FLEX_CANDIDATES = (base: string, dir: string): string[] => [ @@ -89,6 +90,11 @@ const FLEX_CANDIDATES = (base: string, dir: string): string[] => [ path.join(dir, 'lex.yy.cc'), path.join(dir, base + '.yy.c'), path.join(dir, base + '.yy.cpp'), + path.join(dir, base + '.c'), + path.join(dir, base + '.cc'), + path.join(dir, base + '.c++'), + path.join(dir, base + '.cxx'), + path.join(dir, base + '.cpp'), ]; /** Scan CMakeLists.txt up the directory tree and return a build directory hint. */ @@ -150,16 +156,33 @@ function findMakefileBuildDir(sourceFilePath: string): string | undefined { return undefined; } +let lastFoundCache: { sourceFilePath: string; buildDir: string; generatedPath: string } | undefined; + /** Locate the generated file corresponding to a grammar source file. */ async function findGeneratedFile(sourceFilePath: string): Promise { + const ch = getNavChannel(); const config = workspace.getConfiguration('bisonFlex'); - const settingBuildDir = config.get('buildDirectory', '').trim() || undefined; + const rawBuildDir = config.get('buildDirectory', '').trim(); + + if (lastFoundCache?.sourceFilePath === sourceFilePath && lastFoundCache.buildDir === rawBuildDir) { + return lastFoundCache.generatedPath; + } + + const settingBuildDir = rawBuildDir ? resolveSettingBuildDir(rawBuildDir, sourceFilePath) : undefined; const sourceDir = path.dirname(sourceFilePath); const base = path.basename(sourceFilePath, path.extname(sourceFilePath)); const ext = path.extname(sourceFilePath).toLowerCase(); - const isBison = ['.y', '.yy', '.ypp', '.bison'].includes(ext); + const isBison = ['.y', '.yy', '.y++', '.ypp', '.yxx', '.bison'].includes(ext); const candidates = isBison ? BISON_CANDIDATES : FLEX_CANDIDATES; + ch.show(true); + ch.appendLine(`\n─── Show in Generated: ${path.basename(sourceFilePath)} ───`); + if (rawBuildDir) { + ch.appendLine(` buildDirectory: "${rawBuildDir}" → ${settingBuildDir}`); + } else { + ch.appendLine(` buildDirectory: (not set)`); + } + const dirsToTry: string[] = []; if (settingBuildDir) dirsToTry.push(settingBuildDir); const cmakeDir = findCmakeBuildDir(sourceFilePath); @@ -169,22 +192,51 @@ async function findGeneratedFile(sourceFilePath: string): Promise dirsToTry.push(sourceDir); for (const dir of dirsToTry) { + ch.appendLine(` dir: ${dir}`); for (const c of candidates(base, dir)) { - if (fs.existsSync(c)) return c; + ch.appendLine(` ${c}`); + if (fs.existsSync(c)) { + ch.appendLine(` ✓ found: ${c}`); + lastFoundCache = { sourceFilePath, buildDir: rawBuildDir, generatedPath: c }; + return c; + } } } // Workspace-wide search as last resort — let user pick if ambiguous - const pattern = isBison ? `**/${base}.tab.{c,cpp,cc}` : `**/lex.yy.{c,cc}`; - const found = await workspace.findFiles(pattern, '**/node_modules/**', 10); - if (found.length === 0) return null; - if (found.length === 1) return found[0].fsPath; + ch.appendLine(` → no local match, trying workspace search`); + let pattern = isBison ? `**/${base}.tab.{c,cpp,cc}` : `**/lex.yy.{c,cc}`; + let found = await workspace.findFiles(pattern, '**/node_modules/**', 10); + + // in case of no results, check for ylwrap names (no tab/lex) + // we only do that after the initial run to not force a picker if + // the tool's default names are available somewhere in the workspace + if (found.length === 0) { + pattern = `**/${base}.{c,cc,c++,cxx,cpp}`; + ch.appendLine(` → retrying with ylwrap pattern: ${pattern}`); + found = await workspace.findFiles(pattern, '**/node_modules/**', 10); + } + + if (found.length === 0) { + ch.appendLine(` ✗ not found`); + return null; + } + if (found.length === 1) { + ch.appendLine(` ✓ found (workspace): ${found[0].fsPath}`); + lastFoundCache = { sourceFilePath, buildDir: rawBuildDir, generatedPath: found[0].fsPath }; + return found[0].fsPath; + } const pick = await window.showQuickPick( found.map(u => ({ label: workspace.asRelativePath(u), fsPath: u.fsPath })), { placeHolder: 'Multiple generated files found — select one' } ); - return pick ? pick.fsPath : null; + ch.appendLine(` ✓ user selected: ${pick?.fsPath ?? '(cancelled)'}`); + if (pick) { + lastFoundCache = { sourceFilePath, buildDir: rawBuildDir, generatedPath: pick.fsPath }; + return pick.fsPath; + } + return null; } /** @@ -197,6 +249,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); diff --git a/client/src/lineDirectiveUtils.ts b/client/src/lineDirectiveUtils.ts new file mode 100644 index 0000000..456042d --- /dev/null +++ b/client/src/lineDirectiveUtils.ts @@ -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; +} diff --git a/package.json b/package.json index 896161c..bd09186 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bison-flex-lang", "displayName": "Bison/Flex Language Support", "description": "Full-featured language support for GNU Bison (.y, .yy) and Flex/RE-flex (.l, .ll) — syntax highlighting with embedded C/C++, real-time diagnostics, intelligent autocompletion, and hover documentation for all directives.", - "version": "1.5.2", + "version": "1.5.3", "publisher": "theodevelop", "license": "MIT", "repository": { diff --git a/tests/test-line-directive.ts b/tests/test-line-directive.ts new file mode 100644 index 0000000..1595ae8 --- /dev/null +++ b/tests/test-line-directive.ts @@ -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 ', + '#include ', + '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); diff --git a/tests/test-new-providers.ts b/tests/test-new-providers.ts index 01198f0..1e11ead 100644 --- a/tests/test-new-providers.ts +++ b/tests/test-new-providers.ts @@ -213,6 +213,7 @@ const fakeDiag: Diagnostic = { range: Range.create(3, 10, 3, 19), message: `Token 'UNDECLARED' is used but not declared with %token.`, source: 'bison', + code: 'bison/undeclared-token', }; const caParams: CodeActionParams = {