Skip to content

Commit d7815fb

Browse files
committed
Add UDQ/ACTIONX support: recognition, highlighting, hover, completion, alignment
Brings the UDQ expression sub-language and ACTIONX blocks into the editor: - Recognise UDQ/ACTIONX: validate that every UDQ statement begins with a control word (ASSIGN/DEFINE/UNITS/UPDATE), and flag an ACTIONX action block that is never closed by a matching ENDACTIO. - Syntax highlighting (tmLanguage) for UDQ control words, UDQ functions (name-followed-by-paren), and comparison/logical operators. - Hover and completion (new udq.ts vocabulary module): hover a UDQ control word or function for its meaning/signature; completion offers control words at statement start and UDQ functions inside expressions. - Column alignment for UDQ expression blocks: right-align the expression column and control words, parse division-safe, and preserve interspersed comments and blank lines inside a UDQ table without breaking the alignment groups. Refs #18 Squashed from 5 commits cherry-picked from feature/udq-expression-align.
1 parent 2aab7a8 commit d7815fb

10 files changed

Lines changed: 752 additions & 2 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ Marketplace listing: <https://marketplace.visualstudio.com/items?itemName=magne-
7777
section keyword to the next) or individual keywords in the gutter.
7878
- **Align Record Columns** — tidy up record blocks so every column lines up;
7979
handles comment lines inside the group and aligns to heading comments above
80-
the group.
80+
the group. `UDQ` expression blocks get a dedicated three-column layout —
81+
the control word (`DEFINE`/`ASSIGN`/`UNITS`/`UPDATE`) right-aligned, the
82+
variable name left-aligned, and the expression right-aligned so every
83+
statement's terminating `/` lines up (a `/` used for division inside the
84+
expression is not mistaken for the terminator).
8185
- **Add Column Headers** — insert a `--` heading comment with parameter names
8286
from the reference manual and align the record group to those positions
8387
(idempotent). For multi-record keywords the names come from the record

vscode-extension/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ Provides syntax highlighting for OPM Flow simulation deck files with support for
2222
- **Strings**: Text in single quotes
2323
- **Template variables**: `<NAME>` placeholders used in macro/ERT workflows
2424
- **END keyword**: Specially highlighted file terminator
25+
- **UDQ / ACTIONX constructs**: UDQ control words (`DEFINE`, `ASSIGN`, `UNITS`,
26+
`UPDATE`), UDQ functions (`SORTA`, `SUM`, `ABS`, …), the comparison and
27+
logical operators used in expressions (`>=`, `<=`, `==`, `AND`, `OR`), and the
28+
`ACTIONX` / `ENDACTIO` action-block delimiters
2529

2630
### Keyword Autocompletion
2731

@@ -56,6 +60,21 @@ start typing an uppercase letter (e.g. `O` for `OPEN`) or press
5660
`Ctrl+Space` (`Cmd+Space` on macOS) to open the suggestions manually. Selecting
5761
an option inserts the value quoted, e.g. `'OPEN'`.
5862

