@@ -1436,12 +1436,50 @@ document.addEventListener('keydown', e => {
14361436 return ;
14371437 }
14381438
1439+ if ( ! editingCell && e . key === 'Tab' && ! e . ctrlKey && ! e . metaKey && ! e . altKey ) {
1440+ e . preventDefault ( ) ;
1441+ const refCell = anchorCell || getCellTarget ( document . activeElement ) || currentSelection [ 0 ] || document . querySelector ( 'td.selected, th.selected' ) ;
1442+ if ( ! refCell ) return ;
1443+ const coords = getCellCoords ( refCell ) ;
1444+ if ( ! coords || ! Number . isInteger ( coords . row ) || ! Number . isInteger ( coords . col ) || coords . col < 0 ) {
1445+ return ;
1446+ }
1447+ const bounds = getDataColumnBounds ( ) ;
1448+ if ( ! bounds ) return ;
1449+ const { minCol, maxCol } = bounds ;
1450+ const firstDataRow = getFirstDataRow ( ) ;
1451+ const isBackward = ! ! e . shiftKey ;
1452+ let targetRow = coords . row ;
1453+ let targetCol = coords . col + ( isBackward ? - 1 : 1 ) ;
1454+ if ( ! isBackward && targetCol > maxCol ) {
1455+ targetRow += 1 ;
1456+ targetCol = minCol ;
1457+ } else if ( isBackward && targetCol < minCol ) {
1458+ if ( targetRow <= firstDataRow ) {
1459+ return ;
1460+ }
1461+ targetRow -= 1 ;
1462+ targetCol = maxCol ;
1463+ }
1464+ const nextCell = ensureRenderedCellByCoords ( targetRow , targetCol ) ;
1465+ if ( nextCell ) {
1466+ setSingleSelection ( nextCell ) ;
1467+ }
1468+ return ;
1469+ }
1470+
14391471 /* ──────── NEW: ENTER + DIRECT TYPING HANDLERS ──────── */
14401472 if ( ! editingCell && anchorCell && currentSelection . length === 1 ) {
14411473 if ( e . key === 'Enter' ) {
14421474 e . preventDefault ( ) ;
1475+ const cell = anchorCell ;
14431476 // Detail edit via Enter
1444- editCell ( anchorCell , undefined , 'detail' ) ;
1477+ editCell ( cell , undefined , 'detail' ) ;
1478+ if ( e . shiftKey ) {
1479+ // Shift+Enter from selection should open detail edit and insert
1480+ // a newline immediately on the very first keypress.
1481+ appendVisibleNewlineAtEnd ( cell ) ;
1482+ }
14451483 return ;
14461484 }
14471485 if ( e . key . length === 1 && ! e . ctrlKey && ! e . metaKey && ! e . altKey ) {
@@ -1560,31 +1598,57 @@ document.addEventListener('keydown', e => {
15601598 }
15611599 if ( editingCell && e . key === 'Enter' ) {
15621600 e . preventDefault ( ) ;
1601+ if ( e . shiftKey ) {
1602+ if ( ! insertNewlineAtCaret ( editingCell ) ) {
1603+ appendVisibleNewlineAtEnd ( editingCell ) ;
1604+ }
1605+ return ;
1606+ }
15631607 const { row, col } = getCellCoords ( editingCell ) ;
15641608 editingCell . blur ( ) ;
15651609 const targetRow = row + 1 ;
1566- const nextCell = table . querySelector ( 'td[data-row="' + targetRow + '\"][data-col="' + col + '"]' ) ;
1610+ // Editing Enter commits and moves selection down (no auto-edit).
1611+ const nextCell = ensureRenderedCellByCoords ( targetRow , col ) ;
15671612 if ( nextCell ) {
1568- editCell ( nextCell ) ;
1613+ setSingleSelection ( nextCell ) ;
15691614 } else {
15701615 try {
15711616 const st = vscode . getState ( ) || { } ;
1572- vscode . setState ( { ...st , anchorRow : targetRow , anchorCol : col , pendingEdit : 'detail' } ) ;
1617+ vscode . setState ( { ...st , anchorRow : targetRow , anchorCol : col } ) ;
15731618 } catch { }
15741619 }
15751620 }
15761621 if ( editingCell && e . key === 'Tab' ) {
15771622 e . preventDefault ( ) ;
1578- const { row, col } = getCellCoords ( editingCell ) ;
1579- editingCell . blur ( ) ;
1580- let nextCell ;
1581- if ( e . shiftKey ) {
1582- nextCell = table . querySelector ( 'td[data-row="' + row + '"][data-col="' + ( col - 1 ) + '"]' ) ;
1583- } else {
1584- nextCell = table . querySelector ( 'td[data-row="' + row + '"][data-col="' + ( col + 1 ) + '"]' ) ;
1623+ const cell = editingCell ;
1624+ const { row, col } = getCellCoords ( cell ) ;
1625+ const bounds = getDataColumnBounds ( ) ;
1626+ const firstDataRow = getFirstDataRow ( ) ;
1627+ const isBackward = ! ! e . shiftKey ;
1628+ let targetRow = row ;
1629+ let targetCol = col ;
1630+ let canMove = ! ! bounds ;
1631+ if ( bounds ) {
1632+ targetCol = col + ( isBackward ? - 1 : 1 ) ;
1633+ if ( ! isBackward && targetCol > bounds . maxCol ) {
1634+ targetRow += 1 ;
1635+ targetCol = bounds . minCol ;
1636+ } else if ( isBackward && targetCol < bounds . minCol ) {
1637+ if ( targetRow <= firstDataRow ) {
1638+ canMove = false ;
1639+ } else {
1640+ targetRow -= 1 ;
1641+ targetCol = bounds . maxCol ;
1642+ }
1643+ }
15851644 }
1645+ cell . blur ( ) ;
1646+ // Editing Tab commits and moves selection only (no auto-edit).
1647+ const nextCell = canMove ? ensureRenderedCellByCoords ( targetRow , targetCol ) : null ;
15861648 if ( nextCell ) {
1587- editCell ( nextCell ) ;
1649+ setSingleSelection ( nextCell ) ;
1650+ } else {
1651+ setSingleSelection ( cell ) ;
15881652 }
15891653 }
15901654 if ( editingCell && e . key === 'Escape' ) {
@@ -1614,6 +1678,94 @@ const setCursorAtPoint = (cell, x, y) => {
16141678 if ( range ) { let sel = window . getSelection ( ) ; sel . removeAllRanges ( ) ; sel . addRange ( range ) ; }
16151679} ;
16161680
1681+ const getDataColumnBounds = ( ) => {
1682+ const cols = Array . from ( table . querySelectorAll ( 'td[data-col], th[data-col]' ) )
1683+ . map ( el => parseInt ( el . getAttribute ( 'data-col' ) || 'NaN' , 10 ) )
1684+ . filter ( col => Number . isInteger ( col ) && col >= 0 ) ;
1685+ if ( ! cols . length ) {
1686+ return null ;
1687+ }
1688+ return { minCol : Math . min ( ...cols ) , maxCol : Math . max ( ...cols ) } ;
1689+ } ;
1690+
1691+ const setSingleSelection = cell => {
1692+ if ( ! cell ) return ;
1693+ clearSelection ( ) ;
1694+ cell . classList . add ( 'selected' ) ;
1695+ currentSelection . push ( cell ) ;
1696+ anchorCell = cell ;
1697+ rangeEndCell = cell ;
1698+ persistState ( ) ;
1699+ try { cell . focus ( { preventScroll : true } ) ; } catch { try { cell . focus ( ) ; } catch { } }
1700+ cell . scrollIntoView ( { block : 'nearest' , inline : 'nearest' , behavior : 'smooth' } ) ;
1701+ } ;
1702+
1703+ const NEWLINE_SENTINEL_ATTR = 'data-csv-newline-sentinel' ;
1704+ const removeNewlineSentinels = cell => {
1705+ if ( ! cell ) return ;
1706+ cell . querySelectorAll ( `[${ NEWLINE_SENTINEL_ATTR } ="true"]` ) . forEach ( node => node . remove ( ) ) ;
1707+ } ;
1708+
1709+ const placeCaretBeforeSentinel = sentinel => {
1710+ const sel = window . getSelection ( ) ;
1711+ if ( ! sel ) return ;
1712+ const range = document . createRange ( ) ;
1713+ if ( sentinel . firstChild ) {
1714+ range . setStart ( sentinel . firstChild , 0 ) ;
1715+ } else {
1716+ range . setStartBefore ( sentinel ) ;
1717+ }
1718+ range . collapse ( true ) ;
1719+ sel . removeAllRanges ( ) ;
1720+ sel . addRange ( range ) ;
1721+ } ;
1722+
1723+ const appendVisibleNewlineAtEnd = cell => {
1724+ removeNewlineSentinels ( cell ) ;
1725+ const sentinel = document . createElement ( 'span' ) ;
1726+ sentinel . setAttribute ( NEWLINE_SENTINEL_ATTR , 'true' ) ;
1727+ sentinel . textContent = '\u200B' ;
1728+ cell . appendChild ( document . createTextNode ( '\n' ) ) ;
1729+ cell . appendChild ( sentinel ) ;
1730+ placeCaretBeforeSentinel ( sentinel ) ;
1731+ } ;
1732+
1733+ const isRangeAtEndOfCell = ( cell , range ) => {
1734+ const probe = document . createRange ( ) ;
1735+ probe . selectNodeContents ( cell ) ;
1736+ probe . setEnd ( range . endContainer , range . endOffset ) ;
1737+ const caretOffset = probe . toString ( ) . length ;
1738+ return caretOffset >= ( cell . textContent || '' ) . length ;
1739+ } ;
1740+
1741+ const insertNewlineAtCaret = cell => {
1742+ removeNewlineSentinels ( cell ) ;
1743+ const sel = window . getSelection ( ) ;
1744+ if ( ! sel || sel . rangeCount === 0 ) return false ;
1745+ const range = sel . getRangeAt ( 0 ) ;
1746+ if ( ! cell . contains ( range . commonAncestorContainer ) ) return false ;
1747+ const atEnd = range . collapsed && isRangeAtEndOfCell ( cell , range ) ;
1748+ range . deleteContents ( ) ;
1749+ if ( atEnd ) {
1750+ const sentinel = document . createElement ( 'span' ) ;
1751+ sentinel . setAttribute ( NEWLINE_SENTINEL_ATTR , 'true' ) ;
1752+ sentinel . textContent = '\u200B' ;
1753+ const fragment = document . createDocumentFragment ( ) ;
1754+ fragment . appendChild ( document . createTextNode ( '\n' ) ) ;
1755+ fragment . appendChild ( sentinel ) ;
1756+ range . insertNode ( fragment ) ;
1757+ placeCaretBeforeSentinel ( sentinel ) ;
1758+ return true ;
1759+ }
1760+ const newlineNode = document . createTextNode ( '\n' ) ;
1761+ range . insertNode ( newlineNode ) ;
1762+ range . setStartAfter ( newlineNode ) ;
1763+ range . setEndAfter ( newlineNode ) ;
1764+ sel . removeAllRanges ( ) ;
1765+ sel . addRange ( range ) ;
1766+ return true ;
1767+ } ;
1768+
16171769const editCell = ( cell , event , mode = 'detail' ) => {
16181770 if ( editingCell === cell ) return ;
16191771 if ( editingCell ) editingCell . blur ( ) ;
@@ -1625,6 +1777,7 @@ const editCell = (cell, event, mode = 'detail') => {
16251777 cell . setAttribute ( 'contenteditable' , 'true' ) ;
16261778 cell . focus ( ) ;
16271779 const onBlurHandler = ( ) => {
1780+ removeNewlineSentinels ( cell ) ;
16281781 const value = cell . textContent ;
16291782 const coords = getCellCoords ( cell ) ;
16301783 vscode . postMessage ( { type : 'editCell' , row : coords . row , col : coords . col , value : value } ) ;
0 commit comments