Skip to content

Commit 6cb8193

Browse files
committed
updated editor panel code overflow string wrapper
1 parent 26ef953 commit 6cb8193

3 files changed

Lines changed: 147 additions & 42 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Variables
44
BINARY_NAME=ti
5-
VERSION=0.0.1.7
5+
VERSION=0.0.2.1
66
BUILD_DIR=build
77
GO=go
88
BUILD_NUMBER=$(shell printf "%04d" $$(($(shell git rev-list --count HEAD 2>/dev/null || echo "0") + 1)))

internal/ui/editor.go

Lines changed: 132 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

tests/property/ui_properties_test.go

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ func TestProperty_UnsavedChangesTracking(t *testing.T) {
131131
properties.TestingRun(t, gopter.ConsoleReporter(false))
132132
}
133133

134-
135134
// Feature: Terminal Intelligence (TI), Property 14: Line Numbers Display
136135
// **Validates: Requirements 6.4**
137136
//
@@ -198,18 +197,16 @@ func containsLineNumbers(view string) bool {
198197
return len(view) > 0 && strings.Contains(view, " │ ")
199198
}
200199

201-
202200
// genFileExtension generates file extensions for supported file types
203201
func genFileExtension() gopter.Gen {
204202
return gen.OneConstOf(
205-
".sh", // bash
206-
".bash", // bash
207-
".ps1", // powershell
208-
".md", // markdown
203+
".sh", // bash
204+
".bash", // bash
205+
".ps1", // powershell
206+
".md", // markdown
209207
)
210208
}
211209

212-
213210
// Feature: Terminal Intelligence (TI), Property 13: Terminal Resize Proportional Adjustment
214211
// **Validates: Requirements 6.3**
215212
//
@@ -236,9 +233,9 @@ func TestProperty_TerminalResizeProportionalAdjustment(t *testing.T) {
236233
editorPane := app.GetEditorPane()
237234
aiPane := app.GetAIPane()
238235

239-
expectedEditorWidth := (width / 2) + 4
240-
expectedAIWidth := (width / 2) - 1
241-
expectedPaneHeight := height - 7 // Account for header (3 lines), editor title (3 lines), and status bar (1 line)
236+
expectedEditorWidth := (width / 2) + 2
237+
expectedAIWidth := width + 2 - expectedEditorWidth
238+
expectedPaneHeight := height - 7 // Account for header (3 lines), editor title (3 lines), and status bar (1 line)
242239

243240
// Verify editor pane size matches formula
244241
if editorPane.GetWidth() != expectedEditorWidth {
@@ -314,12 +311,12 @@ func TestProperty_ActivePaneVisualIndication(t *testing.T) {
314311
// Editor should be focused, AI should not
315312
editorView := editorPane.View()
316313
aiView := aiPane.View()
317-
314+
318315
// Both views should exist
319316
if len(editorView) == 0 || len(aiView) == 0 {
320317
return true // Skip if views are empty
321318
}
322-
319+
323320
// Views should be different (one focused, one not)
324321
if editorView == aiView {
325322
t.Logf("Editor and AI views should be different when one is focused")
@@ -329,12 +326,12 @@ func TestProperty_ActivePaneVisualIndication(t *testing.T) {
329326
// AI should be focused, Editor should not
330327
editorView := editorPane.View()
331328
aiView := aiPane.View()
332-
329+
333330
// Both views should exist
334331
if len(editorView) == 0 || len(aiView) == 0 {
335332
return true // Skip if views are empty
336333
}
337-
334+
338335
// Views should be different (one focused, one not)
339336
if editorView == aiView {
340337
t.Logf("Editor and AI views should be different when one is focused")
@@ -359,9 +356,9 @@ func TestProperty_ActivePaneVisualIndication(t *testing.T) {
359356

360357
return true
361358
},
362-
gen.IntRange(20, 300), // Terminal width range
363-
gen.IntRange(10, 100), // Terminal height range
364-
gen.Bool(), // Start with editor or AI pane
359+
gen.IntRange(20, 300), // Terminal width range
360+
gen.IntRange(10, 100), // Terminal height range
361+
gen.Bool(), // Start with editor or AI pane
365362
))
366363

367364
properties.TestingRun(t, gopter.ConsoleReporter(false))

0 commit comments

Comments
 (0)