63+
### UDQ and ACTIONX Support
64+
65+
The user-defined-quantity sub-language (`UDQ` blocks) and `ACTIONX` action blocks
66+
are recognised so the editor can assist with their distinct syntax:
67+
68+
- **Completion** — inside a `UDQ` block, the start of a statement offers the
69+
control words (`ASSIGN`, `DEFINE`, `UNITS`, `UPDATE`); inside a UDQ formula or
70+
an `ACTIONX` condition, the UDQ functions (`SORTA`, `SUM`, `ABS`, …) are
71+
offered and inserted with parentheses ready for the argument.
72+
- **Hover** — hovering a UDQ control word shows what it does, and hovering a UDQ
73+
function shows its signature and description.
74+
- **Diagnostics** — a `UDQ` statement that doesn't start with a control word, and
75+
an `ACTIONX` block left unclosed by `ENDACTIO`, are flagged (see
76+
[Diagnostics](#diagnostics)).
77+
5978
### Hover Tooltips
6079

6180
Hover over any keyword to see a quick tooltip with:
@@ -119,6 +138,11 @@ Squiggles in the editor catch the most common deck-shape mistakes:
119138
header (an include fragment, not a complete deck).
120139
- **Mutually exclusive keywords** — two keywords that `opm-common` marks as
121140
`prohibits` partners both appearing in the same deck.
141+
- **UDQ statement without a control word** — a statement inside a `UDQ` block
142+
that does not begin with `ASSIGN`, `DEFINE`, `UNITS`, or `UPDATE`.
143+
Continuation lines of a statement whose `/` is deferred are not flagged.
144+
- **Unclosed `ACTIONX` block** — an `ACTIONX` action block with no matching
145+
`ENDACTIO` before the end of the deck.
122146

123147
Keywords whose record bodies don't fit the generic model can be silenced
124148
wholesale via the `opm-flow.diagnostics.excludedKeywords` setting — see
@@ -355,6 +379,15 @@ The language is registered as `opm-flow`.
355379

356380
### Unreleased
357381

382+
- **UDQ and ACTIONX support** — the `UDQ` expression sub-language and `ACTIONX`
383+
action blocks are now recognised. Syntax highlighting scopes UDQ control
384+
words, UDQ functions, and expression operators, plus the `ACTIONX` /
385+
`ENDACTIO` block delimiters. Hover and completion cover UDQ control words and
386+
functions, and two new diagnostics flag a UDQ statement that doesn't start
387+
with a control word and an `ACTIONX` block left unclosed by `ENDACTIO`.
388+
Column alignment also gives `UDQ` expression blocks a dedicated three-column
389+
layout (control word, variable, expression) that treats a `/` used for
390+
division as part of the expression rather than the record terminator.
358391
- **Boilerplate keyword completion** — accepting a keyword completion now inserts
359392
a sample data record as a tab-navigable snippet (documented defaults or
360393
type-appropriate dummy values, terminated to match the keyword's shape) instead

vscode-extension/src/analysis.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,31 @@ const index: Record<string, AnalysisEntry> = {
4343
BARE: {
4444
name: 'BARE',
4545
},
46+
UDQ: {
47+
name: 'UDQ',
48+
sections: ['SCHEDULE'],
49+
size_kind: 'list',
50+
},
51+
ACTIONX: {
52+
name: 'ACTIONX',
53+
sections: ['SCHEDULE'],
54+
size_kind: 'list',
55+
},
56+
ENDACTIO: {
57+
name: 'ENDACTIO',
58+
sections: ['SCHEDULE'],
59+
size_kind: 'none',
60+
},
61+
WELOPEN: {
62+
name: 'WELOPEN',
63+
sections: ['SCHEDULE'],
64+
size_kind: 'list',
65+
},
66+
TSTEP: {
67+
name: 'TSTEP',
68+
sections: ['SCHEDULE'],
69+
size_kind: 'array',
70+
},
4671
};
4772

4873
// ---------------------------------------------------------------------------
@@ -1776,3 +1801,107 @@ describe('computeDiagnostics — quick-fix codes', () => {
17761801
expect(d!.suggestion).toBeUndefined();
17771802
});
17781803
});
1804+
1805+
// ---------------------------------------------------------------------------
1806+
// UDQ control-word validation
1807+
// ---------------------------------------------------------------------------
1808+
1809+
describe('computeDiagnostics — UDQ control words', () => {
1810+
it('accepts a UDQ block whose statements use control words', () => {
1811+
const lines = [
1812+
'SCHEDULE',
1813+
'UDQ',
1814+
"DEFINE WUPR1 1/(WWCT 'OP*') /",
1815+
'DEFINE WUPR3 SORTA(WUPR1) /',
1816+
'ASSIGN WU2 3.0 /',
1817+
'UNITS WUPR1 BARSA /',
1818+
'/',
1819+
];
1820+
expect(computeDiagnostics(lines, index)).toEqual([]);
1821+
});
1822+
1823+
it('flags a UDQ statement that does not start with a control word', () => {
1824+
const lines = [
1825+
'SCHEDULE',
1826+
'UDQ',
1827+
'DEFIN WUPR1 1 /',
1828+
'/',
1829+
];
1830+
const diags = computeDiagnostics(lines, index);
1831+
expect(diags).toHaveLength(1);
1832+
expect(diags[0].line).toBe(2);
1833+
expect(diags[0].message).toMatch(/expected a control word/);
1834+
expect(diags[0].message).toContain('DEFIN');
1835+
});
1836+
1837+
it('does not flag a continuation line of an open UDQ statement', () => {
1838+
// The '/' is deferred to the next line, so the second line continues the
1839+
// statement and must not be checked for a leading control word.
1840+
const lines = [
1841+
'SCHEDULE',
1842+
'UDQ',
1843+
'DEFINE WUPR1',
1844+
" 1/(WWCT 'OP*') /",
1845+
'/',
1846+
];
1847+
expect(computeDiagnostics(lines, index)).toEqual([]);
1848+
});
1849+
});
1850+
1851+
// ---------------------------------------------------------------------------
1852+
// ACTIONX ... ENDACTIO block
1853+
// ---------------------------------------------------------------------------
1854+
1855+
describe('computeDiagnostics — ACTIONX block', () => {
1856+
it('accepts a complete ACTIONX ... ENDACTIO block', () => {
1857+
const lines = [
1858+
'SCHEDULE',
1859+
'ACTIONX',
1860+
'ACT01 10 /',
1861+
'FMWPR >= 4 AND /',
1862+
"WUPR3 'OP*' = 1 /",
1863+
'/',
1864+
'WELOPEN',
1865+
" '?' SHUT 0 0 0 2* /",
1866+
'/',
1867+
'ENDACTIO',
1868+
];
1869+
expect(computeDiagnostics(lines, index)).toEqual([]);
1870+
});
1871+
1872+
it('flags an ACTIONX block that is never closed by ENDACTIO', () => {
1873+
const lines = [
1874+
'SCHEDULE',
1875+
'ACTIONX',
1876+
'ACT01 10 /',
1877+
'FMWPR >= 4 /',
1878+
'/',
1879+
'WELOPEN',
1880+
" '?' SHUT 0 0 0 2* /",
1881+
'/',
1882+
];
1883+
const diags = computeDiagnostics(lines, index);
1884+
expect(diags).toHaveLength(1);
1885+
expect(diags[0].line).toBe(1);
1886+
expect(diags[0].message).toMatch(/ENDACTIO/);
1887+
});
1888+
1889+
it('does not flag when a later ACTIONX block is properly closed', () => {
1890+
const lines = [
1891+
'SCHEDULE',
1892+
'ACTIONX',
1893+
'ACT01 10 /',
1894+
'FMWPR >= 4 /',
1895+
'/',
1896+
'ENDACTIO',
1897+
'TSTEP',
1898+
' 10 10 /',
1899+
'ACTIONX',
1900+
'ACT02 10 /',
1901+
'FMWPR >= 5 /',
1902+
'/',
1903+
'ENDACTIO',
1904+
];
1905+
expect(computeDiagnostics(lines, index)).toEqual([]);
1906+
});
1907+
});

vscode-extension/src/analysis.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,14 @@ const TEMPLATE_SUFFIX_RE = /^[A-Z0-9]+$/;
231231
*/
232232
const UDQ_NAME_RE = /^[ABCFGRSW]U[A-Z0-9_]+$/;
233233

234+
/**
235+
* Control words that introduce a statement inside a `UDQ` block. Every record
236+
* in a UDQ block must begin with one of these (see the OPM Flow manual `UDQ`
237+
* keyword): `ASSIGN` a constant, `DEFINE` a formula, set the display `UNITS`,
238+
* or `UPDATE` the evaluation state.
239+
*/
240+
const UDQ_CONTROL_WORDS = new Set(['ASSIGN', 'DEFINE', 'UNITS', 'UPDATE']);
241+
234242
/**
235243
* Region summary vector qualified by a named FIP region set, e.g. ``ROIP_ABC``
236244
* (= base vector ``ROIP`` over region set ``ABC``) or ``RPR__ABC``. The base is
@@ -475,6 +483,13 @@ export function computeDiagnostics(
475483
// deck, the rest pulled in via INCLUDE), so once we've seen one we can no
476484
// longer trust `currentSection` and must suppress the wrong-section check.
477485
let includeSinceSection = false;
486+
// Tracks an open `ACTIONX` block. ACTIONX opens a block of nested SCHEDULE
487+
// keywords (the action) that must be closed by an `ENDACTIO`; the active
488+
// keyword moves on to those nested keywords, so this is tracked separately
489+
// and evaluated at end-of-deck to flag a block that is never closed.
490+
let actionxOpenLine = -1;
491+
let actionxStart = 0;
492+
let actionxEnd = 0;
478493
// First occurrence of each recognised keyword (by canonical entry name),
479494
// collected during the walk and evaluated once at the end for the
480495
// document-wide requires/prohibits constraints.
@@ -718,6 +733,18 @@ export function computeDiagnostics(
718733
continue;
719734
}
720735

736+
// ACTIONX ... ENDACTIO block tracking. ACTIONX opens an action block
737+
// that must be closed by ENDACTIO. The intervening (nested) keywords
738+
// become the active keyword in turn, so the open state is tracked on
739+
// the side and reported at end-of-deck if never closed.
740+
if (activeKw.name === 'ACTIONX') {
741+
actionxOpenLine = i;
742+
actionxStart = activeKwIndent;
743+
actionxEnd = activeKwIndent + kw.length;
744+
} else if (activeKw.name === 'ENDACTIO') {
745+
actionxOpenLine = -1;
746+
}
747+
721748
// Record the first occurrence of this keyword for the document-wide
722749
// requires/prohibits checks. Keyed by the canonical entry name so a
723750
// templated deck token (FTPRSEA) maps to its base (FTPR); the range
@@ -766,6 +793,23 @@ export function computeDiagnostics(
766793
const tokens = tokenizeLine(text);
767794
if (tokens.length === 0) continue;
768795

796+
// UDQ body statements must begin with a control word
797+
// (ASSIGN/DEFINE/UNITS/UPDATE). Check only the first line of a statement —
798+
// `openRecordLine < 0` means no earlier statement is still awaiting its
799+
// '/', so this line starts a new statement rather than continuing one.
800+
if (activeKw.name === 'UDQ' && openRecordLine < 0) {
801+
const head = tokens[0].text.toUpperCase();
802+
if (!UDQ_CONTROL_WORDS.has(head)) {
803+
out.push({
804+
line: i,
805+
startChar: tokens[0].start,
806+
endChar: tokens[0].end,
807+
message:
808+
`UDQ: expected a control word (ASSIGN, DEFINE, UNITS or UPDATE) but found '${tokens[0].text}'.`,
809+
});
810+
}
811+
}
812+
769813
const lastTok = tokens[tokens.length - 1];
770814
const hasTerm = lineHasRecordTerminator(text, lastTok.end);
771815

@@ -857,6 +901,16 @@ export function computeDiagnostics(
857901

858902
closeKw();
859903

904+
// An ACTIONX block left open at end of deck has no matching ENDACTIO.
905+
if (actionxOpenLine >= 0) {
906+
out.push({
907+
line: actionxOpenLine,
908+
startChar: actionxStart,
909+
endChar: actionxEnd,
910+
message: `ACTIONX: action block is not closed; a matching ENDACTIO is required.`,
911+
});
912+
}
913+
860914
// --- Cross-keyword constraints (requires / prohibits) -------------------
861915
// Evaluated document-wide once all keyword occurrences are known.
862916
const reportedProhibitPairs = new Set<string>();

0 commit comments

Comments
 (0)