From d994c6b0c1f8fe0a5f297761e6ecf25bc4d7d614 Mon Sep 17 00:00:00 2001 From: Bob Cozzi Date: Tue, 7 Apr 2026 09:11:41 -0500 Subject: [PATCH 1/2] fix: ruler improvements for fixed-format SQL continuation, stale decoration, and Source Change Date overlap - Skip Shift+F4 ruler on legacy embedded SQL continuation lines (col 7 = '+') - Clear ruler immediately when active editor changes (e.g. Cmd+Shift+F4 webview opens) so the decoration does not remain frozen over the source - Cap ruler background width at 80ch to avoid painting over Source Change Date columns that IBM i source physical files carry beyond column 80 --- CHANGELOG.md | 3 ++ extension/client/src/language/columnAssist.ts | 33 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 036ae40f..c9b1f8df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes to the "vscode-rpgle" extension can be found in the ### Fixed +- **Shift+F4 ruler incorrectly shown on legacy embedded SQL lines**: the fixed-format spec ruler is no longer painted on C spec SQL continuation lines (column 7 = `+`, e.g. `.....C+ FROM qiws.qcustcdt;`), matching the existing behaviour for directive lines (column 7 = `/`). +- **Shift+F4 ruler persists when Cmd+Shift+F4 Column Assistant opens**: the ruler is now cleared immediately when the active editor changes (e.g. focus moves to the SEU-style Column Assistant webview panel), preventing it from staying frozen over the source until the next cursor move. +- **Shift+F4 ruler overlaps Source Change Date columns**: the opaque ruler background is now capped at 80 columns wide, so it no longer paints over the Source Change Date text that IBM i source physical files carry beyond column 80. - Fixed type inference for fixed-format D-specifications when the data type position (40) is blank - Correctly distinguishes between "no decimals specified" and "decimals = 0" - Applies proper RPGLE type inference rules: diff --git a/extension/client/src/language/columnAssist.ts b/extension/client/src/language/columnAssist.ts index 9268bd59..4e72bae5 100644 --- a/extension/client/src/language/columnAssist.ts +++ b/extension/client/src/language/columnAssist.ts @@ -1,5 +1,5 @@ -import { commands, DecorationOptions, ExtensionContext, Range, Selection, TextDocument, ThemeColor, window } from 'vscode'; +import { commands, DecorationOptions, ExtensionContext, Range, Selection, TextDocument, TextEditor, ThemeColor, window } from 'vscode'; import * as Configuration from "../configuration"; import { loadBase } from '../base'; @@ -17,12 +17,19 @@ const outlineBar = window.createTextEditorDecorationType({}); let rulerEnabled = Configuration.get(Configuration.RULER_ENABLED_BY_DEFAULT) || false let currentEditorLine = -1; +// Tracks the last RPG fixed-format editor that had the ruler painted, so it can +// be cleared when focus moves to a webview (e.g. Ctrl+Shift+F4 prompter) or to +// a non-RPG file, where onDidChangeTextEditorSelection never fires. +let lastRpgleEditor: TextEditor | undefined = undefined; import { SpecFieldDef, SpecFieldValue, SpecRulers, specs } from '../schemas/specs'; const getAreasForLine = (line: string, index: number) => { if (line.length < 6) return undefined; - if (line[6] === `*` || line[6] === `/`) return undefined; + // Skip comments (*), compiler directives (/), and legacy embedded SQL continuation lines (+). + // A `+` in column 7 marks a C spec SQL continuation (e.g. `C+ FROM qiws.qcustcdt;`) + // and should never trigger the spec ruler. + if (line[6] === `*` || line[6] === `/` || line[6] === `+`) return undefined; const specLetter = line[5].toUpperCase(); if (specs[specLetter]) { @@ -107,6 +114,19 @@ export function registerColumnAssist(context: ExtensionContext) { clearRulers(editor); } }), + + // Clear the ruler when the active editor changes (e.g. focus moves to a + // webview panel such as the Ctrl+Shift+F4 Column Assistant, or to a + // different file). onDidChangeTextEditorSelection does not fire in those + // cases, so the ruler would otherwise remain painted indefinitely. + window.onDidChangeActiveTextEditor(newEditor => { + if (lastRpgleEditor && newEditor !== lastRpgleEditor) { + clearRulers(lastRpgleEditor); + } + if (newEditor && rulerEnabled) { + updateRuler(newEditor); + } + }), ) } @@ -183,7 +203,10 @@ function updateRuler(editor = window.activeTextEditor) { before: { contentText: positionsData.outline, color: new ThemeColor(`editorLineNumber.foreground`), - textDecoration: `none; position: absolute; top: -1.4em; background-color: var(--vscode-editor-background); width: 100vw;`, + // width is capped at 80ch (the fixed-format source width) so the + // opaque background does not paint over the Source Change Date + // columns that IBM i source physical files carry beyond column 80. + textDecoration: `none; position: absolute; top: -1.4em; background-color: var(--vscode-editor-background); width: 80ch;`, } } }, @@ -193,6 +216,7 @@ function updateRuler(editor = window.activeTextEditor) { } currentEditorLine = lineNumber; + lastRpgleEditor = editor; } } } @@ -208,6 +232,9 @@ function clearRulers(editor = window.activeTextEditor) { editor.setDecorations(notCurrentArea, []); editor.setDecorations(outlineBar, []); } + if (editor === lastRpgleEditor) { + lastRpgleEditor = undefined; + } } interface FieldBox { From 11383fb6ae5e7fd2b9c847beca7e96aef8987f5e Mon Sep 17 00:00:00 2001 From: Bob Cozzi Date: Wed, 13 May 2026 21:40:50 -0400 Subject: [PATCH 2/2] fix: resolve merge conflicts with upstream OPM RPG support (PR #515) --- CHANGELOG.md | 74 ++++++++- extension/client/src/language/columnAssist.ts | 45 ++++-- extension/client/src/schemas/specs.ts | 145 ++++++++++++++++++ 3 files changed, 245 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9b1f8df..5ef18845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,73 @@ All notable changes to the "vscode-rpgle" extension can be found in the [Releases](https://github.com/codefori/vscode-rpgle/releases) section of the GitHub repository. -## Unreleased +## [Unreleased] + +### Added - OPM RPG Language Support + +- **OPM (Original Program Model) RPG Parser**: Full support for legacy OPM RPG language + - New [`OpmParser`](language/opm/parser.ts) class for fixed-format specification parsing + - Complete specification type support: Control (H), File (F), Extension (E), Input (I), Calculation (C), and Output (O) specs + - [`parseSpecification()`](language/opm/specs.ts) function with typed specification objects for all OPM spec types + - Symbol extraction for: files, data structures, variables, constants, subroutines, PLISTs, KLISTs, and CALL statements + - External file format resolution via table fetch (EXTNAME support) + - Include file processing (`/COPY` directive support) + - Embedded SQL recognition and aggregation + - Local Data Area (LDA) marker detection (`**`) to stop parsing at compile-time data + +- **Dual Parser Architecture** + - [`ParserFactory`](language/parserFactory.ts) class for intelligent parser routing based on RPG language variant + - Reorganized ILE parser to [`language/ile/`](language/ile/) subdirectory for clean separation + - Common [`IParser`](language/parserFactory.ts:12-18) interface implemented by both parsers + - Shared table fetch and include file resolution between OPM and ILE parsers + - Dynamic parser selection in language server based on RPG language variant + +- **Language Server Integration** + - VS Code language activation for OPM RPG via `onLanguage:rpg` event + - All providers updated to use appropriate parser: + - Completions, hover, definitions, references, rename, signature help + - Document symbols (outline view) + - Code actions and linting + - Unified cache model shared between both parsers + +- **Comprehensive Test Suite** + - [`tests/suite/opm/scope.test.ts`](tests/suite/opm/scope.test.ts) - 8 parser integration tests covering real-world OPM scenarios + - [`tests/suite/opm/specs.test.ts`](tests/suite/opm/specs.test.ts) - Specification parsing validation tests + - 7 OPM test fixtures covering various patterns: data structures, file operations, subroutines, PLISTs, KLISTs, edge cases + - Test coverage for: symbol resolution, external formats, multi-line C-specs, constants, LDA boundaries + +### Changed - OPM RPG Language Support + +- **Column Assistant and Fixed-Format Tools now support both RPG language variants**: + - All commands (`Shift+F4`, `Ctrl+Shift+F4`, `Ctrl+[`, `Ctrl+]`) now work with both ILE RPG and OPM RPG + - Added **OPM-specific spec definitions** (`opmSpecs` and `opmSpecRulers`) in [`specs.ts`](extension/client/src/schemas/specs.ts) with correct RPG III column positions + - Column Assistant automatically uses correct spec definitions based on RPG language variant + - **OPM specs supported**: H-spec (Control), E-spec (Extension), F-spec (File), I-spec (Input), C-spec (Calculation), O-spec (Output) + - **Critical fix**: OPM and ILE have **different column positions** for specs (e.g., C-spec Factor1 is 18-27 in OPM vs 12-25 in ILE) + - Updated `documentIsFree()` to recognize OPM as always fixed-format + - Language ID checks updated throughout [`columnAssist.ts`](extension/client/src/language/columnAssist.ts) and [`package.json`](package.json) +- Folder structure reorganized for dual-parser architecture: + - ILE parser moved from `language/*.ts` to `language/ile/*.ts` + - OPM parser added in `language/opm/` directory + - Shared models remain in `language/models/` +- All language server providers now use `getParser(uri)` for dynamic parser selection +- Extension now supports both ILE RPG (`.rpgle`/`.sqlrpgle`) and OPM RPG (`.rpg`/`.sqlrpg`) language variants + +### Added - Previous ILE RPG Enhancements + +- **Input specification (`I` spec) parsing** (fixed-format ILE RPG): the parser now recognises and processes all four I spec sub-types — `programRecord`, `programField`, `externalRecord`, and `externalField`. +- New `parseISpec()` and `prettyTypeFromISpecTokens()` functions in `language/models/fixed.ts` for decoding fixed-format I spec column layout. +- New `trimQuotes()` utility exported from `language/tokens.ts`. +- `cache.inputs` getter — returns all `Declaration` objects whose type is `"input"`, mirroring the existing `cache.structs`, `cache.files`, etc. accessors. +- Document Symbols provider now surfaces Input spec records as `Interface` symbols with their fields listed as `Property` children, making them visible in the Outline view. +- Constants with sub-items are now shown as `Enum` / `EnumMember` symbols in the Outline view. +- Test fixtures for `QRPGLESRC` and `QCBBLESRC` physical files (`tests/tables/qrpglesrc.ts`, `tests/tables/qcbblesrc.ts`). +- Test suite `tests/suite/ispec.test.ts` covering I spec rename/prefix handling, column-position range maths, and program-described file field definitions. +- The popup Hover, Go-to-Definition, and Find-All-References now work for symbols defined on Input specs, subject to the same fixed-format limitations that already existed for F, D, and C specs: because `getWordRangeAtPosition` is not spec/column aware, `hover` does not trigger when the cursor is positioned on a name that is immediately adjacent to a non-space character (e.g. the spec-type letter `D` in col 6 with the name starting in column 7, or a decimal-position digit in col 48 of an I spec and the field name immediately after it.) It is a future objective to enhance this feature. ### Fixed +- **Linter diagnostics not clearing when `**FREE` is removed**: previously, if a source file was opened as `**FREE` (activating the linter) and `**FREE` was later removed or the file was re-opened without it, stale linter diagnostics persisted indefinitely. `refreshLinterDiagnostics` now explicitly sends an empty diagnostics notification when the file is not `**FREE`, clearing any previously published warnings. - **Shift+F4 ruler incorrectly shown on legacy embedded SQL lines**: the fixed-format spec ruler is no longer painted on C spec SQL continuation lines (column 7 = `+`, e.g. `.....C+ FROM qiws.qcustcdt;`), matching the existing behaviour for directive lines (column 7 = `/`). - **Shift+F4 ruler persists when Cmd+Shift+F4 Column Assistant opens**: the ruler is now cleared immediately when the active editor changes (e.g. focus moves to the SEU-style Column Assistant webview panel), preventing it from staying frozen over the source until the next cursor move. - **Shift+F4 ruler overlaps Source Change Date columns**: the opaque ruler background is now capped at 80 columns wide, so it no longer paints over the Source Change Date text that IBM i source physical files carry beyond column 80. @@ -17,10 +80,15 @@ All notable changes to the "vscode-rpgle" extension can be found in the - Subfields with decimals → Zoned - Subfields without decimals → Char - Resolves 5 failing test cases in fixed-format parser tests - - Fixed hover tooltips not displaying for data structure subfields - Added support for qualified data structures (accessed as `DS.FIELD`) - Added support for unqualified data structures (accessed as standalone `FIELD`) - Hover text now displays parent structure context: `parent.field Type QUALIFIED` or `*UNQUALIFIED` or `*UNNAMED` - Shows description tags from external file metadata when available - - Displays reference count in hover information \ No newline at end of file + - Displays reference count in hover information + +### Changed + +- `cache.find()` and `cache.findAll()` now accept an optional `ignorePrefix` parameter and internally delegate sub-item lookup to a new private `findSubfields()` method, improving correctness when files declare a `PREFIX` keyword. +- `prettyTypeFromToken` renamed to `prettyTypeFromDSpecTokens` for clarity. +- `language/models/fixed.js` converted to `language/models/fixed.ts` (TypeScript) to support the new typed I spec APIs; the original `.js` file has been removed. diff --git a/extension/client/src/language/columnAssist.ts b/extension/client/src/language/columnAssist.ts index 4e72bae5..6e7588a9 100644 --- a/extension/client/src/language/columnAssist.ts +++ b/extension/client/src/language/columnAssist.ts @@ -22,31 +22,35 @@ let currentEditorLine = -1; // a non-RPG file, where onDidChangeTextEditorSelection never fires. let lastRpgleEditor: TextEditor | undefined = undefined; -import { SpecFieldDef, SpecFieldValue, SpecRulers, specs } from '../schemas/specs'; +import { SpecFieldDef, SpecFieldValue, SpecRulers, specs, opmSpecs, opmSpecRulers } from '../schemas/specs'; -const getAreasForLine = (line: string, index: number) => { +const getAreasForLine = (line: string, index: number, languageId: string = 'rpgle') => { if (line.length < 6) return undefined; // Skip comments (*), compiler directives (/), and legacy embedded SQL continuation lines (+). // A `+` in column 7 marks a C spec SQL continuation (e.g. `C+ FROM qiws.qcustcdt;`) // and should never trigger the spec ruler. if (line[6] === `*` || line[6] === `/` || line[6] === `+`) return undefined; + // Use OPM specs for .rpg files, ILE specs for .rpgle files + const specDefinitions = languageId === 'rpg' ? opmSpecs : specs; + const rulerDefinitions = languageId === 'rpg' ? opmSpecRulers : SpecRulers; + const specLetter = line[5].toUpperCase(); - if (specs[specLetter]) { - const specification = specs[specLetter]; + if (specDefinitions[specLetter]) { + const specification = specDefinitions[specLetter]; const active = specification.findIndex((box: any) => index >= box.start && index <= box.end); return { specification, active, - outline: SpecRulers[specLetter] + outline: rulerDefinitions[specLetter] }; - } else if (SpecRulers[specLetter]) { + } else if (rulerDefinitions[specLetter]) { return { specification: [] as SpecFieldDef[], active: -1, - outline: SpecRulers[specLetter] + outline: rulerDefinitions[specLetter] }; } } @@ -55,6 +59,9 @@ function documentIsFree(document: TextDocument) { if (document.languageId === `rpgle`) { const line = document.getText(new Range(0, 0, 0, 6)).toUpperCase(); return line === `**FREE`; + } else if (document.languageId === `rpg`) { + // OPM RPG is always fixed-format + return false; } return false; @@ -67,14 +74,15 @@ export function registerColumnAssist(context: ExtensionContext) { if (editor) { const document = editor.document; - if (document.languageId === `rpgle`) { + if (document.languageId === `rpgle` || document.languageId === `rpg`) { if (!documentIsFree(document)) { const lineNumber = editor.selection.start.line; const positionIndex = editor.selection.start.character; const positionsData = await promptLine( document.getText(new Range(lineNumber, 0, lineNumber, 100)), - positionIndex + positionIndex, + document.languageId ); if (positionsData) { @@ -131,14 +139,15 @@ export function registerColumnAssist(context: ExtensionContext) { } function moveFromPosition(direction: "left"|"right", editor = window.activeTextEditor) { - if (editor && editor.document.languageId === `rpgle` && !documentIsFree(editor.document)) { + if (editor && (editor.document.languageId === `rpgle` || editor.document.languageId === `rpg`) && !documentIsFree(editor.document)) { const document = editor.document; const lineNumber = editor.selection.start.line; const positionIndex = editor.selection.start.character; const positionsData = getAreasForLine( document.getText(new Range(lineNumber, 0, lineNumber, 100)), - positionIndex + positionIndex, + document.languageId ); if (positionsData) { @@ -165,14 +174,15 @@ function updateRuler(editor = window.activeTextEditor) { if (editor) { const document = editor.document; - if (document.languageId === `rpgle`) { + if (document.languageId === `rpgle` || document.languageId === `rpg`) { if (!documentIsFree(document)) { const lineNumber = editor.selection.start.line; const positionIndex = editor.selection.start.character; const positionsData = getAreasForLine( document.getText(new Range(lineNumber, 0, lineNumber, 100)), - positionIndex + positionIndex, + document.languageId ); if (positionsData) { @@ -245,7 +255,7 @@ interface FieldBox { maxLength?: number } -async function promptLine (line: string, _index: number): Promise { +async function promptLine (line: string, _index: number, languageId: string = 'rpgle'): Promise { const base = loadBase(); if (!base) { @@ -258,9 +268,12 @@ async function promptLine (line: string, _index: number): Promise