@@ -738,6 +738,7 @@ func (e *EditorPane) deleteWord() {
738738}
739739
740740// adjustScroll adjusts the scroll offset to keep cursor visible.
741+ // Accounts for line wrapping.
741742// Scrolls down if cursor is below visible area.
742743// Scrolls up if cursor is above visible area.
743744// Ensures scroll offset is never negative.
@@ -747,14 +748,56 @@ func (e *EditorPane) adjustScroll() {
747748 visibleLines = 1
748749 }
749750
751+ maxLineWidth := e .width - 12
752+ if maxLineWidth < 10 {
753+ maxLineWidth = 10
754+ }
755+
756+ lines := strings .Split (e .content , "\n " )
757+
758+ // Calculate visual line position of cursor
759+ cursorVisualLine := 0
760+ for i := 0 ; i <= e .cursorLine && i < len (lines ); i ++ {
761+ rawLine := lines [i ]
762+ expanded := strings .ReplaceAll (rawLine , "\t " , " " )
763+ runes := []rune (expanded )
764+ lineLen := len (runes )
765+
766+ if lineLen == 0 {
767+ if i == e .cursorLine {
768+ break
769+ }
770+ cursorVisualLine ++
771+ continue
772+ }
773+
774+ chunks := (lineLen + maxLineWidth - 1 ) / maxLineWidth
775+ if chunks == 0 {
776+ chunks = 1
777+ }
778+
779+ if i == e .cursorLine {
780+ col := e .cursorCol
781+ if col > len (rawLine ) {
782+ col = len (rawLine )
783+ }
784+ prefix := strings .ReplaceAll (rawLine [:col ], "\t " , " " )
785+ expandedCol := len ([]rune (prefix ))
786+ cursorVisualLine += expandedCol / maxLineWidth
787+ break
788+ } else {
789+ cursorVisualLine += chunks
790+ }
791+ }
792+
750793 // Scroll down if cursor is below visible area
751- if e . cursorLine >= e .scrollOffset + visibleLines {
752- e .scrollOffset = e . cursorLine - visibleLines + 1
794+ if cursorVisualLine >= e .scrollOffset + visibleLines {
795+ e .scrollOffset = cursorVisualLine - visibleLines + 1
753796 }
754797
755798 // Scroll up if cursor is above visible area
756- if e . cursorLine < e .scrollOffset {
757- e .scrollOffset = e . cursorLine
799+ if cursorVisualLine < e .scrollOffset {
800+ e .scrollOffset = cursorVisualLine
758801 }
759802
760803 // Ensure scroll offset is not negative
@@ -769,7 +812,7 @@ func (e *EditorPane) adjustScroll() {
769812// Rendering details:
770813// - Line numbers: 3-digit format, gray color
771814// - Cursor: Reverse video on current character (or space at end of line)
772- // - Long lines: Truncated with "..." to prevent wrapping
815+ // - Long lines: Wrapped visually to prevent horizontal scrolling
773816// - Empty lines below content: Shown as "~" (vim-style)
774817// - Border: Blue when focused, gray when unfocused
775818//
@@ -786,8 +829,6 @@ func (e *EditorPane) View() string {
786829 visibleLines = 1
787830 }
788831
789- totalLines := len (lines )
790-
791832 // Calculate max line width to prevent wrapping
792833 // Available space:
793834 // Width(e.width - 4) -> -4
@@ -805,36 +846,101 @@ func (e *EditorPane) View() string {
805846 maxLineWidth = 10
806847 }
807848
849+ // Build all visual lines
850+ type visualLine struct {
851+ fileLineIdx int
852+ text string
853+ isContinuation bool
854+ isCursorChunk bool
855+ cursorRelCol int
856+ }
857+ var vLines []visualLine
858+
859+ for fileLineIdx , rawLine := range lines {
860+ expanded := strings .ReplaceAll (rawLine , "\t " , " " )
861+ runes := []rune (expanded )
862+ lineLen := len (runes )
863+
864+ if lineLen == 0 {
865+ vLines = append (vLines , visualLine {
866+ fileLineIdx : fileLineIdx ,
867+ text : "" ,
868+ isContinuation : false ,
869+ isCursorChunk : fileLineIdx == e .cursorLine ,
870+ cursorRelCol : 0 ,
871+ })
872+ continue
873+ }
874+
875+ for start := 0 ; start < lineLen ; start += maxLineWidth {
876+ end := start + maxLineWidth
877+ if end > lineLen {
878+ end = lineLen
879+ }
880+ chunk := string (runes [start :end ])
881+
882+ isCursorChunk := false
883+ relCol := - 1
884+ if fileLineIdx == e .cursorLine {
885+ col := e .cursorCol
886+ if col > len (rawLine ) {
887+ col = len (rawLine )
888+ }
889+ prefix := strings .ReplaceAll (rawLine [:col ], "\t " , " " )
890+ expandedCol := len ([]rune (prefix ))
891+
892+ if expandedCol >= start && (expandedCol < end || (expandedCol == end && end == lineLen )) {
893+ isCursorChunk = true
894+ relCol = expandedCol - start
895+ }
896+ }
897+
898+ vLines = append (vLines , visualLine {
899+ fileLineIdx : fileLineIdx ,
900+ text : chunk ,
901+ isContinuation : start > 0 ,
902+ isCursorChunk : isCursorChunk ,
903+ cursorRelCol : relCol ,
904+ })
905+ }
906+ }
907+
808908 // Render exactly visibleLines lines
809909 var renderedLines []string
810910 for i := 0 ; i < visibleLines ; i ++ {
811- fileLineIdx := e .scrollOffset + i
911+ vIdx := e .scrollOffset + i
912+
913+ if vIdx < len (vLines ) {
914+ vl := vLines [vIdx ]
812915
813- if fileLineIdx < totalLines {
814- lineNum := fileLineIdx + 1
815- lineNumStr := fmt .Sprintf ("%3d" , lineNum )
816- lineNumStyled := lipgloss .NewStyle ().
817- Foreground (lipgloss .Color ("240" )).
818- Render (lineNumStr )
916+ var lineNumStyled string
917+ if ! vl .isContinuation {
918+ lineNumStr := fmt .Sprintf ("%3d" , vl .fileLineIdx + 1 )
919+ lineNumStyled = lipgloss .NewStyle ().Foreground (lipgloss .Color ("240" )).Render (lineNumStr )
920+ } else {
921+ lineNumStyled = lipgloss .NewStyle ().Foreground (lipgloss .Color ("240" )).Render (" " )
922+ }
819923
820- line := lines [ fileLineIdx ]
924+ line := vl . text
821925
822- // Truncate long lines to strictly prevent wrapping
823- if len (line ) > maxLineWidth {
824- line = line [:maxLineWidth - 3 ] + "..."
926+ // Fill the chunk to maxLineWidth so that it doesn't shorten the container border
927+ runeLine := []rune (line )
928+ if len (runeLine ) < maxLineWidth {
929+ line += strings .Repeat (" " , maxLineWidth - len (runeLine ))
825930 }
826931
827- // Highlight cursor line
828- if fileLineIdx == e . cursorLine && e .focused {
932+ // Highlight cursor chunk
933+ if vl . isCursorChunk && e .focused {
829934 cursorStyle := lipgloss .NewStyle ().Reverse (true )
830- if e .cursorCol < len (line ) {
831- line = line [:e .cursorCol ] + cursorStyle .Render (string (line [e .cursorCol ])) + line [e .cursorCol + 1 :]
935+ runeLine = []rune (line ) // re-evaluate after padding
936+ if vl .cursorRelCol < len (runeLine ) {
937+ line = string (runeLine [:vl .cursorRelCol ]) + cursorStyle .Render (string (runeLine [vl .cursorRelCol ])) + string (runeLine [vl .cursorRelCol + 1 :])
832938 } else {
833939 line += cursorStyle .Render (" " )
834940 }
835941 }
836942
837- if color , ok := e .diffMarkers [fileLineIdx ]; ok {
943+ if color , ok := e .diffMarkers [vl . fileLineIdx ]; ok {
838944 if color == "red" {
839945 line = lipgloss .NewStyle ().Foreground (lipgloss .Color ("9" )).Render (line )
840946 } else if color == "green" {
@@ -844,7 +950,9 @@ func (e *EditorPane) View() string {
844950
845951 renderedLines = append (renderedLines , lineNumStyled + " │ " + line )
846952 } else {
847- renderedLines = append (renderedLines , " ~" )
953+ // Ensure empty lines have the appropriate width padding to match content lines
954+ emptyLine := " ~" + strings .Repeat (" " , maxLineWidth + 2 ) // 2 for the space+pipe padding
955+ renderedLines = append (renderedLines , emptyLine )
848956 }
849957 }
850958
0 commit comments