Skip to content

Commit 84d6c9f

Browse files
committed
Add csv.showTrailingEmptyRow setting to optionally hide trailing empty row
1 parent 8313a46 commit 84d6c9f

File tree

4 files changed

+99
-11
lines changed

4 files changed

+99
-11
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ Global (Settings UI or `settings.json`):
101101
- `csv.columnColorMode` (string, default `type`): `type` keeps CSV’s type-based column colors; `theme` uses your theme foreground color for all columns.
102102
- `csv.columnColorPalette` (string, default `default`): Type-color palette when `csv.columnColorMode` is `type`. `cool` biases colors toward greens/blues; `warm` biases colors toward oranges/reds.
103103
- `csv.clickableLinks` (boolean, default `true`): Make URLs in cells clickable. Ctrl/Cmd+click to open links.
104+
- `csv.showTrailingEmptyRow` (boolean, default `true`): Show the extra empty row at the end of the table. Turn this off to hide that visual append row.
104105
- `csv.separatorMode` (string, default `extension`): Separator selection mode when no per-file override exists. `extension` uses extension mapping, `auto` detects from content first, `default` always uses `csv.defaultSeparator`.
105106
- `csv.defaultSeparator` (string, default `,`): Fallback separator. Use `\\t` in `settings.json` for tabs.
106107
- `csv.separatorByExtension` (object): Extension-to-separator mapping (defaults include `.csv``,`, `.tsv`/`.tab`→tab, `.psv``|`).

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@
145145
"default": true,
146146
"description": "Make URLs in cells clickable. Ctrl/Cmd+click to open links."
147147
},
148+
"csv.showTrailingEmptyRow": {
149+
"type": "boolean",
150+
"default": true,
151+
"description": "Show an extra empty row at the end of the table for quick append-style editing."
152+
},
148153
"csv.separatorMode": {
149154
"type": "string",
150155
"enum": [

src/CsvEditorProvider.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,7 @@ class CsvEditorController {
976976
const clickableLinks = config.get<boolean>('clickableLinks', true);
977977
const columnColorMode = config.get<string>('columnColorMode', 'type');
978978
const columnColorPalette = config.get<string>('columnColorPalette', 'default');
979+
const showTrailingEmptyRow = config.get<boolean>('showTrailingEmptyRow', true);
979980

980981
const { tableHtml, chunksJson, colorCss } =
981982
this.generateTableAndChunks(
@@ -985,7 +986,8 @@ class CsvEditorController {
985986
hiddenRows,
986987
clickableLinks,
987988
columnColorMode,
988-
columnColorPalette
989+
columnColorPalette,
990+
showTrailingEmptyRow
989991
);
990992

991993
const nonce = this.getNonce();
@@ -1009,7 +1011,8 @@ class CsvEditorController {
10091011
hiddenRows: number,
10101012
clickableLinks: boolean,
10111013
columnColorMode: string,
1012-
columnColorPalette: string
1014+
columnColorPalette: string,
1015+
showTrailingEmptyRow: boolean
10131016
): { tableHtml: string; chunksJson: string; colorCss: string } {
10141017
let headerFlag = treatHeader;
10151018
const totalRows = data.length;
@@ -1045,7 +1048,11 @@ class CsvEditorController {
10451048
const CHUNK_SIZE = 1000;
10461049
const allRows = headerFlag ? bodyData : data.slice(offset);
10471050
const allRowsCount = allRows.length; // preserve total before any truncation
1048-
const serialIndexWidthCh = Math.max(4, String(Math.max(1, allRowsCount + 1)).length + 1);
1051+
// Always keep one editable row for fully empty views; otherwise allow disabling the
1052+
// trailing virtual row via settings.
1053+
const includeTrailingEmptyRow = showTrailingEmptyRow || allRowsCount === 0;
1054+
const serialIndexMaxDisplay = includeTrailingEmptyRow ? allRowsCount + 1 : allRowsCount;
1055+
const serialIndexWidthCh = Math.max(4, String(Math.max(1, serialIndexMaxDisplay)).length + 1);
10491056
const chunks: string[] = [];
10501057
const chunked = allRowsCount > CHUNK_SIZE;
10511058

@@ -1113,7 +1120,7 @@ class CsvEditorController {
11131120
}
11141121
tableHtml += `</tr>`;
11151122
});
1116-
if (!chunked) {
1123+
if (!chunked && includeTrailingEmptyRow) {
11171124
const virtualAbs = offset + 1 + bodyData.length;
11181125
const idxCell = addSerialIndex ? `<td tabindex="0" style="min-width: ${serialIndexWidthCh}ch; max-width: ${serialIndexWidthCh}ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; color: #888;" data-row="${virtualAbs}" data-col="-1">${bodyData.length + 1}</td>` : '';
11191126
const dataCells = Array.from({ length: numColumns }, (_, i) => `<td tabindex="0" style="min-width: ${Math.min(columnWidths[i] || 0, 100)}ch; max-width: 100ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; color: ${columnColors[i]}; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere;" data-row="${virtualAbs}" data-col="${i}"></td>`).join('');
@@ -1138,7 +1145,7 @@ class CsvEditorController {
11381145
}
11391146
tableHtml += `</tr>`;
11401147
});
1141-
if (!chunked) {
1148+
if (!chunked && includeTrailingEmptyRow) {
11421149
const virtualAbs = offset + nonHeaderRows.length;
11431150
const displayIdx = nonHeaderRows.length + 1;
11441151
const idxCell = addSerialIndex ? `<td tabindex="0" style="min-width: ${serialIndexWidthCh}ch; max-width: ${serialIndexWidthCh}ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; color: #888;" data-row="${virtualAbs}" data-col="-1">${displayIdx}</td>` : '';
@@ -1148,8 +1155,8 @@ class CsvEditorController {
11481155
tableHtml += `</tbody>`;
11491156
}
11501157
tableHtml += `</table>`;
1151-
// If chunked, append a final chunk with the virtual row so it appears at the end
1152-
if (chunked) {
1158+
// If chunked, append a final chunk with the virtual row so it appears at the end.
1159+
if (chunked && includeTrailingEmptyRow) {
11531160
const startAbs = headerFlag ? offset + 1 : offset;
11541161
const virtualAbs = startAbs + allRowsCount;
11551162
const displayIdx = allRowsCount + 1;
@@ -2292,10 +2299,20 @@ export class CsvEditorProvider implements vscode.CustomTextEditorProvider {
22922299
hiddenRows: number,
22932300
clickableLinks: boolean = true,
22942301
columnColorMode: 'type' | 'theme' = 'type',
2295-
columnColorPalette: 'default' | 'cool' | 'warm' = 'default'
2302+
columnColorPalette: 'default' | 'cool' | 'warm' = 'default',
2303+
showTrailingEmptyRow: boolean = true
22962304
): { chunkCount: number; hasTable: boolean } {
22972305
const c: any = new (CsvEditorController as any)({} as any);
2298-
const result = c.generateTableAndChunks(data, treatHeader, addSerialIndex, hiddenRows, clickableLinks, columnColorMode, columnColorPalette);
2306+
const result = c.generateTableAndChunks(
2307+
data,
2308+
treatHeader,
2309+
addSerialIndex,
2310+
hiddenRows,
2311+
clickableLinks,
2312+
columnColorMode,
2313+
columnColorPalette,
2314+
showTrailingEmptyRow
2315+
);
22992316
try {
23002317
const chunks = JSON.parse(result.chunksJson);
23012318
return { chunkCount: Array.isArray(chunks) ? chunks.length : 0, hasTable: typeof result.tableHtml === 'string' && result.tableHtml.includes('<table') };
@@ -2310,10 +2327,20 @@ export class CsvEditorProvider implements vscode.CustomTextEditorProvider {
23102327
hiddenRows: number,
23112328
clickableLinks: boolean = true,
23122329
columnColorMode: 'type' | 'theme' = 'type',
2313-
columnColorPalette: 'default' | 'cool' | 'warm' = 'default'
2330+
columnColorPalette: 'default' | 'cool' | 'warm' = 'default',
2331+
showTrailingEmptyRow: boolean = true
23142332
): { tableHtml: string; chunks: string[] } {
23152333
const c: any = new (CsvEditorController as any)({} as any);
2316-
const result = c.generateTableAndChunks(data, treatHeader, addSerialIndex, hiddenRows, clickableLinks, columnColorMode, columnColorPalette);
2334+
const result = c.generateTableAndChunks(
2335+
data,
2336+
treatHeader,
2337+
addSerialIndex,
2338+
hiddenRows,
2339+
clickableLinks,
2340+
columnColorMode,
2341+
columnColorPalette,
2342+
showTrailingEmptyRow
2343+
);
23172344
let chunks: string[] = [];
23182345
try { chunks = JSON.parse(result.chunksJson); } catch {}
23192346
return { tableHtml: result.tableHtml, chunks };

src/test/virtuals-invariants.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,61 @@ describe('Virtual row and cell invariants', () => {
5353
}
5454
});
5555

56+
it('can hide the trailing virtual row for non-empty non-chunked data', () => {
57+
const data = [
58+
['a'],
59+
['b', 'c', 'd']
60+
];
61+
const { tableHtml, chunks } = CsvEditorProvider.__test.generateTableAndChunksRaw(
62+
data,
63+
/*treatHeader*/ false,
64+
/*addSerialIndex*/ false,
65+
/*hiddenRows*/ 0,
66+
/*clickableLinks*/ true,
67+
/*columnColorMode*/ 'type',
68+
/*columnColorPalette*/ 'default',
69+
/*showTrailingEmptyRow*/ false
70+
);
71+
assert.strictEqual(chunks.length, 0);
72+
assert.ok(!tableHtml.includes('data-row="2" data-col="0"'));
73+
assert.ok(!tableHtml.includes('data-row="2" data-col="1"'));
74+
assert.ok(!tableHtml.includes('data-row="2" data-col="2"'));
75+
});
76+
77+
it('can hide the trailing virtual row for non-empty chunked data', () => {
78+
const rows: string[][] = Array.from({ length: 1500 }, (_, i) => [String(i + 1), 'x', 'y']);
79+
const { tableHtml, chunks } = CsvEditorProvider.__test.generateTableAndChunksRaw(
80+
rows,
81+
/*treatHeader*/ false,
82+
/*addSerialIndex*/ false,
83+
/*hiddenRows*/ 0,
84+
/*clickableLinks*/ true,
85+
/*columnColorMode*/ 'type',
86+
/*columnColorPalette*/ 'default',
87+
/*showTrailingEmptyRow*/ false
88+
);
89+
assert.ok(!tableHtml.includes('data-row="1500"'));
90+
// Only one chunk should remain (rows 1000-1499). No final virtual-row chunk.
91+
assert.strictEqual(chunks.length, 1);
92+
assert.ok(!chunks[0].includes('data-row="1500"'));
93+
});
94+
95+
it('still renders one editable row for empty data even when trailing row is disabled', () => {
96+
const rows: string[][] = [];
97+
const { tableHtml, chunks } = CsvEditorProvider.__test.generateTableAndChunksRaw(
98+
rows,
99+
/*treatHeader*/ false,
100+
/*addSerialIndex*/ false,
101+
/*hiddenRows*/ 0,
102+
/*clickableLinks*/ true,
103+
/*columnColorMode*/ 'type',
104+
/*columnColorPalette*/ 'default',
105+
/*showTrailingEmptyRow*/ false
106+
);
107+
assert.strictEqual(chunks.length, 0);
108+
assert.ok(tableHtml.includes('data-row="0" data-col="0"'));
109+
});
110+
56111
it('sizes serial index column from total row count for chunked data', () => {
57112
const rows: string[][] = Array.from({ length: 12345 }, (_, i) => [String(i + 1), 'x']);
58113
const { tableHtml, chunks } = CsvEditorProvider.__test.generateTableAndChunksRaw(rows, /*treatHeader*/ false, /*addSerialIndex*/ true, /*hiddenRows*/ 0);

0 commit comments

Comments
 (0)