Skip to content

Commit a27ee0b

Browse files
committed
Virtual rows and cells
1 parent c9d337d commit a27ee0b

File tree

2 files changed

+122
-20
lines changed

2 files changed

+122
-20
lines changed

media/main.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -674,8 +674,16 @@ document.addEventListener('keydown', e => {
674674
e.preventDefault();
675675
const { row, col } = getCellCoords(editingCell);
676676
editingCell.blur();
677-
const nextCell = table.querySelector('td[data-row="'+(row+1)+'"][data-col="'+col+'"]');
678-
if (nextCell) editCell(nextCell);
677+
const targetRow = row + 1;
678+
const nextCell = table.querySelector('td[data-row="'+targetRow+'\"][data-col="'+col+'"]');
679+
if (nextCell) {
680+
editCell(nextCell);
681+
} else {
682+
try {
683+
const st = vscode.getState() || {};
684+
vscode.setState({ ...st, anchorRow: targetRow, anchorCol: col, pendingEdit: 'detail' });
685+
} catch {}
686+
}
679687
}
680688
if (editingCell && e.key === 'Tab') {
681689
e.preventDefault();
@@ -784,6 +792,29 @@ window.addEventListener('message', event => {
784792
}
785793
});
786794

795+
// After initial restoreState, if there's a pending edit request, perform it
796+
const maybeResumePendingEdit = () => {
797+
try {
798+
const st = vscode.getState() || {};
799+
if (st && typeof st.anchorRow === 'number' && typeof st.anchorCol === 'number' && st.pendingEdit === 'detail') {
800+
const tag = (hasHeader && st.anchorRow === 0 ? 'th' : 'td');
801+
const sel = table.querySelector(`${tag}[data-row="${st.anchorRow}"][data-col="${st.anchorCol}"]`);
802+
if (sel) {
803+
editCell(sel, undefined, 'detail');
804+
// clear pending flag
805+
const next = { ...st };
806+
delete next.pendingEdit;
807+
vscode.setState(next);
808+
}
809+
}
810+
} catch {}
811+
};
812+
813+
// Try after load and after visibility/focus restores
814+
setTimeout(maybeResumePendingEdit, 0);
815+
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') setTimeout(maybeResumePendingEdit, 0); });
816+
window.addEventListener('focus', () => { setTimeout(maybeResumePendingEdit, 0); }, { passive: true });
817+
787818
document.addEventListener('keydown', e => {
788819
if(!editingCell && e.key === 'Escape'){
789820
clearSelection();

src/CsvEditorProvider.ts

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,48 @@ class CsvEditorController {
146146
const oldText = this.document.getText();
147147
const result = Papa.parse(oldText, { dynamicTyping: false, delimiter: separator });
148148
const data = result.data as string[][];
149-
while (data.length <= row) data.push([]);
150-
while (data[row].length <= col) data[row].push('');
151-
data[row][col] = value;
149+
const hadRows = data.length;
150+
const hadColsAtRow = (data[row] ? data[row].length : 0);
151+
const wasEditingLastRow = row >= (data.length - 1);
152+
153+
const rowExists = row < data.length;
154+
const colExists = rowExists && col < data[row].length;
155+
156+
if (value === '') {
157+
if (!rowExists) {
158+
// Do not expand file with an empty virtual row
159+
this.isUpdatingDocument = false;
160+
return;
161+
}
162+
if (!colExists) {
163+
// Do not expand row width with an empty virtual cell
164+
this.isUpdatingDocument = false;
165+
return;
166+
}
167+
// Existing cell: clear value
168+
data[row][col] = '';
169+
} else {
170+
// Non-empty value: expand as needed and set
171+
while (data.length <= row) data.push([]);
172+
while (data[row].length <= col) data[row].push('');
173+
data[row][col] = value;
174+
}
175+
// If we edited the (previous) last row, trim trailing empty rows recursively
176+
let trimmed = false;
177+
if (wasEditingLastRow) {
178+
const isRowEmpty = (arr: string[] | undefined) => {
179+
if (!arr || arr.length === 0) return true;
180+
for (let i = 0; i < arr.length; i++) {
181+
if ((arr[i] ?? '') !== '') return false;
182+
}
183+
return true;
184+
};
185+
while (data.length > 0 && isRowEmpty(data[data.length - 1])) {
186+
data.pop();
187+
trimmed = true;
188+
}
189+
}
190+
152191
const newCsvText = Papa.unparse(data, { delimiter: separator });
153192

154193
const fullRange = new vscode.Range(
@@ -163,6 +202,11 @@ class CsvEditorController {
163202
this.isUpdatingDocument = false;
164203
console.log(`CSV: Updated row ${row + 1}, column ${col + 1} to "${value}"`);
165204
this.currentWebviewPanel?.webview.postMessage({ type: 'updateCell', row, col, value });
205+
206+
// Trigger a full re-render if structure may have changed (new row/col created)
207+
if (trimmed || row >= hadRows || col >= hadColsAtRow) {
208+
try { this.updateWebviewContent(); } catch (e) { console.error('CSV: refresh failed after structural edit', e); }
209+
}
166210
}
167211

168212
private async handleSave() {
@@ -394,7 +438,8 @@ class CsvEditorController {
394438
bodyData = data.slice(offset);
395439
}
396440
const visibleForWidth = headerFlag ? [headerRow, ...bodyData] : bodyData;
397-
const numColumns = Math.max(...visibleForWidth.map(row => row.length), 0);
441+
let numColumns = Math.max(...visibleForWidth.map(row => row.length), 0);
442+
if (numColumns === 0) numColumns = 1; // ensure at least 1 column for the virtual row
398443

399444
const columnData = Array.from({ length: numColumns }, (_, i) => bodyData.map(row => row[i] || ''));
400445
const columnTypes = columnData.map(col => this.estimateColumnDataType(col));
@@ -404,16 +449,18 @@ class CsvEditorController {
404449
const CHUNK_SIZE = 1000;
405450
const allRows = headerFlag ? bodyData : data.slice(offset);
406451
const chunks: string[] = [];
452+
const chunked = allRows.length > CHUNK_SIZE;
407453

408454
if (allRows.length > CHUNK_SIZE) {
409455
for (let i = CHUNK_SIZE; i < allRows.length; i += CHUNK_SIZE) {
410456
const htmlChunk = allRows.slice(i, i + CHUNK_SIZE).map((row, localR) => {
411457
const startAbs = headerFlag ? offset + 1 : offset;
412458
const absRow = startAbs + i + localR;
413-
const cells = row.map((cell, cIdx) => {
414-
const safe = this.escapeHtml(cell);
415-
return `<td tabindex="0" style="min-width:${Math.min(columnWidths[cIdx]||0,100)}ch;max-width:100ch;border:1px solid ${isDark?'#555':'#ccc'};color:${columnColors[cIdx]};overflow:hidden;white-space:nowrap;text-overflow:ellipsis;" data-row="${absRow}" data-col="${cIdx}">${safe}</td>`;
416-
}).join('');
459+
let cells = '';
460+
for (let cIdx = 0; cIdx < numColumns; cIdx++) {
461+
const safe = this.escapeHtml(row[cIdx] || '');
462+
cells += `<td tabindex="0" style="min-width:${Math.min(columnWidths[cIdx]||0,100)}ch;max-width:100ch;border:1px solid ${isDark?'#555':'#ccc'};color:${columnColors[cIdx]};overflow:hidden;white-space:nowrap;text-overflow:ellipsis;" data-row="${absRow}" data-col="${cIdx}">${safe}</td>`;
463+
}
417464

418465
return `<tr>${
419466
addSerialIndex ? `<td tabindex="0" style="min-width:4ch;max-width:4ch;border:1px solid ${isDark?'#555':'#ccc'};color:#888;" data-row="${absRow}" data-col="-1">${absRow}</td>` : ''
@@ -438,41 +485,65 @@ class CsvEditorController {
438485
? `<th tabindex="0" style="min-width: 4ch; max-width: 4ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; background-color: ${isDark ? '#1e1e1e' : '#ffffff'}; color: #888;"></th>`
439486
: ''
440487
}`;
441-
headerRow.forEach((cell, i) => {
442-
const safe = this.escapeHtml(cell);
488+
for (let i = 0; i < numColumns; i++) {
489+
const safe = this.escapeHtml(headerRow[i] || '');
443490
tableHtml += `<th tabindex="0" style="min-width: ${Math.min(columnWidths[i] || 0, 100)}ch; max-width: 100ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; background-color: ${isDark ? '#1e1e1e' : '#ffffff'}; color: ${columnColors[i]}; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;" data-row="${offset}" data-col="${i}">${safe}</th>`;
444-
});
491+
}
445492
tableHtml += `</tr></thead><tbody>`;
446493
bodyData.forEach((row, r) => {
447494
tableHtml += `<tr>${
448495
addSerialIndex
449496
? `<td tabindex="0" style="min-width: 4ch; max-width: 4ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; color: #888;" data-row="${offset + 1 + r}" data-col="-1">${offset + 1 + r}</td>`
450497
: ''
451498
}`;
452-
row.forEach((cell, i) => {
453-
const safe = this.escapeHtml(cell);
499+
for (let i = 0; i < numColumns; i++) {
500+
const safe = this.escapeHtml(row[i] || '');
454501
tableHtml += `<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: hidden; white-space: nowrap; text-overflow: ellipsis;" data-row="${offset + 1 + r}" data-col="${i}">${safe}</td>`;
455-
});
502+
}
456503
tableHtml += `</tr>`;
457504
});
505+
if (!chunked) {
506+
const virtualAbs = offset + 1 + bodyData.length;
507+
const idxCell = addSerialIndex ? `<td tabindex="0" style="min-width: 4ch; max-width: 4ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; color: #888;" data-row="${virtualAbs}" data-col="-1">${virtualAbs}</td>` : '';
508+
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: hidden; white-space: nowrap; text-overflow: ellipsis;" data-row="${virtualAbs}" data-col="${i}"></td>`).join('');
509+
tableHtml += `<tr>${idxCell}${dataCells}</tr>`;
510+
}
458511
tableHtml += `</tbody>`;
459512
} else {
460513
tableHtml += `<tbody>`;
461-
data.slice(offset).forEach((row, r) => {
514+
const nonHeaderRows = data.slice(offset);
515+
nonHeaderRows.forEach((row, r) => {
462516
tableHtml += `<tr>${
463517
addSerialIndex
464518
? `<td tabindex="0" style="min-width: 4ch; max-width: 4ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; color: #888;" data-row="${offset + r}" data-col="-1">${r + 1}</td>`
465519
: ''
466520
}`;
467-
row.forEach((cell, i) => {
468-
const safe = this.escapeHtml(cell);
521+
for (let i = 0; i < numColumns; i++) {
522+
const safe = this.escapeHtml(row[i] || '');
469523
tableHtml += `<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: hidden; white-space: nowrap; text-overflow: ellipsis;" data-row="${offset + r}" data-col="${i}">${safe}</td>`;
470-
});
524+
}
471525
tableHtml += `</tr>`;
472526
});
527+
if (!chunked) {
528+
const virtualAbs = offset + nonHeaderRows.length;
529+
const displayIdx = nonHeaderRows.length + 1;
530+
const idxCell = addSerialIndex ? `<td tabindex="0" style="min-width: 4ch; max-width: 4ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; color: #888;" data-row="${virtualAbs}" data-col="-1">${displayIdx}</td>` : '';
531+
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: hidden; white-space: nowrap; text-overflow: ellipsis;" data-row="${virtualAbs}" data-col="${i}"></td>`).join('');
532+
tableHtml += `<tr>${idxCell}${dataCells}</tr>`;
533+
}
473534
tableHtml += `</tbody>`;
474535
}
475536
tableHtml += `</table>`;
537+
// If chunked, append a final chunk with the virtual row so it appears at the end
538+
if (chunked) {
539+
const startAbs = headerFlag ? offset + 1 : offset;
540+
const virtualAbs = startAbs + allRows.length;
541+
const displayIdx = headerFlag ? virtualAbs : (allRows.length + 1);
542+
const idxCell = addSerialIndex ? `<td tabindex="0" style="min-width: 4ch; max-width: 4ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; color: #888;" data-row="${virtualAbs}" data-col="-1">${displayIdx}</td>` : '';
543+
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: hidden; white-space: nowrap; text-overflow: ellipsis;" data-row="${virtualAbs}" data-col="${i}"></td>`).join('');
544+
const vrow = `<tr>${idxCell}${dataCells}</tr>`;
545+
chunks.push(vrow);
546+
}
476547

477548
return { tableHtml, chunksJson: JSON.stringify(chunks), colorCss };
478549
}

0 commit comments

Comments
 (0)