From 8c9798c7f684f96f1169c3438efb4b7f057ad228 Mon Sep 17 00:00:00 2001 From: Maas Lalani Date: Wed, 10 Aug 2022 17:01:50 -0400 Subject: [PATCH] feat(textarea): findWordRight / findWordLeft --- textarea/insert.go | 117 ----------- textarea/modal.go | 486 +++++++++++++++++++++++++++++++++++++++++++++ textarea/normal.go | 288 --------------------------- 3 files changed, 486 insertions(+), 405 deletions(-) delete mode 100644 textarea/insert.go delete mode 100644 textarea/normal.go diff --git a/textarea/insert.go b/textarea/insert.go deleted file mode 100644 index 2337ab4..0000000 --- a/textarea/insert.go +++ /dev/null @@ -1,117 +0,0 @@ -package textarea - -import ( - tea "github.com/charmbracelet/bubbletea" - rw "github.com/mattn/go-runewidth" -) - -func (m *Model) insertUpdate(msg tea.Msg) tea.Cmd { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "esc": - return m.SetMode(ModeNormal) - case "ctrl+k": - m.col = clamp(m.col, 0, len(m.value[m.row])) - if m.col >= len(m.value[m.row]) { - m.mergeLineBelow(m.row) - break - } - m.deleteAfterCursor() - case "ctrl+u": - m.col = clamp(m.col, 0, len(m.value[m.row])) - if m.col <= 0 { - m.mergeLineAbove(m.row) - break - } - m.deleteBeforeCursor() - case "backspace", "ctrl+h": - m.col = clamp(m.col, 0, len(m.value[m.row])) - if m.col <= 0 { - m.mergeLineAbove(m.row) - break - } - if len(m.value[m.row]) > 0 { - m.value[m.row] = append(m.value[m.row][:max(0, m.col-1)], m.value[m.row][m.col:]...) - if m.col > 0 { - m.SetCursor(m.col - 1) - } - } - case "delete", "ctrl+d": - if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) { - m.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][m.col+1:]...) - } - if m.col >= len(m.value[m.row]) { - m.mergeLineBelow(m.row) - break - } - case "alt+backspace", "ctrl+w": - if m.col <= 0 { - m.mergeLineAbove(m.row) - break - } - m.deleteWordLeft() - case "alt+delete", "alt+d": - m.col = clamp(m.col, 0, len(m.value[m.row])) - if m.col >= len(m.value[m.row]) { - m.mergeLineBelow(m.row) - break - } - m.deleteWordRight() - case "enter", "ctrl+m": - if len(m.value) >= maxHeight { - return nil - } - m.col = clamp(m.col, 0, len(m.value[m.row])) - m.splitLine(m.row, m.col) - case "end", "ctrl+e": - m.CursorEnd() - case "home", "ctrl+a": - m.CursorStart() - case "right", "ctrl+f": - if m.col < len(m.value[m.row]) { - m.SetCursor(m.col + 1) - } else { - if m.row < len(m.value)-1 { - m.row++ - m.CursorStart() - } - } - case "down", "ctrl+n": - m.CursorDown() - case "alt+right", "alt+f": - m.wordRight() - case "ctrl+v": - return Paste - case "left", "ctrl+b": - if m.col == 0 && m.row != 0 { - m.row-- - m.CursorEnd() - break - } - if m.col > 0 { - m.SetCursor(m.col - 1) - } - case "up", "ctrl+p": - m.CursorUp() - case "alt+left", "alt+b": - m.wordLeft() - default: - if m.CharLimit > 0 && rw.StringWidth(m.Value()) >= m.CharLimit { - break - } - - m.col = min(m.col, len(m.value[m.row])) - m.value[m.row] = append(m.value[m.row][:m.col], append(msg.Runes, m.value[m.row][m.col:]...)...) - m.SetCursor(m.col + len(msg.Runes)) - } - - case pasteMsg: - m.handlePaste(string(msg)) - - case pasteErrMsg: - m.Err = msg - } - - return nil -} diff --git a/textarea/modal.go b/textarea/modal.go index 87bab20..7f60cc9 100644 --- a/textarea/modal.go +++ b/textarea/modal.go @@ -1,8 +1,13 @@ package textarea import ( + "strconv" + "strings" + "unicode" + "github.com/charmbracelet/bubbles/cursor" tea "github.com/charmbracelet/bubbletea" + rw "github.com/mattn/go-runewidth" ) // Mode is the possible modes of the textarea for modal editing. @@ -23,6 +28,7 @@ func (m *Model) SetMode(mode Mode) tea.Cmd { return m.Cursor.SetCursorMode(cursor.CursorBlink) case ModeNormal: m.mode = ModeNormal + m.col = clamp(m.col-1, 0, len(m.value[m.row])) return m.Cursor.SetCursorMode(cursor.CursorStatic) } return nil @@ -75,3 +81,483 @@ type NormalCommand struct { // Range is the range of characters to perform the action on. Range Range } + +func (m *Model) insertUpdate(msg tea.Msg) tea.Cmd { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc": + return m.SetMode(ModeNormal) + case "ctrl+k": + m.col = clamp(m.col, 0, len(m.value[m.row])) + if m.col >= len(m.value[m.row]) { + m.mergeLineBelow(m.row) + break + } + m.deleteAfterCursor() + case "ctrl+u": + m.col = clamp(m.col, 0, len(m.value[m.row])) + if m.col <= 0 { + m.mergeLineAbove(m.row) + break + } + m.deleteBeforeCursor() + case "backspace", "ctrl+h": + m.col = clamp(m.col, 0, len(m.value[m.row])) + if m.col <= 0 { + m.mergeLineAbove(m.row) + break + } + if len(m.value[m.row]) > 0 { + m.value[m.row] = append(m.value[m.row][:max(0, m.col-1)], m.value[m.row][m.col:]...) + if m.col > 0 { + m.SetCursor(m.col - 1) + } + } + case "delete", "ctrl+d": + if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) { + m.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][m.col+1:]...) + } + if m.col >= len(m.value[m.row]) { + m.mergeLineBelow(m.row) + break + } + case "alt+backspace", "ctrl+w": + if m.col <= 0 { + m.mergeLineAbove(m.row) + break + } + m.deleteWordLeft() + case "alt+delete", "alt+d": + m.col = clamp(m.col, 0, len(m.value[m.row])) + if m.col >= len(m.value[m.row]) { + m.mergeLineBelow(m.row) + break + } + m.deleteWordRight() + case "enter", "ctrl+m": + if len(m.value) >= maxHeight { + return nil + } + m.col = clamp(m.col, 0, len(m.value[m.row])) + m.splitLine(m.row, m.col) + case "end", "ctrl+e": + m.CursorEnd() + case "home", "ctrl+a": + m.CursorStart() + case "right", "ctrl+f": + if m.col < len(m.value[m.row]) { + m.SetCursor(m.col + 1) + } else { + if m.row < len(m.value)-1 { + m.row++ + m.CursorStart() + } + } + case "down", "ctrl+n": + m.CursorDown() + case "alt+right", "alt+f": + m.wordRight() + case "ctrl+v": + return Paste + case "left", "ctrl+b": + if m.col == 0 && m.row != 0 { + m.row-- + m.CursorEnd() + break + } + if m.col > 0 { + m.SetCursor(m.col - 1) + } + case "up", "ctrl+p": + m.CursorUp() + case "alt+left", "alt+b": + m.wordLeft() + default: + if m.CharLimit > 0 && rw.StringWidth(m.Value()) >= m.CharLimit { + break + } + + m.col = min(m.col, len(m.value[m.row])) + m.value[m.row] = append(m.value[m.row][:m.col], append(msg.Runes, m.value[m.row][m.col:]...)...) + m.SetCursor(m.col + len(msg.Runes)) + } + + case pasteMsg: + m.handlePaste(string(msg)) + + case pasteErrMsg: + m.Err = msg + } + + return nil +} + +func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd { + var cmd tea.Cmd + var execute bool + + switch msg := msg.(type) { + case tea.KeyMsg: + if m.command.Action == ActionReplace { + for i := m.col; i < m.col+max(m.command.Count, 1); i++ { + if i >= len(m.value[m.row]) || len(msg.Runes) <= 0 { + break + } + m.value[m.row][i] = msg.Runes[0] + } + m.command = &NormalCommand{} + return nil + } + switch msg.String() { + case "esc": + m.command = &NormalCommand{} + return m.SetMode(ModeNormal) + case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": + if m.command.Count == 0 && msg.String() == "0" { + m.command.Range = Range{ + Start: Position{Row: m.row, Col: m.col}, + End: Position{Row: m.row, Col: 0}, + } + execute = true + break + } + + v := m.command.Buffer + msg.String() + count, err := strconv.Atoi(v) + if err != nil { + count, _ = strconv.Atoi(msg.String()) + m.command.Buffer = msg.String() + m.command.Count = count + } else { + m.command.Buffer = v + m.command.Count = count + } + case "G": + var row int + if m.command.Count > 0 { + row = m.command.Count - 1 + } else { + row = len(m.value) - 1 + } + m.row = clamp(row, 0, len(m.value)-1) + return nil + case "g": + if m.command.Buffer == "g" { + m.command = &NormalCommand{} + m.row = clamp(m.command.Count-1, 0, len(m.value)-1) + } else { + m.command = &NormalCommand{Buffer: "g"} + } + return nil + case "x": + m.command.Action = ActionDelete + m.command.Range = Range{ + Start: Position{Row: m.row, Col: m.col}, + End: Position{Row: m.row, Col: m.col + max(m.command.Count, 1)}, + } + case "X": + m.command.Action = ActionDelete + m.command.Range = Range{ + Start: Position{Row: m.row, Col: m.col}, + End: Position{Row: m.row, Col: m.col - max(m.command.Count, 1)}, + } + case "c": + if m.command.Action == ActionChange { + m.CursorStart() + m.deleteAfterCursor() + m.command = &NormalCommand{} + return m.SetMode(ModeInsert) + } + m.command.Action = ActionChange + case "d": + if m.command.Action == ActionDelete { + for i := 0; i < max(m.command.Count, 1); i++ { + m.value[m.row] = []rune{} + if m.row < len(m.value)-1 { + m.mergeLineBelow(m.row) + } else { + m.mergeLineAbove(m.row) + } + } + m.command = &NormalCommand{} + return nil + } + m.command.Action = ActionDelete + case "y": + m.command.Action = ActionYank + case "r": + m.command.Action = ActionReplace + case "i": + m.command.Range = Range{ + Start: Position{Row: m.row, Col: m.col}, + End: Position{Row: m.row, Col: m.col}, + } + cmd = m.SetMode(ModeInsert) + case "I": + m.command.Range = Range{ + Start: Position{Row: m.row, Col: m.col}, + End: Position{Row: m.row, Col: 0}, + } + cmd = m.SetMode(ModeInsert) + case "a": + m.command.Range = Range{ + Start: Position{Row: m.row, Col: m.col}, + End: Position{Row: m.row, Col: m.col + 1}, + } + cmd = m.SetMode(ModeInsert) + case "A": + m.command.Range = Range{ + Start: Position{Row: m.row, Col: m.col}, + End: Position{Row: m.row, Col: len(m.value[m.row]) + 1}, + } + cmd = m.SetMode(ModeInsert) + case "^": + m.command.Range = Range{ + Start: Position{m.row, m.col}, + End: Position{m.row, 0}, + } + case "$": + m.command.Range = Range{ + Start: Position{m.row, m.col}, + End: Position{m.row, len(m.value[m.row])}, + } + case "e", "E": + end := m.findWordEndRight(max(m.command.Count, 1), msg.String() == "E") + if m.command.Action == ActionDelete { + end.Col = min(end.Col+1, len(m.value[end.Row])) + } + m.command.Range = Range{ + Start: Position{m.row, m.col}, + End: end, + } + case "w", "W": + m.command.Range = Range{ + Start: Position{m.row, m.col}, + End: m.findWordStartRight(max(m.command.Count, 1), msg.String() == "W"), + } + case "b", "B": + m.command.Range = Range{ + Start: Position{m.row, m.col}, + End: m.findWordLeft(max(m.command.Count, 1), msg.String() == "B"), + } + case "h", "l": + direction := 1 + if msg.String() == "h" { + direction = -1 + } + m.lastCharOffset = 0 + m.command.Range = Range{ + Start: Position{m.row, m.col}, + End: Position{m.row, clamp(m.col+(direction*max(m.command.Count, 1)), 0, len(m.value[m.row]))}, + } + case "j", "k": + direction := 1 + if msg.String() == "k" { + direction = -1 + } + row := clamp(m.row+(direction*max(m.command.Count, 1)), 0, len(m.value)-1) + li := m.LineInfo() + charOffset := max(m.lastCharOffset, li.CharOffset) + m.lastCharOffset = charOffset + + rowContent := m.value[row] + charWidth := rw.StringWidth(string(rowContent)) + + col := 0 + offset := 0 + + for offset < charOffset { + if col > len(m.value[row]) || offset >= charWidth-1 { + break + } + offset += rw.RuneWidth(m.value[row][col]) + col++ + } + + m.command.Range = Range{ + Start: Position{m.row, m.col}, + End: Position{row, col}, + } + case "C": + m.deleteAfterCursor() + m.command = &NormalCommand{} + return m.SetMode(ModeInsert) + case "D": + m.deleteAfterCursor() + m.command = &NormalCommand{} + return nil + case "J": + m.CursorEnd() + m.mergeLineBelow(m.row) + return nil + case "p": + cmd = Paste + } + + if !strings.ContainsAny(msg.String(), "jk") { + m.lastCharOffset = 0 + } + + if strings.ContainsAny(msg.String(), "iIaAeEwWbBhjklp$^xX") || execute { + switch m.command.Action { + case ActionDelete: + m.deleteRange(m.command.Range) + case ActionChange: + m.deleteRange(m.command.Range) + cmd = m.SetMode(ModeInsert) + case ActionMove: + m.row = clamp(m.command.Range.End.Row, 0, len(m.value)-1) + m.col = clamp(m.command.Range.End.Col, 0, len(m.value[m.row])) + } + m.command = &NormalCommand{} + } + + case pasteMsg: + m.handlePaste(string(msg)) + } + + return cmd +} + +// findWordLeft locates the start of the word on the left of the current word. +// It takes whether or not to break words on spaces or any non-alpha-numeric +// character as an argument. +func (m *Model) findWordLeft(count int, ignorePunctuation bool) Position { + wordBreak := isSoftWordBreak + if ignorePunctuation { + wordBreak = isWordBreak + } + + row, col := m.row, m.col + + for count > 0 { + if col <= 0 && row > 0 { + row-- + col = len(m.value[row]) - 1 + } + + // Skip all spaces (and punctuation) to the left of the cursor. + for col > 0 && wordBreak(m.value[row][col-1]) { + col-- + } + + // Then skip all non-spaces to the left of the cursor. + for col > 0 && !wordBreak(m.value[row][col-1]) { + col-- + } + + count-- + + if row <= 0 && col <= 0 { + break + } + } + + return Position{Row: row, Col: col} +} + +// findWordStartRight locates the start of the next word. It takes whether or not to +// break words on spaces or any non-alpha-numeric character as an argument. +func (m *Model) findWordStartRight(count int, ignorePunctuation bool) Position { + wordBreak := isSoftWordBreak + if ignorePunctuation { + wordBreak = isWordBreak + } + + row, col := m.row, m.col + + for count > 0 { + if col >= len(m.value[row])-1 && row < len(m.value)-1 { + row++ + col = 0 + } + + // Skip until the start of a word is found. + for col < len(m.value[row]) && !wordBreak(m.value[row][col]) { + col++ + } + // Skip all spaces to the right of the cursor. + for col < len(m.value[row])-1 && wordBreak(m.value[row][col]) { + col++ + } + count-- + + if row >= len(m.value)-1 && col >= len(m.value[row])-1 { + break + } + } + + return Position{Row: row, Col: col} +} + +// findWordEndRight locates the start of the next word. It takes whether or not to +// break words on spaces or any non-alpha-numeric character as an argument. +func (m *Model) findWordEndRight(count int, ignorePunctuation bool) Position { + wordBreak := isSoftWordBreak + if ignorePunctuation { + wordBreak = isWordBreak + } + + row, col := m.row, m.col + + for count > 0 { + if col > len(m.value[row]) && row < len(m.value)-1 { + row++ + col = 0 + } + + // Skip all spaces to the right of the cursor. + for col < len(m.value[row])-1 && wordBreak(m.value[row][col+1]) { + col++ + } + + // Then skip all non-spaces to the right of the cursor. + for col < len(m.value[row])-1 && !wordBreak(m.value[row][col+1]) { + col++ + } + + count-- + + if row <= 0 && col <= 0 { + break + } + } + + return Position{Row: row, Col: col} +} + +func isWordBreak(char rune) bool { + return unicode.IsSpace(char) +} + +func isSoftWordBreak(char rune) bool { + return unicode.IsSpace(char) || unicode.IsPunct(char) +} + +func (m *Model) deleteRange(r Range) { + if r.Start.Row == r.End.Row && r.Start.Col == r.End.Col { + return + } + + minCol, maxCol := min(r.Start.Col, r.End.Col), max(r.Start.Col, r.End.Col) + + minCol = clamp(minCol, 0, len(m.value[r.Start.Row])) + maxCol = clamp(maxCol, 0, len(m.value[r.Start.Row])) + + if r.Start.Row == r.End.Row { + m.value[r.Start.Row] = append(m.value[r.Start.Row][:minCol], m.value[r.Start.Row][maxCol:]...) + m.SetCursor(minCol) + return + } + + minRow, maxRow := min(r.Start.Row, r.End.Row), max(r.Start.Row, r.End.Row) + + for i := max(minRow, 0); i <= min(maxRow, len(m.value)-1); i++ { + m.value[i] = []rune{} + } + + m.value = append(m.value[:minRow], m.value[maxRow:]...) + + m.row = clamp(0, minRow, len(m.value)) +} diff --git a/textarea/normal.go b/textarea/normal.go deleted file mode 100644 index cdba62c..0000000 --- a/textarea/normal.go +++ /dev/null @@ -1,288 +0,0 @@ -package textarea - -import ( - "strconv" - "strings" - "unicode" - - tea "github.com/charmbracelet/bubbletea" -) - -func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd { - var cmd tea.Cmd - var execute bool - - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "esc": - m.command = &NormalCommand{} - case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": - if m.command.Count == 0 && msg.String() == "0" { - m.command.Range = Range{ - Start: Position{Row: m.row, Col: m.col}, - End: Position{Row: m.row, Col: 0}, - } - execute = true - break - } - - v := m.command.Buffer + msg.String() - count, err := strconv.Atoi(v) - if err != nil { - count, _ = strconv.Atoi(msg.String()) - m.command.Buffer = msg.String() - m.command.Count = count - } else { - m.command.Buffer = v - m.command.Count = count - } - case "G": - var row int - if m.command.Count > 0 { - row = m.command.Count - 1 - } else { - row = len(m.value) - 1 - } - m.row = clamp(row, 0, len(m.value)-1) - return nil - case "g": - if m.command.Buffer == "g" { - m.command = &NormalCommand{} - m.row = clamp(m.command.Count-1, 0, len(m.value)-1) - } else { - m.command = &NormalCommand{Buffer: "g"} - } - return nil - case "x": - m.command.Action = ActionDelete - m.command.Range = Range{ - Start: Position{Row: m.row, Col: m.col}, - End: Position{Row: m.row, Col: m.col + max(m.command.Count, 1)}, - } - case "X": - m.command.Action = ActionDelete - m.command.Range = Range{ - Start: Position{Row: m.row, Col: m.col}, - End: Position{Row: m.row, Col: m.col - max(m.command.Count, 1)}, - } - case "c": - if m.command.Action == ActionChange { - m.CursorStart() - m.deleteAfterCursor() - m.command = &NormalCommand{} - return m.SetMode(ModeInsert) - } - m.command.Action = ActionChange - case "d": - if m.command.Action == ActionDelete { - for i := 0; i < max(m.command.Count, 1); i++ { - m.value[m.row] = []rune{} - if m.row < len(m.value)-1 { - m.mergeLineBelow(m.row) - } else { - m.mergeLineAbove(m.row) - } - } - m.command = &NormalCommand{} - return nil - } - m.command.Action = ActionDelete - case "y": - m.command.Action = ActionYank - case "r": - m.command.Action = ActionReplace - case "i": - m.command.Range = Range{ - Start: Position{Row: m.row, Col: m.col}, - End: Position{Row: m.row, Col: m.col}, - } - cmd = m.SetMode(ModeInsert) - case "I": - m.command.Range = Range{ - Start: Position{Row: m.row, Col: m.col}, - End: Position{Row: m.row, Col: 0}, - } - cmd = m.SetMode(ModeInsert) - case "a": - m.command.Range = Range{ - Start: Position{Row: m.row, Col: m.col}, - End: Position{Row: m.row, Col: m.col + 1}, - } - cmd = m.SetMode(ModeInsert) - case "A": - m.command.Range = Range{ - Start: Position{Row: m.row, Col: m.col}, - End: Position{Row: m.row, Col: len(m.value[m.row]) + 1}, - } - cmd = m.SetMode(ModeInsert) - case "^": - m.command.Range = Range{ - Start: Position{m.row, m.col}, - End: Position{m.row, 0}, - } - case "$": - m.command.Range = Range{ - Start: Position{m.row, m.col}, - End: Position{m.row, len(m.value[m.row])}, - } - case "e", "E": - m.command.Range = Range{ - Start: Position{m.row, m.col}, - End: m.findWordRight(max(m.command.Count, 1), msg.String() == "E"), - } - case "w", "W": - m.command.Range = Range{ - Start: Position{m.row, m.col}, - End: m.findWordRight(max(m.command.Count, 1), msg.String() == "W"), - } - case "b", "B": - m.command.Range = Range{ - Start: Position{m.row, m.col}, - End: m.findWordLeft(max(m.command.Count, 1), msg.String() == "B"), - } - case "h": - m.command.Range = Range{ - Start: Position{m.row, m.col}, - End: Position{m.row, m.col - max(m.command.Count, 1)}, - } - case "j": - m.command.Range = Range{ - Start: Position{m.row, m.col}, - End: Position{min(m.row+max(m.command.Count, 1), len(m.value)-1), m.col}, - } - case "k": - m.command.Range = Range{ - Start: Position{m.row, m.col}, - End: Position{max(m.row-max(m.command.Count, 1), 0), m.col}, - } - case "l": - m.command.Range = Range{ - Start: Position{m.row, m.col}, - End: Position{m.row, m.col + max(m.command.Count, 1)}, - } - case "C": - m.deleteAfterCursor() - m.command = &NormalCommand{} - return m.SetMode(ModeInsert) - case "D": - m.deleteAfterCursor() - m.command = &NormalCommand{} - return nil - case "J": - m.CursorEnd() - m.mergeLineBelow(m.row) - return nil - case "p": - cmd = Paste - } - - if strings.ContainsAny(msg.String(), "iIaAewWbBhjklp$^xX") || execute { - switch m.command.Action { - case ActionDelete: - m.deleteRange(m.command.Range) - case ActionChange: - m.deleteRange(m.command.Range) - cmd = m.SetMode(ModeInsert) - case ActionMove: - m.row = clamp(m.command.Range.End.Row, 0, len(m.value)-1) - m.col = clamp(m.command.Range.End.Col, 0, len(m.value[m.row])) - } - m.command = &NormalCommand{} - } - - case pasteMsg: - m.handlePaste(string(msg)) - } - - return cmd -} - -// findWordRight locates the end of the current word or the end of the next word -// if already at the end of the current word. It takes whether or not to break -// words on spaces or any non-alpha-numeric character as an argument. -func (m *Model) findWordRight(count int, _ bool) Position { - row, col := m.row, m.col - - for count > 0 { - if col < len(m.value[row])-1 && unicode.IsSpace(m.value[row][col+1]) { - col++ - } else if col >= len(m.value[row])-1 && row < len(m.value)-1 { - row++ - col = 0 - } - - for col < len(m.value[row])-1 { - if !unicode.IsSpace(m.value[row][col+1]) { - col++ - } else { - count-- - break - } - } - - if row >= len(m.value)-1 && col >= len(m.value[row])-1 { - break - } - } - - return Position{Row: row, Col: col} -} - -// findWordLeft locates the start of the next word. It takes whether or not to -// break words on spaces or any non-alpha-numeric character as an argument. -func (m *Model) findWordLeft(count int, onlySpaces bool) Position { - _ = onlySpaces - row, col := m.row, m.col - - for count > 0 { - if col > 0 && unicode.IsSpace(m.value[row][col-1]) { - col-- - } else if col <= 0 && row > 0 { - row-- - col = len(m.value[row]) - 1 - } - - for col > 0 { - if !unicode.IsSpace(m.value[row][col-1]) { - col-- - } else { - count-- - break - } - } - - if row <= 0 && col <= 0 { - break - } - } - - return Position{Row: row, Col: col} -} - -func (m *Model) deleteRange(r Range) { - if r.Start.Row == r.End.Row && r.Start.Col == r.End.Col { - return - } - - minCol, maxCol := min(r.Start.Col, r.End.Col), max(r.Start.Col, r.End.Col) - - minCol = clamp(minCol, 0, len(m.value[r.Start.Row])) - maxCol = clamp(maxCol, 0, len(m.value[r.Start.Row])) - - if r.Start.Row == r.End.Row { - m.value[r.Start.Row] = append(m.value[r.Start.Row][:minCol], m.value[r.Start.Row][maxCol:]...) - m.SetCursor(minCol) - return - } - - minRow, maxRow := min(r.Start.Row, r.End.Row), max(r.Start.Row, r.End.Row) - - for i := max(minRow, 0); i <= min(maxRow, len(m.value)-1); i++ { - m.value[i] = []rune{} - } - - m.value = append(m.value[:minRow], m.value[maxRow:]...) - - m.row = clamp(0, minRow, len(m.value)-1) -}