@@ -218,6 +218,9 @@ const ZOOM_MAX = 200;
218218const ZOOM_STEP = 10 ;
219219let zoomLevel = 100 ; // Percentage (100 = default)
220220
221+ // Word wrap setting
222+ let wordWrapEnabled = false ;
223+
221224// Get current line height based on zoom
222225function getLineHeight ( ) : number {
223226 return Math . round ( BASE_LINE_HEIGHT * ( zoomLevel / 100 ) ) ;
@@ -294,6 +297,7 @@ const elements = {
294297 btnCancelSplit : document . getElementById ( 'btn-cancel-split' ) as HTMLButtonElement ,
295298 // Columns
296299 btnColumns : document . getElementById ( 'btn-columns' ) as HTMLButtonElement ,
300+ btnWordWrap : document . getElementById ( 'btn-word-wrap' ) as HTMLButtonElement ,
297301 columnsModal : document . getElementById ( 'columns-modal' ) as HTMLDivElement ,
298302 columnsLoading : document . getElementById ( 'columns-loading' ) as HTMLDivElement ,
299303 columnsContent : document . getElementById ( 'columns-content' ) as HTMLDivElement ,
@@ -845,7 +849,12 @@ function renderVisibleLines(): void {
845849
846850 // Use transform for GPU-accelerated positioning (smoother scrolling)
847851 // Note: no right:0 to allow horizontal scroll expansion
848- lineElement . style . cssText = `position:absolute;top:0;left:0;transform:translateY(${ top } px);will-change:transform;white-space:pre;` ;
852+ if ( wordWrapEnabled ) {
853+ // Word wrap mode: use relative positioning for natural flow
854+ lineElement . style . cssText = `position:relative;white-space:pre-wrap;word-break:break-all;` ;
855+ } else {
856+ lineElement . style . cssText = `position:absolute;top:0;left:0;transform:translateY(${ top } px);will-change:transform;white-space:pre;` ;
857+ }
849858
850859 fragment . appendChild ( lineElement ) ;
851860
@@ -863,8 +872,14 @@ function renderVisibleLines(): void {
863872 logContentElement . appendChild ( fragment ) ;
864873
865874 // Set content size for scrolling
866- logContentElement . style . height = `${ virtualHeight } px` ;
867- logContentElement . style . minWidth = `${ Math . max ( maxContentWidth , logViewerElement . clientWidth ) } px` ;
875+ if ( wordWrapEnabled ) {
876+ // Word wrap mode: let content flow naturally
877+ logContentElement . style . height = 'auto' ;
878+ logContentElement . style . minWidth = '' ;
879+ } else {
880+ logContentElement . style . height = `${ virtualHeight } px` ;
881+ logContentElement . style . minWidth = `${ Math . max ( maxContentWidth , logViewerElement . clientWidth ) } px` ;
882+ }
868883
869884 // Restore horizontal scroll position
870885 if ( scrollLeft > 0 ) {
@@ -1464,17 +1479,23 @@ async function buildMinimap(onProgress?: (percent: number) => void): Promise<voi
14641479function renderMinimap ( ) : void {
14651480 if ( ! minimapContentElement || ! minimapElement ) return ;
14661481
1482+ const totalLines = getTotalLines ( ) ;
1483+ if ( totalLines === 0 || minimapData . length === 0 ) return ;
1484+
14671485 const minimapHeight = minimapElement . clientHeight ;
1468- const lineHeight = Math . max ( 1 , minimapHeight / minimapData . length ) ;
1486+ // Calculate how many actual lines each sample represents
1487+ const linesPerSample = totalLines / minimapData . length ;
1488+ // Each minimap line should take proportional height
1489+ const lineHeight = minimapHeight / minimapData . length ;
14691490
14701491 minimapContentElement . innerHTML = '' ;
14711492
14721493 for ( let i = 0 ; i < minimapData . length ; i ++ ) {
14731494 const data = minimapData [ i ] ;
14741495 const line = document . createElement ( 'div' ) ;
14751496 line . className = `minimap-line level-${ data . level || 'default' } ` ;
1497+ // Use exact height without margin to ensure proper alignment with markers
14761498 line . style . height = `${ Math . max ( 1 , lineHeight ) } px` ;
1477- line . style . marginBottom = lineHeight < 2 ? '0' : '1px' ;
14781499 minimapContentElement . appendChild ( line ) ;
14791500 }
14801501
@@ -3525,6 +3546,27 @@ function resetZoom(): void {
35253546 applyZoom ( ) ;
35263547}
35273548
3549+ function toggleWordWrap ( ) : void {
3550+ wordWrapEnabled = ! wordWrapEnabled ;
3551+
3552+ // Update button state
3553+ if ( wordWrapEnabled ) {
3554+ elements . btnWordWrap . classList . add ( 'active' ) ;
3555+ } else {
3556+ elements . btnWordWrap . classList . remove ( 'active' ) ;
3557+ }
3558+
3559+ if ( logViewerElement ) {
3560+ if ( wordWrapEnabled ) {
3561+ logViewerElement . classList . add ( 'word-wrap' ) ;
3562+ } else {
3563+ logViewerElement . classList . remove ( 'word-wrap' ) ;
3564+ }
3565+ // Re-render to apply word wrap
3566+ renderVisibleLines ( ) ;
3567+ }
3568+ }
3569+
35283570function applyZoom ( ) : void {
35293571 // Update status bar
35303572 elements . statusZoom . textContent = `${ zoomLevel } %` ;
@@ -3768,6 +3810,86 @@ function setupKeyboardShortcuts(): void {
37683810 resetZoom ( ) ;
37693811 }
37703812
3813+ // Arrow key navigation (only when not in input fields)
3814+ const isInputFocused = document . activeElement instanceof HTMLInputElement ||
3815+ document . activeElement instanceof HTMLTextAreaElement ||
3816+ document . activeElement ?. closest ( '.terminal-container' ) ;
3817+
3818+ if ( ! isInputFocused && logViewerElement ) {
3819+ const totalLines = getTotalLines ( ) ;
3820+ const visibleLines = Math . floor ( logViewerElement . clientHeight / getLineHeight ( ) ) ;
3821+
3822+ // Arrow Down: Move down one line
3823+ if ( e . key === 'ArrowDown' ) {
3824+ e . preventDefault ( ) ;
3825+ const newLine = Math . min ( ( state . selectedLine ?? 0 ) + 1 , totalLines - 1 ) ;
3826+ state . selectedLine = newLine ;
3827+ goToLine ( newLine ) ;
3828+ renderVisibleLines ( ) ;
3829+ }
3830+
3831+ // Arrow Up: Move up one line
3832+ if ( e . key === 'ArrowUp' ) {
3833+ e . preventDefault ( ) ;
3834+ const newLine = Math . max ( ( state . selectedLine ?? 0 ) - 1 , 0 ) ;
3835+ state . selectedLine = newLine ;
3836+ goToLine ( newLine ) ;
3837+ renderVisibleLines ( ) ;
3838+ }
3839+
3840+ // Arrow Right: Scroll right
3841+ if ( e . key === 'ArrowRight' ) {
3842+ e . preventDefault ( ) ;
3843+ logViewerElement . scrollLeft += 50 ;
3844+ }
3845+
3846+ // Arrow Left: Scroll left
3847+ if ( e . key === 'ArrowLeft' ) {
3848+ e . preventDefault ( ) ;
3849+ logViewerElement . scrollLeft = Math . max ( 0 , logViewerElement . scrollLeft - 50 ) ;
3850+ }
3851+
3852+ // Page Down: Move down by visible lines
3853+ if ( e . key === 'PageDown' && ! e . ctrlKey && ! e . metaKey ) {
3854+ e . preventDefault ( ) ;
3855+ const newLine = Math . min ( ( state . selectedLine ?? 0 ) + visibleLines , totalLines - 1 ) ;
3856+ state . selectedLine = newLine ;
3857+ goToLine ( newLine ) ;
3858+ renderVisibleLines ( ) ;
3859+ }
3860+
3861+ // Page Up: Move up by visible lines
3862+ if ( e . key === 'PageUp' && ! e . ctrlKey && ! e . metaKey ) {
3863+ e . preventDefault ( ) ;
3864+ const newLine = Math . max ( ( state . selectedLine ?? 0 ) - visibleLines , 0 ) ;
3865+ state . selectedLine = newLine ;
3866+ goToLine ( newLine ) ;
3867+ renderVisibleLines ( ) ;
3868+ }
3869+
3870+ // Home: Go to first line
3871+ if ( e . key === 'Home' && ! e . ctrlKey && ! e . metaKey ) {
3872+ e . preventDefault ( ) ;
3873+ state . selectedLine = 0 ;
3874+ goToLine ( 0 ) ;
3875+ renderVisibleLines ( ) ;
3876+ }
3877+
3878+ // End: Go to last line
3879+ if ( e . key === 'End' && ! e . ctrlKey && ! e . metaKey ) {
3880+ e . preventDefault ( ) ;
3881+ state . selectedLine = totalLines - 1 ;
3882+ goToLine ( totalLines - 1 ) ;
3883+ renderVisibleLines ( ) ;
3884+ }
3885+ }
3886+
3887+ // Alt/Option + Z: Toggle word wrap (use code for Mac compatibility)
3888+ if ( e . altKey && e . code === 'KeyZ' ) {
3889+ e . preventDefault ( ) ;
3890+ toggleWordWrap ( ) ;
3891+ }
3892+
37713893 // Help: F1 or ?
37723894 if ( e . key === 'F1' || ( e . key === '?' && ! e . ctrlKey && ! e . metaKey ) ) {
37733895 e . preventDefault ( ) ;
@@ -3922,6 +4044,9 @@ function init(): void {
39224044 elements . btnColumnsAll . addEventListener ( 'click' , ( ) => setAllColumnsVisibility ( true ) ) ;
39234045 elements . btnColumnsNone . addEventListener ( 'click' , ( ) => setAllColumnsVisibility ( false ) ) ;
39244046
4047+ // Word wrap
4048+ elements . btnWordWrap . addEventListener ( 'click' , toggleWordWrap ) ;
4049+
39254050 // Split mode and value change handlers
39264051 document . querySelectorAll ( 'input[name="split-mode"]' ) . forEach ( ( radio ) => {
39274052 radio . addEventListener ( 'change' , updateSplitPreview ) ;
0 commit comments