package tui import ( "github.com/lixenwraith/terminal" ) // splitLines splits string into lines by newline character func splitLines(s string) []string { if s == "" { return []string{""} } var lines []string start := 0 for i := 0; i < len(s); i++ { if s[i] == '\n' { lines = append(lines, s[start:i]) start = i + 1 } } lines = append(lines, s[start:]) return lines } // EditorState holds multi-line text editor state type EditorState struct { Lines []string CursorLine int CursorCol int ScrollX int ScrollY int ViewportW int // Updated during render ViewportH int // Updated during render } // NewEditorState creates initialized editor state func NewEditorState(initial string) *EditorState { lines := splitLines(initial) if len(lines) == 0 { lines = []string{""} } return &EditorState{ Lines: lines, } } // --- Value access --- // Value returns all lines joined with newlines func (e *EditorState) Value() string { if len(e.Lines) == 0 { return "" } result := e.Lines[0] for i := 1; i < len(e.Lines); i++ { result += "\n" + e.Lines[i] } return result } // SetValue replaces all content and resets cursor func (e *EditorState) SetValue(s string) { e.Lines = splitLines(s) if len(e.Lines) == 0 { e.Lines = []string{""} } e.CursorLine = 0 e.CursorCol = 0 e.ScrollX = 0 e.ScrollY = 0 } // Clear empties the editor func (e *EditorState) Clear() { e.Lines = []string{""} e.CursorLine = 0 e.CursorCol = 0 e.ScrollX = 0 e.ScrollY = 0 } // --- Line queries --- // LineCount returns number of lines func (e *EditorState) LineCount() int { return len(e.Lines) } // CurrentLine returns current line text func (e *EditorState) CurrentLine() string { if e.CursorLine < 0 || e.CursorLine >= len(e.Lines) { return "" } return e.Lines[e.CursorLine] } // --- Cursor clamping --- // clampCursor ensures cursor is within valid bounds func (e *EditorState) clampCursor() { if len(e.Lines) == 0 { e.Lines = []string{""} } if e.CursorLine < 0 { e.CursorLine = 0 } if e.CursorLine >= len(e.Lines) { e.CursorLine = len(e.Lines) - 1 } lineLen := len([]rune(e.Lines[e.CursorLine])) if e.CursorCol < 0 { e.CursorCol = 0 } if e.CursorCol > lineLen { e.CursorCol = lineLen } } // --- Character insertion --- // Insert adds a rune at cursor position func (e *EditorState) Insert(r rune) { e.clampCursor() line := []rune(e.Lines[e.CursorLine]) line = append(line[:e.CursorCol], append([]rune{r}, line[e.CursorCol:]...)...) e.Lines[e.CursorLine] = string(line) e.CursorCol++ } // InsertString adds string at cursor, handling newlines func (e *EditorState) InsertString(s string) { for _, r := range s { if r == '\n' { e.InsertNewline() } else { e.Insert(r) } } } // InsertNewline splits current line at cursor func (e *EditorState) InsertNewline() { e.clampCursor() runes := []rune(e.Lines[e.CursorLine]) before := string(runes[:e.CursorCol]) after := string(runes[e.CursorCol:]) e.Lines[e.CursorLine] = before e.Lines = append(e.Lines[:e.CursorLine+1], append([]string{after}, e.Lines[e.CursorLine+1:]...)...) e.CursorLine++ e.CursorCol = 0 } // --- Character deletion --- // DeleteBackward deletes character before cursor or merges lines func (e *EditorState) DeleteBackward() bool { e.clampCursor() if e.CursorCol > 0 { line := []rune(e.Lines[e.CursorLine]) line = append(line[:e.CursorCol-1], line[e.CursorCol:]...) e.Lines[e.CursorLine] = string(line) e.CursorCol-- return true } if e.CursorLine > 0 { prevLine := e.Lines[e.CursorLine-1] curLine := e.Lines[e.CursorLine] newCol := len([]rune(prevLine)) e.Lines[e.CursorLine-1] = prevLine + curLine e.Lines = append(e.Lines[:e.CursorLine], e.Lines[e.CursorLine+1:]...) e.CursorLine-- e.CursorCol = newCol return true } return false } // DeleteForward deletes character at cursor or merges with next line func (e *EditorState) DeleteForward() bool { e.clampCursor() line := []rune(e.Lines[e.CursorLine]) if e.CursorCol < len(line) { line = append(line[:e.CursorCol], line[e.CursorCol+1:]...) e.Lines[e.CursorLine] = string(line) return true } if e.CursorLine < len(e.Lines)-1 { e.Lines[e.CursorLine] = e.Lines[e.CursorLine] + e.Lines[e.CursorLine+1] e.Lines = append(e.Lines[:e.CursorLine+1], e.Lines[e.CursorLine+2:]...) return true } return false } // --- Word deletion --- // DeleteWordBackward deletes word before cursor func (e *EditorState) DeleteWordBackward() bool { e.clampCursor() if e.CursorCol == 0 { return e.DeleteBackward() } line := []rune(e.Lines[e.CursorLine]) end := e.CursorCol // Skip trailing non-word chars for end > 0 && !isWordChar(line[end-1]) { end-- } // Skip word chars start := end for start > 0 && isWordChar(line[start-1]) { start-- } if start == e.CursorCol { start = e.CursorCol - 1 } line = append(line[:start], line[e.CursorCol:]...) e.Lines[e.CursorLine] = string(line) e.CursorCol = start return true } // DeleteWordForward deletes word after cursor func (e *EditorState) DeleteWordForward() bool { e.clampCursor() line := []rune(e.Lines[e.CursorLine]) if e.CursorCol >= len(line) { return e.DeleteForward() } start := e.CursorCol end := start // Skip word chars for end < len(line) && isWordChar(line[end]) { end++ } // Skip trailing non-word chars for end < len(line) && !isWordChar(line[end]) { end++ } if end == start { end = start + 1 } line = append(line[:start], line[end:]...) e.Lines[e.CursorLine] = string(line) return true } // Line deletion // DeleteToEndOfLine deletes from cursor to end of line func (e *EditorState) DeleteToEndOfLine() bool { e.clampCursor() line := []rune(e.Lines[e.CursorLine]) if e.CursorCol < len(line) { e.Lines[e.CursorLine] = string(line[:e.CursorCol]) return true } // At end of line - merge with next return e.DeleteForward() } // DeleteToStartOfLine deletes from start to cursor func (e *EditorState) DeleteToStartOfLine() bool { e.clampCursor() if e.CursorCol > 0 { line := []rune(e.Lines[e.CursorLine]) e.Lines[e.CursorLine] = string(line[e.CursorCol:]) e.CursorCol = 0 return true } return false } // DeleteLine removes current line func (e *EditorState) DeleteLine() bool { if len(e.Lines) == 1 { e.Lines[0] = "" e.CursorCol = 0 return true } e.Lines = append(e.Lines[:e.CursorLine], e.Lines[e.CursorLine+1:]...) e.clampCursor() return true } // --- Line navigation --- // MoveUp moves cursor to previous line func (e *EditorState) MoveUp() { if e.CursorLine > 0 { e.CursorLine-- e.clampCursor() } } // MoveDown moves cursor to next line func (e *EditorState) MoveDown() { if e.CursorLine < len(e.Lines)-1 { e.CursorLine++ e.clampCursor() } } // --- Character navigation --- // MoveLeft moves cursor left, wrapping to previous line func (e *EditorState) MoveLeft() { if e.CursorCol > 0 { e.CursorCol-- } else if e.CursorLine > 0 { e.CursorLine-- e.CursorCol = len([]rune(e.Lines[e.CursorLine])) } } // MoveRight moves cursor right, wrapping to next line func (e *EditorState) MoveRight() { lineLen := len([]rune(e.Lines[e.CursorLine])) if e.CursorCol < lineLen { e.CursorCol++ } else if e.CursorLine < len(e.Lines)-1 { e.CursorLine++ e.CursorCol = 0 } } // --- Word navigation --- // MoveWordLeft moves cursor to previous word boundary func (e *EditorState) MoveWordLeft() { if e.CursorCol == 0 { if e.CursorLine > 0 { e.CursorLine-- e.CursorCol = len([]rune(e.Lines[e.CursorLine])) } return } line := []rune(e.Lines[e.CursorLine]) for e.CursorCol > 0 && !isWordChar(line[e.CursorCol-1]) { e.CursorCol-- } for e.CursorCol > 0 && isWordChar(line[e.CursorCol-1]) { e.CursorCol-- } } // MoveWordRight moves cursor to next word boundary func (e *EditorState) MoveWordRight() { line := []rune(e.Lines[e.CursorLine]) lineLen := len(line) if e.CursorCol >= lineLen { if e.CursorLine < len(e.Lines)-1 { e.CursorLine++ e.CursorCol = 0 } return } for e.CursorCol < lineLen && isWordChar(line[e.CursorCol]) { e.CursorCol++ } for e.CursorCol < lineLen && !isWordChar(line[e.CursorCol]) { e.CursorCol++ } } // --- Positions navigation --- // MoveToLineStart moves cursor to start of line func (e *EditorState) MoveToLineStart() { e.CursorCol = 0 } // MoveToLineEnd moves cursor to end of line func (e *EditorState) MoveToLineEnd() { e.CursorCol = len([]rune(e.Lines[e.CursorLine])) } // MoveToStart moves cursor to start of document func (e *EditorState) MoveToStart() { e.CursorLine = 0 e.CursorCol = 0 } // MoveToEnd moves cursor to end of document func (e *EditorState) MoveToEnd() { e.CursorLine = len(e.Lines) - 1 e.CursorCol = len([]rune(e.Lines[e.CursorLine])) } // --- Page navigation --- // PageUp moves cursor up by half viewport func (e *EditorState) PageUp() { delta := e.ViewportH / 2 if delta < 1 { delta = 1 } e.CursorLine -= delta if e.CursorLine < 0 { e.CursorLine = 0 } e.clampCursor() } // PageDown moves cursor down by half viewport func (e *EditorState) PageDown() { delta := e.ViewportH / 2 if delta < 1 { delta = 1 } e.CursorLine += delta if e.CursorLine >= len(e.Lines) { e.CursorLine = len(e.Lines) - 1 } e.clampCursor() } // --- Scroll management --- // AdjustScroll updates scroll to keep cursor visible func (e *EditorState) AdjustScroll(viewportW, viewportH int) { e.ViewportW = viewportW e.ViewportH = viewportH // Vertical if e.CursorLine < e.ScrollY { e.ScrollY = e.CursorLine } if e.CursorLine >= e.ScrollY+viewportH { e.ScrollY = e.CursorLine - viewportH + 1 } if e.ScrollY < 0 { e.ScrollY = 0 } // Horizontal if e.CursorCol < e.ScrollX { e.ScrollX = e.CursorCol } if e.CursorCol >= e.ScrollX+viewportW { e.ScrollX = e.CursorCol - viewportW + 1 } if e.ScrollX < 0 { e.ScrollX = 0 } } // --- Input handling --- // HandleKey processes keyboard input, returns true if state changed func (e *EditorState) HandleKey(key terminal.Key, r rune, mod terminal.Modifier) bool { switch key { case terminal.KeyUp: e.MoveUp() return true case terminal.KeyDown: e.MoveDown() return true case terminal.KeyLeft: if mod&terminal.ModCtrl != 0 { e.MoveWordLeft() } else { e.MoveLeft() } return true case terminal.KeyRight: if mod&terminal.ModCtrl != 0 { e.MoveWordRight() } else { e.MoveRight() } return true case terminal.KeyHome: if mod&terminal.ModCtrl != 0 { e.MoveToStart() } else { e.MoveToLineStart() } return true case terminal.KeyEnd: if mod&terminal.ModCtrl != 0 { e.MoveToEnd() } else { e.MoveToLineEnd() } return true case terminal.KeyPageUp: e.PageUp() return true case terminal.KeyPageDown: e.PageDown() return true case terminal.KeyEnter: e.InsertNewline() return true case terminal.KeyBackspace: if mod&terminal.ModCtrl != 0 { return e.DeleteWordBackward() } return e.DeleteBackward() case terminal.KeyDelete: if mod&terminal.ModCtrl != 0 { return e.DeleteWordForward() } return e.DeleteForward() case terminal.KeyCtrlA: e.MoveToLineStart() return true case terminal.KeyCtrlE: e.MoveToLineEnd() return true case terminal.KeyCtrlK: return e.DeleteToEndOfLine() case terminal.KeyCtrlU: return e.DeleteToStartOfLine() case terminal.KeyCtrlW: return e.DeleteWordBackward() case terminal.KeyRune: if r >= 32 { e.Insert(r) return true } } return false }