diff --git a/RPGLE-NOTEBOOK-PLAN.md b/RPGLE-NOTEBOOK-PLAN.md new file mode 100644 index 00000000..13a059c0 --- /dev/null +++ b/RPGLE-NOTEBOOK-PLAN.md @@ -0,0 +1,681 @@ +# RPG Notebook Integration — Implementation Plan + +## Executive Summary + +Add RPG cell support to IBM i Notebooks (vscode-db2i) using RPGLE-REPL as the +backend execution engine. Users write RPG snippets in notebook cells, the +extension calls the `REPL_EXECUTE` stored procedure with the cell source as a +CLOB, and receives results back as a SQL result set. Fixed-format ruler overlays +enhance the editing experience. A configuration setting gates the feature, +pointing to the RPGLE-REPL library. + +--- + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────┐ +│ VS Code Notebook Cell (languageId: "rpgle") │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ dcl-s myName char(20); │ │ +│ │ myName = 'Hello RPG'; │ │ +│ │ replPrint(myName); │ │ +│ └───────────────────────────────────────────────────┘ │ +│ Ctrl+R / Run Cell │ +└──────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────┐ +│ IBMiController._doExecution() (new `rpgle` case) │ +│ │ +│ 1. Generate unique session ID │ +│ 2. CALL {lib}.REPL_EXECUTE(:source, :sessionId) │ +│ 3. Read result set (returned by stored procedure) │ +│ 4. Render results → NotebookCellOutput │ +└──────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────┐ +│ IBM i (RPGLE-REPL library) │ +│ │ +│ REPL_EXECUTE stored procedure (REPLEXEC program): │ +│ 1. Parse CLOB source into lines │ +│ 2. Write lines → REPLSRC (as named snippet) │ +│ 3. Generate → Compile → Run (REPL pipeline) │ +│ 4. Return REPLRSLT rows as result set │ +│ 5. Clean up REPLSRC snippet │ +└──────────────────────────────────────────────────────────┘ +``` + +--- + +## Phase 1: Detailed Design + +### 1. Configuration Setting + +**Setting:** `vscode-db2i.rpgleRepl.library` + +| Property | Value | +|----------|-------| +| Type | `string` | +| Default | `""` (empty = feature disabled) | +| Description | Library containing RPGLE-REPL objects. Leave empty to disable RPG notebook cells. Install RPGLE-REPL from https://github.com/tom-writes-code/rpgle-repl | +| Scope | Global (user-level) | + +**Behaviour:** +- **Empty/unset:** RPG language option is not registered for the notebook + controller. If a user manually changes a cell to `rpgle`, execution shows a + clear error message: _"RPG notebook cells require RPGLE-REPL. Set the + `vscode-db2i.rpgleRepl.library` setting to the library containing RPGLE-REPL, + or visit https://github.com/tom-writes-code/rpgle-repl for installation + instructions."_ +- **Set:** The library name is used in all SQL paths and CL calls (e.g. + `CALL MYLIB/REPLWRPR ...`). + +**Validation (on connect):** When the setting is non-empty and a connection is +established, run a lightweight check: + +```sql +SELECT COUNT(*) FROM QSYS2.SYSTABLES +WHERE TABLE_SCHEMA = :lib AND TABLE_NAME = 'REPLRSLT' +``` + +If the table doesn't exist, show an informational warning: +_"RPGLE-REPL library 'MYLIB' does not appear to contain RPGLE-REPL objects. +RPG notebook cells will not work until RPGLE-REPL is installed."_ + +### 2. Language Registration + +**Current state:** `supportedLanguages = ['sql', 'cl', 'shellscript']` + +**Change:** Always include `rpgle` in the supported languages list. The +controller will handle the "not configured" case at execution time with a +helpful error message. This means: +- Users can always switch a cell to RPG language +- The language picker always shows RPG as an option +- Unconfigured users get guided to install RPGLE-REPL on first execution + +**Serialisation:** No changes needed — the existing `metadata.tags[0]` +mechanism already stores arbitrary language IDs. An `rpgle` cell serialises +and deserialises correctly today. + +**Contributes keybinding:** Add `rpgle` to the Ctrl+R keybinding activation: + +```json +{ + "command": "notebook.cell.execute", + "key": "ctrl+r", + "mac": "cmd+r", + "when": "editorLangId == rpgle && resourceExtname == .inb" +} +``` + +### 3. Execution Flow (Controller) + +New case in `IBMiController._doExecution()`: + +``` +case 'rpgle': +``` + +#### Step 1 — Generate Session ID + +Use a UUID-based session ID to isolate this cell execution from others: + +```typescript +const sessionId = `NB-${crypto.randomUUID().substring(0, 22)}`; +``` + +This gives a unique 25-char ID (within the VARCHAR(28) limit) prefixed with +`NB-` to clearly identify notebook-originated sessions. + +#### Step 2 — Call REPL_EXECUTE Stored Procedure + +A single SQL CALL sends the cell source and receives the result set: + +```typescript +const result = await selected.job.query( + `CALL ${lib}.REPL_EXECUTE(?, ?)`, + { parameters: [cellSource, sessionId] } +).execute(); +``` + +The stored procedure handles everything internally: +1. Parses the CLOB into lines +2. Writes them to REPLSRC as a named snippet +3. Runs the full REPL pipeline (generate → compile → run) +4. Returns results from REPLRSLT as a result set +5. Cleans up the REPLSRC snippet + +The result set columns are: + +| Column | Type | Description | +|--------|------|-------------| +| `LINE_NUMBER` | DEC(4,0) | Source line that produced the result | +| `RESULT_NUMBER` | DEC(4,0) | Sequential result ID for the line | +| `RESULT_DESCRIPTION` | VARCHAR(1000) | The result text | +| `LOOP_COUNT` | DEC(5,0) | Loop iteration count | +| `RESULT_TYPE` | CHAR(32) | EVALUATION, TEST-SUCCESS, TEST-FAILURE, etc. | + +#### Step 3 — Render Results + +See [Section 5: Result Rendering](#5-result-rendering). + +#### Step 4 — Cleanup + +The stored procedure cleans up REPLSRC internally. REPLRSLT rows remain +tagged by `external_session_id` and accumulate harmlessly; periodic cleanup +can be added later if needed. + +### 4. REPL_EXECUTE Stored Procedure + +The stored procedure `REPL_EXECUTE` (backed by the `REPLEXEC` SQLRPGLE program) +provides a single-call interface for notebook integration. It replaces the +multi-step INSERT → CALL REPLWRPR → SELECT → DELETE dance with one SQL CALL +that accepts source and returns results. + +**SQL signature:** + +```sql +CALL {lib}.REPL_EXECUTE( + 'dcl-s greeting char(20); + greeting = ''Hello from RPG!''; + replPrint(greeting);', + 'NB-session-123' +) +``` + +**Internal flow (REPLEXEC.SQLRPGLE):** + +1. Derive snippet name from session ID (first 20 chars) +2. Clear any previous snippet/results for this name +3. Parse CLOB by line-feed character, INSERT each line into REPLSRC as a + named snippet +4. Call `createGeneratedSourceObject()` with the snippet location +5. Call `compileGeneratedSourceObject()` in batch mode +6. Call `runGeneratedProgramObject()` +7. UPDATE all REPLRSLT rows for this job with the external session ID +8. On any error, record the error via `replResult_recordCustomMessage()` and + still tag results with the external session ID +9. DECLARE/OPEN a cursor on REPLRSLT filtered by external session ID +10. SET RESULT SETS to return the cursor to the caller +11. Clean up REPLSRC snippet rows and REPLVARS + +**Registration (run after building REPLEXEC):** + +```sql +CREATE OR REPLACE PROCEDURE {lib}.REPL_EXECUTE ( + IN P_SOURCE CLOB(64K), + IN P_SESSION_ID VARCHAR(28) +) +DYNAMIC RESULT SETS 1 +EXTERNAL NAME '{lib}/REPLEXEC' +LANGUAGE RPGLE +PARAMETER STYLE GENERAL; +``` + +**Why this approach:** +- Single SQL round-trip from the extension +- All REPLSRC/REPLRSLT management is internal to the procedure +- Error paths are handled — errors are always tagged with the session ID +- The extension never touches REPLSRC or REPLRSLT directly +- REPLWRPR and REPLPRTR remain available for 5250 and CI/CD use + +### 5. Result Rendering + +Results from `REPLRSLT` should be rendered in a rich, RPG-aware format. + +#### 5a. Basic Table View + +The simplest approach: render a markdown table mapping each source line to its +results. + +```markdown +| Line | Code | Result | Type | +|------|-----------------------------------|------------------|------------| +| 1 | dcl-s myName char(20); | | | +| 2 | myName = 'Hello RPG'; | Hello RPG | EVALUATION | +| 3 | replPrint(myName); | Hello RPG | EVALUATION | +``` + +#### 5b. Rich HTML View (Recommended) + +A styled HTML output that mirrors the REPL experience: + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ 1 │ dcl-s myName char(20); │ +│ 2 │ myName = 'Hello RPG'; → Hello RPG │ +│ 3 │ replPrint(myName); → Hello RPG │ +└──────────────────────────────────────────────────────────────────┘ +``` + +**Design:** +- Each source line is shown with its line number +- Results (from REPLRSLT) appear right-aligned or on the same line, styled + distinctly (e.g. colour-coded by `result_type`) +- `EVALUATION` results: shown in muted colour alongside the line +- `TEST-SUCCESS`: green checkmark ✓ with result text +- `TEST-FAILURE`: red cross ✗ with result text +- Loop results: show loop count (e.g. `→ Loop executed ×5`) +- Compile errors: red banner at the top with error details +- Lines with no results: shown without decoration + +**HTML structure:** + +```html +
+
+ 1 + dcl-s myName char(20); +
+
+ 2 + myName = 'Hello RPG'; + → Hello RPG +
+
+ 3 + replPrint(myName); + → Hello RPG +
+
+``` + +**CSS theming:** Use VS Code CSS variables for theme compatibility: +- `--vscode-editor-foreground` for code text +- `--vscode-testing-iconPassed` for test successes +- `--vscode-testing-iconFailed` for test failures +- `--vscode-descriptionForeground` for evaluation results +- `--vscode-editor-background` for the code background +- Monospace font (`var(--vscode-editor-font-family)`) for code lines + +#### 5c. Error Handling in Rendering + +- **REPLWRPR returns -1:** Show error banner with the error message +- **No results returned:** Show "Snippet compiled and ran successfully (no + output)" message +- **Compilation failure:** Parse the error message and show it prominently. + Consider also fetching from the job log or spool file for more detail. + +### 6. Fixed-Format Ruler Overlay + +When editing RPG cells, overlay column rulers for fixed-format specifications. + +#### Detection Logic + +A line is fixed-format if its first non-blank character (in column 6 position, +or equivalently the first character in the cell since notebooks don't have +sequence numbers) is an uppercase letter that corresponds to an RPG spec type. + +RPGLE-REPL already has this logic in `REPL_INS.codeIsFixedFormat()` and +`REPL_EVAL.codeIsFixedFormat()`: + +> First character is one of: `F`, `I`, `O`, `D`, `C`, `H`, `P` + +For the notebook, detect on the **first character of the line** being one of +these uppercase letters. + +#### Ruler Display + +When any line in the cell starts with a fixed-format character, enable ruler +mode for the cell. The ruler should match the spec type being used. + +**RPG IV Fixed-Format Column Rulers:** + +``` +H-spec: ....+....1....+....2....+....3....+....4....+....5....+....6....+....7.. +F-spec: FFilename++IPEASFRlen+LKlen+AIDevice+.Functions+++++++++++++++++++++++++ +D-spec: DName+++++++++++ETDsFrom+++To/L+++IDc.Functions+++++++++++++++++++++++++ +I-spec: IFilename++SqNORiPos1+NCCPos2+CCPos3+NCC.................................. +C-spec: CL0N01Factor1+++++++Opcode(E)+Factor2+++++++Result++++++++Len++D+HiLoEq.. +O-spec: OFilename++DF..N01N02N03Excnam++++B++A++Sb+Sa+........................... +P-spec: PName+++++++++++..B...................Functions++++++++++++++++++++++++++++ +``` + +#### Implementation + +This should be a **VS Code editor decoration** applied to cells with +`languageId === 'rpgle'`, not part of the cell output. Use: + +```typescript +vscode.window.onDidChangeActiveTextEditor(editor => { + if (isNotebookCellEditor(editor) && editor.document.languageId === 'rpgle') { + applyFixedFormatRulers(editor); + } +}); +``` + +**Approach — Editor Decorations:** + +- Create `DecorationTypes` for ruler lines +- Detect the spec type of each line (first character) +- Apply a decoration at line 0 (or as a sticky header) showing the ruler for + the detected spec type(s) +- Use `vscode.DecorationRenderOptions` with `before` pseudo-element or an + overlay approach +- The ruler text should be in a muted green colour (matching the 5250 REPL + experience) + +**Alternative — Simpler Approach with Status Bar:** + +If editor decorations prove too complex for notebook cells, show the ruler in +the status bar or as a hover tooltip when the cursor is on a fixed-format line. + +**Alternative — Top-of-cell Comment:** + +```rpgle +// ....+....1....+....2....+....3....+....4....+....5....+....6....+....7.. +// CL0N01Factor1+++++++Opcode(E)+Factor2+++++++Result++++++++Len++D+HiLoEq +C EVAL MYVAR = 'HELLO' +``` + +Auto-insert the ruler as a comment at the top of the cell when fixed-format is +detected. This is the simplest approach and requires no decoration API — the +ruler is simply part of the cell content. However, it would need to be stripped +before sending to REPLSRC. + +### 7. Serialisation + +No changes needed to the serialisation format. The existing Jupyter-compatible +format already supports arbitrary language IDs via `metadata.tags[0]`. An RPG +cell serialises as: + +```json +{ + "cell_type": "code", + "metadata": { "tags": ["rpgle"] }, + "source": "dcl-s myName char(20);\nmyName = 'Hello RPG';\nreplPrint(myName);", + "outputs": [...] +} +``` + +### 8. Export (HTML) + +The export function in `export.ts` needs a new case for `rpgle` cells. It +should either: +- Re-execute the cell and render the rich HTML, or +- Use cached outputs if available + +--- + +## Identified Gaps + +### Gap 1: 71-Character Line Limit — RESOLVED + +**Status:** Fixed. `REPLSRC.code` increased to `VARCHAR(200)`. Source PF record +length increased from 128 to 240 (giving 220 usable characters). Template +`t_lineOfCode.code` updated to `char(200)`. + +### ~~Gap 2: REPLWRPR Cannot Read by Arbitrary Session ID~~ — RESOLVED + +**Status:** No longer relevant. The `REPL_EXECUTE` stored procedure handles +everything internally — no need for the extension to write to REPLSRC or call +REPLWRPR. + +### Gap 3: Session ID for Results Correlation — RESOLVED + +**Status:** Fixed. `REPLWRPR` now tags results with `external_session_id` in +all error paths. `REPLEXEC` also handles this correctly. + +### Gap 4: Concurrent Cell Execution + +**Problem:** If two RPG cells are executed simultaneously (or via "Run All"), +they share the same RPGLE-REPL tables. The snippet-based approach helps +isolate source, but the compilation and execution pipeline in RPGLE-REPL uses +fixed names: +- Module: `QTEMP/REPL_MOD` +- Program: `QTEMP/REPL_PGM` +- Source: `QTEMP/QREPL_SRC(REPL_SRC)` + +Since these are in `QTEMP`, they're job-scoped. If the extension uses a +**single SQL job** to run REPLWRPR, sequential execution is fine. But parallel +execution within the same job would collide. + +**Resolution:** Execute RPG cells sequentially (the existing controller loop +already does this — `for...of` with `await`). Document that RPG cells cannot +run in parallel. + +### Gap 5: Control Statements (ctl-opt) + +**Problem:** The 5250 REPL has F16 to set control statements (ctl-opt). These +are stored in REPLSRC with `source_type = 'control'`. How should notebook +cells handle ctl-opt? + +**Recommendations:** + +1. **Inline approach:** Allow users to write `ctl-opt` at the top of their RPG + cell. RPGLE-REPL already handles this naturally — if the first line is a + control statement, it becomes part of the generated source. + +2. **Separate cell approach:** Allow a dedicated "RPG Control" cell type that + sets session-wide ctl-opt. This is more complex and probably unnecessary for + v1. + +3. **Use defaults:** Rely on the organisation/user default ctl-opt configured + in RPGLE-REPL. This works without any changes. + +**Recommendation:** Go with option 1 (inline) for simplicity. Users write +`ctl-opt` at the top of their cell if needed. The REPLWRPR/REPL_GEN pipeline +handles ctl-opt in the mainline source automatically. + +**Actually, wait** — looking more carefully at the code: `REPL_GEN` fetches +control statements separately via `fetchSessionControlStatement()`, which +reads from REPLSRC where `source_type = 'control'`. Mainline code with +`ctl-opt` in it would be treated as regular code and may conflict. + +**Revised recommendation:** Either: +- (a) Write any `ctl-opt` lines to REPLSRC with `source_type = 'control'` + separately, or +- (b) Verify that RPGLE-REPL handles inline `ctl-opt` in mainline code + gracefully (it probably does since it generates valid SQLRPGLE, and the + compiler would accept `ctl-opt` at the top of mainline). Need to test this. + +### Gap 6: Error Diagnostics Quality + +**Problem:** When compilation fails, REPLWRPR writes a generic error message +to stdout/stderr. The actual compiler errors are in the job log or spool file. +The extension has no easy way to retrieve detailed compiler diagnostics. + +**Recommendations:** + +1. **Check spool files:** After a REPLWRPR failure, query + `QSYS2.OUTPUT_QUEUE_ENTRIES` for the latest spool file containing CRTSQLRPGI + output. + +2. **Query job log:** Use `QSYS2.JOBLOG_INFO()` to fetch recent messages. + +3. **Use verbosity level 3:** Set `STD('3')` on REPLWRPR to get full compiler + output on stdout. Parse this output for error messages. + +4. **RPGLE-REPL enhancement:** Add a results table entry for compilation + errors that captures the error text. Currently, `writeError()` only writes + a generic message. + +**Recommendation:** Use verbosity level 2 or 3 and parse the CL command +output. For v1, this gives enough information. Consider RPGLE-REPL +enhancements for v2. + +### Gap 7: Multi-Cell State + +**Problem:** In the 5250 REPL, all code exists in a single session. Variables +declared on one line are available on subsequent lines. In notebooks, users +might expect variables declared in cell 1 to be available in cell 2. + +**Resolution:** Each cell is independent. This matches how SQL notebook cells +work (each is a separate statement). Document this clearly: + +_"Each RPG cell is compiled and run independently. Variables declared in one +cell are not available in other cells. Use a single cell for code that needs +shared state."_ + +This is actually consistent with how RPGLE-REPL works — each "Run" compiles +the entire snippet as one program. + +### Gap 8: Large Result Sets and Loops + +**Problem:** If RPG code has a loop that executes 10,000 times, REPLRSLT could +have 10,000+ rows. The loop_count mechanism helps (it merges them), but +`replPrint()` inside a loop would generate individual rows. + +**Resolution:** Cap the number of results displayed (e.g. first 500 rows) and +show a "truncated" message. This prevents the notebook output from becoming +unwieldy. + +--- + +## Changes Required to RPGLE-REPL + +### Changes Made + +| Change | Status | +|--------|--------| +| `REPLSRC.code` increased from `VARCHAR(71)` to `VARCHAR(200)` | Done | +| `t_lineOfCode.code` increased from `char(71)` to `char(200)` | Done | +| Source PF RCDLEN increased from 128 to 240 in `REPL_GEN` | Done | +| `REPLWRPR` now tags `external_session_id` on all error paths | Done | +| New `REPLEXEC.SQLRPGLE` stored procedure program | Done | +| New `REPLEXEC.SQL` DDL for procedure registration | Done | +| Build rules updated for REPLEXEC program | Done | + +### Remaining Considerations + +| Item | Notes | +|------|-------| +| The 5250 REPL display remains at 71 chars | By design — terminal width constraint | +| REPLEXEC CLOB line-splitting uses EBCDIC LF (x'25') | JDBC converts Unicode LF to EBCDIC LF | +| REPLRSLT rows accumulate across executions | Harmless; add periodic cleanup later if needed | + +--- + +## Implementation Tasks (vscode-db2i) + +### Task 1: Configuration Setting + +**Files:** `package.json`, `src/configuration.ts` + +- Add `vscode-db2i.rpgleRepl.library` setting to `package.json` contributes +- Add helper method to `Configuration` class +- Add validation on connect + +### Task 2: Register RPG Language + +**Files:** `src/notebooks/Controller.ts`, `src/notebooks/contributes.json` + +- Add `rpgle` to `supportedLanguages` array +- Add Ctrl+R keybinding for `rpgle` cells +- Language registration in notebook contributes + +### Task 3: RPG Execution Logic + +**Files:** `src/notebooks/Controller.ts` (or new file +`src/notebooks/logic/rpgle.ts`) + +- Session ID generation (`NB-` + UUID, max 28 chars) +- Single SQL CALL to `{lib}.REPL_EXECUTE(:source, :sessionId)` +- Parse returned result set into source-line-to-result map +- Handle errors (procedure may return error rows with `result_type` info) +- No direct REPLSRC/REPLRSLT access needed from the extension + +### Task 4: Result Rendering + +**Files:** new `src/notebooks/logic/rpgleResults.ts` + +- HTML generation for RPG results display +- CSS theming with VS Code variables +- Error state rendering +- Test result colour coding + +### Task 5: Fixed-Format Ruler + +**Files:** new `src/notebooks/logic/rpgleRuler.ts` + +- Fixed-format detection +- Ruler decoration types +- Editor decoration application +- Spec-type to ruler mapping + +### Task 6: Export Support + +**Files:** `src/notebooks/logic/export.ts` + +- Add `rpgle` case to export function +- Re-execute or use cached output + +### Task 7: Serialisation Verification + +**Files:** `src/notebooks/IBMiSerializer.ts` + +- Verify `rpgle` language round-trips correctly (it should work as-is) +- Add tests if not already covered + +--- + +## Is This the Right Approach? + +**Yes.** The `REPL_EXECUTE` stored procedure provides a clean, single-call +interface between the extension and RPGLE-REPL. + +**Strengths:** +- Single SQL round-trip — no multi-step INSERT/CALL/SELECT/DELETE +- All internal state management (REPLSRC, REPLRSLT, REPLVARS) is encapsulated +- Error paths are handled — errors are always tagged and returned +- Uses RPGLE-REPL service programs directly (same pipeline as 5250 REPL) +- REPLWRPR and REPLPRTR remain untouched for 5250 and CI/CD use +- Extension code is simple — just CALL and render + +**Alternatives considered and rejected:** + +1. **Multi-step via REPLWRPR:** INSERT into REPLSRC, CALL REPLWRPR via CL, + SELECT from REPLRSLT, DELETE cleanup. Works but requires 4+ round-trips + and direct table manipulation from the extension. + +2. **Direct service program calls:** Would require binding to RPGLE-REPL + service programs from the server component. Too tightly coupled and requires + a Java/native bridge. + +3. **SSH shell execution of `replwrpr.sh`:** Slower (shell startup, PASE + overhead) and harder to correlate results. + +--- + +## Implementation Order + +``` +Phase 2a: Foundation + ├── Task 1: Configuration setting + ├── Task 2: Language registration + └── Task 7: Serialisation verification + +Phase 2b: Core Execution + ├── Task 3: RPG execution logic + └── Task 4: Result rendering + +Phase 2c: Polish + ├── Task 5: Fixed-format ruler + └── Task 6: Export support +``` + +--- + +## Open Questions + +1. **Should RPG cells share state across cells?** Current plan: no, each cell + is independent. This is simpler and avoids complex state management. Users + can write all related code in one cell. + +2. **Should we support `/COPY` and `/INCLUDE` in notebook cells?** RPGLE-REPL + supports this natively. It should "just work" since the generated source + goes through the normal RPGLE-REPL pipeline. + +3. **Should the ruler be auto-inserted or interactive?** Auto-detecting + fixed-format and showing rulers is the plan. Should users be able to + toggle rulers on/off? + +4. **What happens to REPLSRC snippets if the extension crashes mid-execution?** + Orphaned snippet rows would accumulate. Consider a periodic cleanup or + unique prefix-based cleanup on startup. + +5. **Should we support the `replPrint()` and `replEquals()` helpers explicitly + in documentation?** These are the primary ways to output results. The + auto-evaluation of assignments is a bonus. diff --git a/package-lock.json b/package-lock.json index fe02f579..896acab0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2255,6 +2255,62 @@ "darwin" ] }, + "node_modules/@lancedb/vectordb-darwin-x64": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.4.20.tgz", + "integrity": "sha512-GSYsXE20RIehDu30FjREhJdEzhnwOTV7ZsrSXagStzLY1gr7pyd7sfqxmmUtdD09di7LnQoiM71AOpPTa01YwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lancedb/vectordb-linux-arm64-gnu": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-arm64-gnu/-/vectordb-linux-arm64-gnu-0.4.20.tgz", + "integrity": "sha512-FpNOjOsz3nJVm6EBGyNgbOW2aFhsWZ/igeY45Z8hbZaaK2YBwrg/DASoNlUzgv6IR8cUaGJ2irNVJfsKR2cG6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lancedb/vectordb-linux-x64-gnu": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-x64-gnu/-/vectordb-linux-x64-gnu-0.4.20.tgz", + "integrity": "sha512-pOqWjrRZQSrLTlQPkjidRii7NZDw8Xu9pN6ouVu2JAK8n81FXaPtFCyAI+Y3v9GpnYDN0rvD4eQ36aHAVPsa2g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lancedb/vectordb-win32-x64-msvc": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.4.20.tgz", + "integrity": "sha512-5J5SsYSJ7jRCmU/sgwVHdrGz43B/7R2T9OEoFTKyVAtqTZdu75rkytXyn9SyEayXVhlUOaw76N0ASm0hAoDS/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@mozilla/readability": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.5.0.tgz", diff --git a/package.json b/package.json index e73ef0f2..9898d257 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,17 @@ } } }, + { + "id": "vscode-db2i.rpgleRepl", + "title": "RPGLE REPL", + "properties": { + "vscode-db2i.rpgleRepl.library": { + "type": "string", + "markdownDescription": "Library containing RPGLE-REPL objects. Leave empty to disable RPG notebook cells. Install RPGLE-REPL from [rpgle-repl](https://github.com/tom-writes-code/rpgle-repl).", + "default": "" + } + } + }, { "id": "vscode-db2i.schemaBrowser", "title": "Schema Browser", @@ -1660,7 +1671,7 @@ "command": "notebook.cell.execute", "key": "ctrl+r", "mac": "cmd+r", - "when": "editorLangId == sql && resourceExtname == .inb" + "when": "(editorLangId == sql || editorLangId == rpgle) && resourceExtname == .inb" } ], "colors": [ diff --git a/rpgle-repl.code-workspace b/rpgle-repl.code-workspace new file mode 100644 index 00000000..59b7ca36 --- /dev/null +++ b/rpgle-repl.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "../rpgle-repl" + }, + { + "path": "." + } + ], + "settings": { + "iis.activeFolder": "rpgle-repl" + } +} \ No newline at end of file diff --git a/src/contributes.json b/src/contributes.json index 36d1702a..75622811 100644 --- a/src/contributes.json +++ b/src/contributes.json @@ -85,6 +85,17 @@ "default": true } } + }, + { + "id": "vscode-db2i.rpgleRepl", + "title": "RPGLE REPL", + "properties": { + "vscode-db2i.rpgleRepl.library": { + "type": "string", + "markdownDescription": "Library containing RPGLE-REPL objects. Leave empty to disable RPG notebook cells. Install RPGLE-REPL from [rpgle-repl](https://github.com/tom-writes-code/rpgle-repl).", + "default": "" + } + } } ], "viewsContainers": { diff --git a/src/notebooks/Controller.ts b/src/notebooks/Controller.ts index 7bc3c1ce..baef929c 100644 --- a/src/notebooks/Controller.ts +++ b/src/notebooks/Controller.ts @@ -1,19 +1,26 @@ -import * as vscode from 'vscode'; +import * as vscode from "vscode"; +import * as crypto from "crypto"; //@ts-ignore -import * as mdTable from 'json-to-markdown-table'; - -import { getInstance } from '../base'; -import { JobManager } from '../config'; -import { ChartJsType, chartJsTypes, generateChartHTMLCell } from './logic/chartJs'; -import { ChartDetail, generateChart } from './logic/chart'; -import { getStatementDetail } from './logic/statement'; +import * as mdTable from "json-to-markdown-table"; + +import { getInstance } from "../base"; +import { JobManager } from "../config"; +import { + ChartJsType, + chartJsTypes, + generateChartHTMLCell, +} from "./logic/chartJs"; +import { ChartDetail, generateChart } from "./logic/chart"; +import { getStatementDetail } from "./logic/statement"; +import Configuration from "../configuration"; +import { renderRpgleResults, RpgleResultRow } from "./logic/rpgleRender"; export class IBMiController { readonly controllerId = `db2i-notebook-controller-id`; readonly notebookType = `db2i-notebook`; readonly label = `IBM i Notebook`; - readonly supportedLanguages = [`sql`, `cl`, `shellscript`]; + readonly supportedLanguages = [`sql`, `cl`, `shellscript`, `rpgle`]; private readonly _controller: vscode.NotebookController; private globalCancel = false; @@ -23,7 +30,7 @@ export class IBMiController { this._controller = vscode.notebooks.createNotebookController( this.controllerId, this.notebookType, - this.label + this.label, ); this._controller.supportedLanguages = this.supportedLanguages; @@ -35,12 +42,10 @@ export class IBMiController { this._controller.dispose(); } - - private async _execute( cells: vscode.NotebookCell[], _notebook: vscode.NotebookDocument, - _controller: vscode.NotebookController + _controller: vscode.NotebookController, ) { this.globalCancel = false; @@ -74,8 +79,9 @@ export class IBMiController { case `sql`: try { if (selected) { - const eol = cell.document.eol === vscode.EndOfLine.CRLF ? `\r\n` : `\n`; - + const eol = + cell.document.eol === vscode.EndOfLine.CRLF ? `\r\n` : `\n`; + let content = cell.document.getText().trim(); let chartDetail: ChartDetail | undefined; @@ -86,19 +92,30 @@ export class IBMiController { const results = await query.execute(1000); const table = results.data; - const columnNames = results.metadata.columns.map(c => c.name); - - if (table === undefined && results.success && !results.has_results) { - items.push(vscode.NotebookCellOutputItem.text(`Statement executed successfully. ${results.update_count ? `${results.update_count} rows affected.` : ``}`, `text/markdown`)); + const columnNames = results.metadata.columns.map((c) => c.name); + + if ( + table === undefined && + results.success && + !results.has_results + ) { + items.push( + vscode.NotebookCellOutputItem.text( + `Statement executed successfully. ${results.update_count ? `${results.update_count} rows affected.` : ``}`, + `text/markdown`, + ), + ); break; } if (table.length > 0) { // Add `-` for blanks. - table.forEach(row => { - columnNames.forEach(key => { + table.forEach((row) => { + columnNames.forEach((key) => { //@ts-ignore - if (row[key] === null) { row[key] = `-`; } + if (row[key] === null) { + row[key] = `-`; + } }); }); @@ -106,23 +123,46 @@ export class IBMiController { if (chartDetail.type) { if (chartJsTypes.includes(chartDetail.type as ChartJsType)) { - const possibleChart = generateChart(execution.executionOrder, chartDetail, columnNames, table, generateChartHTMLCell); + const possibleChart = generateChart( + execution.executionOrder, + chartDetail, + columnNames, + table, + generateChartHTMLCell, + ); if (possibleChart) { - items.push(vscode.NotebookCellOutputItem.text(possibleChart, `text/html`)); + items.push( + vscode.NotebookCellOutputItem.text( + possibleChart, + `text/html`, + ), + ); fallbackToTable = false; } } } if (fallbackToTable) { - items.push(vscode.NotebookCellOutputItem.text(mdTable(table, columnNames), `text/markdown`)); + items.push( + vscode.NotebookCellOutputItem.text( + mdTable(table, columnNames), + `text/markdown`, + ), + ); } } else { - items.push(vscode.NotebookCellOutputItem.stderr(`No rows returned from statement.`)); + items.push( + vscode.NotebookCellOutputItem.stderr( + `No rows returned from statement.`, + ), + ); } - } else { - items.push(vscode.NotebookCellOutputItem.stderr(`No job selected in SQL Job Manager.`)); + items.push( + vscode.NotebookCellOutputItem.stderr( + `No job selected in SQL Job Manager.`, + ), + ); } } catch (e) { items.push(vscode.NotebookCellOutputItem.stderr(e.message)); @@ -133,28 +173,32 @@ export class IBMiController { try { const command = await connection.runCommand({ command: cell.document.getText(), - environment: `ile` + environment: `ile`, }); if (command.stdout) { - items.push(vscode.NotebookCellOutputItem.text([ - `\`\`\``, - command.stdout, - `\`\`\`` - ].join(`\n`), `text/markdown`)); + items.push( + vscode.NotebookCellOutputItem.text( + [`\`\`\``, command.stdout, `\`\`\``].join(`\n`), + `text/markdown`, + ), + ); } if (command.stderr) { - items.push(vscode.NotebookCellOutputItem.text([ - `\`\`\``, - command.stderr, - `\`\`\`` - ].join(`\n`), `text/markdown`)); + items.push( + vscode.NotebookCellOutputItem.text( + [`\`\`\``, command.stderr, `\`\`\``].join(`\n`), + `text/markdown`, + ), + ); } } catch (e) { items.push( - vscode.NotebookCellOutputItem.stderr(`Failed to run command. Are you connected?`), - vscode.NotebookCellOutputItem.stderr(e.message) + vscode.NotebookCellOutputItem.stderr( + `Failed to run command. Are you connected?`, + ), + vscode.NotebookCellOutputItem.stderr(e.message), ); } break; @@ -163,46 +207,107 @@ export class IBMiController { try { const command = await connection.runCommand({ command: cell.document.getText(), - environment: `pase` + environment: `pase`, }); if (command.stdout) { - items.push(vscode.NotebookCellOutputItem.text([ - `\`\`\``, - command.stdout, - `\`\`\`` - ].join(`\n`), `text/markdown`)); + items.push( + vscode.NotebookCellOutputItem.text( + [`\`\`\``, command.stdout, `\`\`\``].join(`\n`), + `text/markdown`, + ), + ); } if (command.stderr) { - items.push(vscode.NotebookCellOutputItem.text([ - `\`\`\``, - command.stderr, - `\`\`\`` - ].join(`\n`), `text/markdown`)); + items.push( + vscode.NotebookCellOutputItem.text( + [`\`\`\``, command.stderr, `\`\`\``].join(`\n`), + `text/markdown`, + ), + ); } } catch (e) { items.push( - vscode.NotebookCellOutputItem.stderr(`Failed to runCommand. Are you connected?`), + vscode.NotebookCellOutputItem.stderr( + `Failed to runCommand. Are you connected?`, + ), //@ts-ignore - vscode.NotebookCellOutputItem.stderr(e.message) + vscode.NotebookCellOutputItem.stderr(e.message), ); } break; - } + case `rpgle`: + try { + const lib = Configuration.get(`rpgleRepl.library`); + if (!lib) { + items.push( + vscode.NotebookCellOutputItem.stderr( + `RPG notebook cells require RPGLE-REPL. Set the \`vscode-db2i.rpgleRepl.library\` setting to the library containing RPGLE-REPL, or visit https://github.com/tom-writes-code/rpgle-repl for installation instructions.`, + ), + ); + break; + } + + if (selected) { + const sessionId = `NB-${crypto.randomUUID().substring(0, 22)}`; + const cellSource = cell.document.getText(); + + // Ensure the RPGLE-REPL library is in the job's library list + try { + await selected.job.execute( + `CALL QSYS2.QCMDEXC('ADDLIBLE LIB(${lib}) POSITION(*LAST)')`, + ); + } catch { + // CPF2103: library already in list — safe to ignore + } + + const query = selected.job.query( + `CALL ${lib}.REPL_EXECUTE(?, ?)`, + { parameters: [cellSource, sessionId] }, + ); + const results = await query.execute(); + + if (results.success && results.data && results.data.length > 0) { + const html = renderRpgleResults(cellSource, results.data); + items.push( + vscode.NotebookCellOutputItem.text(html, `text/html`), + ); + } else if (results.success) { + items.push( + vscode.NotebookCellOutputItem.text( + `Snippet compiled and ran successfully (no output).`, + `text/markdown`, + ), + ); + } else { + items.push( + vscode.NotebookCellOutputItem.stderr(`RPG execution failed.`), + ); + } + } else { + items.push( + vscode.NotebookCellOutputItem.stderr( + `No job selected in SQL Job Manager.`, + ), + ); + } + } catch (e) { + items.push(vscode.NotebookCellOutputItem.stderr(e.message)); + } + break; + } } else { items.push( - vscode.NotebookCellOutputItem.stderr(`Failed to execute. Are you connected?`) - ) + vscode.NotebookCellOutputItem.stderr( + `Failed to execute. Are you connected?`, + ), + ); } - execution.replaceOutput([ - new vscode.NotebookCellOutput(items) - ]); + execution.replaceOutput([new vscode.NotebookCellOutput(items)]); execution.end(true, Date.now()); } - - } \ No newline at end of file diff --git a/src/notebooks/contributes.json b/src/notebooks/contributes.json index 9932f2b4..cc3741b7 100644 --- a/src/notebooks/contributes.json +++ b/src/notebooks/contributes.json @@ -17,7 +17,7 @@ "command": "notebook.cell.execute", "key": "ctrl+r", "mac": "cmd+r", - "when": "editorLangId == sql && resourceExtname == .inb" + "when": "(editorLangId == sql || editorLangId == rpgle) && resourceExtname == .inb" } ], "commands": [ diff --git a/src/notebooks/logic/rpgleRender.test.ts b/src/notebooks/logic/rpgleRender.test.ts new file mode 100644 index 00000000..46ec8ac2 --- /dev/null +++ b/src/notebooks/logic/rpgleRender.test.ts @@ -0,0 +1,120 @@ +import { expect, test } from 'vitest' +import { renderRpgleResults, RpgleResultRow } from './rpgleRender' + +test('Renders source lines with no results', () => { + const html = renderRpgleResults('dcl-s x char(10);', []); + + expect(html).toContain('class="rpgle-results"'); + expect(html).toContain('class="line-num">1'); + expect(html).toContain('dcl-s x char(10);'); + expect(html).not.toContain('has-result'); +}); + +test('Renders evaluation result on correct line', () => { + const source = `dcl-s x char(4);\nx = 'hi';`; + const data: RpgleResultRow[] = [ + { LINE_NUMBER: 2, RESULT_NUMBER: 1, RESULT_DESCRIPTION: 'X = hi', LOOP_COUNT: 1, RESULT_TYPE: 'EVALUATION' }, + ]; + + const html = renderRpgleResults(source, data); + + expect(html).toContain('has-result'); + expect(html).toContain('class="result evaluation"'); + expect(html).toContain('→ X = hi'); +}); + +test('Renders test success with checkmark', () => { + const data: RpgleResultRow[] = [ + { LINE_NUMBER: 1, RESULT_NUMBER: 1, RESULT_DESCRIPTION: 'two plus two', LOOP_COUNT: 1, RESULT_TYPE: 'TEST-SUCCESS' }, + ]; + + const html = renderRpgleResults('replEquals(...);', data); + + expect(html).toContain('class="result test-success"'); + expect(html).toContain('✓'); +}); + +test('Renders test failure with cross', () => { + const data: RpgleResultRow[] = [ + { LINE_NUMBER: 1, RESULT_NUMBER: 1, RESULT_DESCRIPTION: 'expected 4 got 5', LOOP_COUNT: 1, RESULT_TYPE: 'TEST-FAILURE' }, + ]; + + const html = renderRpgleResults('replEquals(...);', data); + + expect(html).toContain('class="result test-failure"'); + expect(html).toContain('✗'); +}); + +test('Shows loop count when greater than 1', () => { + const data: RpgleResultRow[] = [ + { LINE_NUMBER: 1, RESULT_NUMBER: 1, RESULT_DESCRIPTION: 'X = hi', LOOP_COUNT: 5, RESULT_TYPE: 'EVALUATION' }, + ]; + + const html = renderRpgleResults('x = something;', data); + + expect(html).toContain('class="loop-count"'); + expect(html).toContain('(×5)'); +}); + +test('Hides loop count element when 1', () => { + const data: RpgleResultRow[] = [ + { LINE_NUMBER: 1, RESULT_NUMBER: 1, RESULT_DESCRIPTION: 'X = hi', LOOP_COUNT: 1, RESULT_TYPE: 'EVALUATION' }, + ]; + + const html = renderRpgleResults('x = something;', data); + + // The CSS class definition is always present, but no element with that class should be rendered + expect(html).not.toContain('class="loop-count"'); + expect(html).not.toContain('(×1)'); +}); + +test('Escapes HTML in source and results', () => { + const data: RpgleResultRow[] = [ + { LINE_NUMBER: 1, RESULT_NUMBER: 1, RESULT_DESCRIPTION: '', LOOP_COUNT: 1, RESULT_TYPE: 'EVALUATION' }, + ]; + + const html = renderRpgleResults('if x < 10;', data); + + expect(html).toContain('<'); + expect(html).not.toContain('