Skip to content

Commit ad63396

Browse files
committed
Honor column heading above a group when aligning records
Align Record Columns now keeps a table lined up under a heading produced by Add Column Headers: when the line directly above a record group is a `--` comment with exactly one word per column, the records are aligned to it and the heading is re-synced via the same path Add Column Headers uses, so the two commands are mutually idempotent. Other comment lines never interfere: a descriptive comment (any other word count, or separated by a blank line) is never treated as a heading, and comments interspersed within the group are left untouched. The new matchHeadingForGroup helper is the exact-one-word-per-column gate.
1 parent 98f9809 commit ad63396

4 files changed

Lines changed: 90 additions & 8 deletions

File tree

vscode-extension/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,10 +235,14 @@ up at the decimal point position). Keyword headers, comment lines, the closing
235235
every data line above and below the comment is aligned against a single shared set
236236
of column widths.
237237

238-
Comments are ignored when aligning: columns are positioned from the record data
239-
alone, and any comment lines (whether above or within the group) are left exactly
240-
as they are. A descriptive comment above a table is never mistaken for a column
241-
heading.
238+
Comments are ignored when aligning, with one exception: a **column heading**
239+
directly above the group — a `--` comment with exactly one word per column, as
240+
produced by [Add Column Headers](#add-column-headers) — is honoured. The records
241+
are aligned to it and the heading is kept in sync, so a table stays lined up
242+
under its heading across repeated alignments. Every other comment line is left
243+
exactly as it is: a descriptive comment above a table (any other word count, or
244+
one separated by a blank line) is never mistaken for a heading, and comments
245+
interspersed within the group are untouched.
242246

243247
Before:
244248
```

vscode-extension/src/extension.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
parseUdqExpressionLine,
1717
formatUdqBlock,
1818
buildHeadingAndAlignedRecords,
19+
matchHeadingForGroup,
1920
tokenColumnCount,
2021
toggleLineComments,
2122
} from './formatting';
@@ -824,14 +825,37 @@ function computeAlignEdits(
824825
// Extract just the record entries for formatting
825826
const records = entries.filter(e => e.record !== null).map(e => e.record as RecordLine);
826827

827-
// Columns are aligned from the record data alone. Comment lines — whether
828-
// above the group or interspersed within it — are ignored for alignment
829-
// and left untouched.
828+
// Columns are aligned from the record data. A column heading directly above
829+
// the group (a `--` comment with one word per column, as produced by "Add
830+
// Column Headers") is honoured: the data is aligned to it and the heading is
831+
// kept in sync. Any other comment line — a descriptive comment above the
832+
// table, a heading not directly adjacent, or comments interspersed within
833+
// the group — is ignored for alignment and left untouched.
830834
if (records.length > 1) {
831835
// Check whether the owning keyword is excluded before emitting any edits.
832836
const activeKw = findActiveKeyword(document, new vscode.Position(i, 0));
833837
if (!excludedKeywords.has((activeKw ?? '').toUpperCase())) {
834-
const formatted = formatRecordGroup(records);
838+
// The candidate heading is the line immediately above the first record.
839+
const headingLineNum = i - 1;
840+
const headingWords =
841+
headingLineNum >= 0
842+
? matchHeadingForGroup(document.lineAt(headingLineNum).text, nCols)
843+
: null;
844+
845+
let formatted: string[];
846+
if (headingWords) {
847+
const built = buildHeadingAndAlignedRecords(records, headingWords);
848+
formatted = built.formattedRecords;
849+
const headingOrig = document.lineAt(headingLineNum).text;
850+
if (built.heading !== headingOrig) {
851+
edits.push(
852+
vscode.TextEdit.replace(document.lineAt(headingLineNum).range, built.heading),
853+
);
854+
}
855+
} else {
856+
formatted = formatRecordGroup(records);
857+
}
858+
835859
let recordIdx = 0;
836860
for (const entry of entries) {
837861
if (entry.record === null) { continue; } // comment line — leave as-is

vscode-extension/src/formatting.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
formatUdqExpressionGroup,
1010
formatUdqBlock,
1111
parseHeadingPositions,
12+
matchHeadingForGroup,
1213
formatRecordGroupWithHeading,
1314
buildHeadingAndAlignedRecords,
1415
tokenColumnCount,
@@ -666,6 +667,39 @@ describe('parseHeadingPositions', () => {
666667
});
667668
});
668669

670+
// ---------------------------------------------------------------------------
671+
// matchHeadingForGroup — the discriminator that keeps ordinary comment lines
672+
// from being treated as column headings during alignment.
673+
// ---------------------------------------------------------------------------
674+
675+
describe('matchHeadingForGroup', () => {
676+
test('matches a comment with exactly one word per column', () => {
677+
expect(matchHeadingForGroup('-- WELL GROUP I J', 4)).toEqual(['WELL', 'GROUP', 'I', 'J']);
678+
expect(matchHeadingForGroup('-- Sw Krw', 2)).toEqual(['Sw', 'Krw']);
679+
});
680+
681+
test('rejects a descriptive comment whose word count differs from the columns', () => {
682+
// A prose comment above the table must not be honoured as a heading.
683+
expect(matchHeadingForGroup('-- multiplies PERMZ in the upper layers', 8)).toBeNull();
684+
expect(matchHeadingForGroup('-- WELL GROUP I', 4)).toBeNull(); // too few
685+
expect(matchHeadingForGroup('-- WELL GROUP I J K', 4)).toBeNull(); // too many
686+
});
687+
688+
test('rejects non-comment lines and empty comments', () => {
689+
expect(matchHeadingForGroup("'PERMZ' 0.2 1 1", 4)).toBeNull();
690+
expect(matchHeadingForGroup('--', 2)).toBeNull();
691+
expect(matchHeadingForGroup('-- ', 2)).toBeNull();
692+
});
693+
694+
test('never matches a single-column group', () => {
695+
expect(matchHeadingForGroup('-- PORO', 1)).toBeNull();
696+
});
697+
698+
test('tolerates indentation and extra spacing between words', () => {
699+
expect(matchHeadingForGroup(' -- A B ', 2)).toEqual(['A', 'B']);
700+
});
701+
});
702+
669703
// ---------------------------------------------------------------------------
670704
// formatRecordGroupWithHeading
671705
// ---------------------------------------------------------------------------

vscode-extension/src/formatting.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,26 @@ export function formatUdqBlock(lines: string[]): string[] {
497497
return lines.map((line, i) => (parsed[i] !== null ? formatted[idx++] : line));
498498
}
499499

500+
/**
501+
* Decide whether a comment line directly above a record group is that group's
502+
* column heading (one label per column) rather than a free-form descriptive
503+
* comment. A heading must be a `--` comment whose word count equals the group's
504+
* column count `nCols` (and there must be at least two columns).
505+
*
506+
* The exact-count rule is what keeps ordinary comment lines from being treated
507+
* as headings: a prose comment almost never has exactly one word per column,
508+
* and the caller only ever offers the line immediately above the table — a
509+
* comment elsewhere, or one separated by a blank line, is never considered.
510+
* Returns the heading words on a match, else null.
511+
*/
512+
export function matchHeadingForGroup(commentLine: string, nCols: number): string[] | null {
513+
if (nCols < 2) return null;
514+
const m = commentLine.match(/^\s*--\s*(\S.*)$/);
515+
if (!m) return null;
516+
const words = m[1].trim().split(/\s+/).filter(Boolean);
517+
return words.length === nCols ? words : null;
518+
}
519+
500520
// Parse absolute char positions of each word in a heading comment line (-- word1 word2 ...)
501521
export function parseHeadingPositions(line: string): number[] | null {
502522
const m = line.match(/^(\s*--\s*)(.*)/);

0 commit comments

Comments
 (0)