mirror of
				https://github.com/Maks1mS/bubbles.git
				synced 2025-10-31 05:42:01 +03:00 
			
		
		
		
	feat: delete/change ranges
This commit is contained in:
		| @@ -44,6 +44,8 @@ const ( | ||||
| 	ActionDelete | ||||
| 	// ActionYank yanks text. | ||||
| 	ActionYank | ||||
| 	// ActionChange deletes text and enters insert mode. | ||||
| 	ActionChange | ||||
| ) | ||||
|  | ||||
| // Position is a (row, column) pair representing a position of the cursor or | ||||
|   | ||||
| @@ -2,12 +2,15 @@ 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: | ||||
| @@ -15,13 +18,24 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd { | ||||
| 		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 = &NormalCommand{Buffer: msg.String(), Count: count} | ||||
| 				m.command.Buffer = msg.String() | ||||
| 				m.command.Count = count | ||||
| 			} else { | ||||
| 				m.command = &NormalCommand{Buffer: v, Count: count} | ||||
| 				m.command.Buffer = v | ||||
| 				m.command.Count = count | ||||
| 			} | ||||
| 		case "G": | ||||
| 			var row int | ||||
| @@ -40,15 +54,38 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd { | ||||
| 				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.CursorStart() | ||||
| 					m.deleteAfterCursor() | ||||
| 					m.mergeLineBelow(m.row) | ||||
| 					m.value[m.row] = []rune{} | ||||
| 					if m.row < len(m.value)-1 { | ||||
| 						m.mergeLineBelow(m.row) | ||||
| 					} else { | ||||
| 						m.mergeLineAbove(m.row) | ||||
| 					} | ||||
| 				} | ||||
| 				m.command = &NormalCommand{} | ||||
| 				break | ||||
| 				return nil | ||||
| 			} | ||||
| 			m.command.Action = ActionDelete | ||||
| 		case "y": | ||||
| @@ -56,16 +93,29 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd { | ||||
| 		case "r": | ||||
| 			m.command.Action = ActionReplace | ||||
| 		case "i": | ||||
| 			return m.SetMode(ModeInsert) | ||||
| 			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.CursorStart() | ||||
| 			return m.SetMode(ModeInsert) | ||||
| 			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.SetCursor(m.col + 1) | ||||
| 			return m.SetMode(ModeInsert) | ||||
| 			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.CursorEnd() | ||||
| 			return m.SetMode(ModeInsert) | ||||
| 			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}, | ||||
| @@ -74,23 +124,22 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd { | ||||
| 		case "$": | ||||
| 			m.command.Range = Range{ | ||||
| 				Start: Position{m.row, m.col}, | ||||
| 				End:   Position{m.row, len(m.value[m.row]) - 1}, | ||||
| 				End:   Position{m.row, len(m.value[m.row])}, | ||||
| 			} | ||||
| 		case "e", "E": | ||||
| 			m.command.Range = Range{ | ||||
| 				Start: Position{m.row, m.col}, | ||||
| 				End:   m.findWordEnd(m.command.Count, msg.String() == "E"), | ||||
| 				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.findWordStart(m.command.Count, msg.String() == "W"), | ||||
| 				End:   m.findWordRight(max(m.command.Count, 1), msg.String() == "W"), | ||||
| 			} | ||||
| 		case "b", "B": | ||||
| 			direction := -1 | ||||
| 			m.command.Range = Range{ | ||||
| 				Start: Position{m.row, m.col}, | ||||
| 				End:   m.findWordStart(direction*m.command.Count, msg.String() == "B"), | ||||
| 				End:   m.findWordLeft(max(m.command.Count, 1), msg.String() == "B"), | ||||
| 			} | ||||
| 		case "h": | ||||
| 			m.command.Range = Range{ | ||||
| @@ -100,18 +149,26 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd { | ||||
| 		case "j": | ||||
| 			m.command.Range = Range{ | ||||
| 				Start: Position{m.row, m.col}, | ||||
| 				End:   Position{m.row + max(m.command.Count, 1), 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{m.row - max(m.command.Count, 1), 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) | ||||
| @@ -120,24 +177,16 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd { | ||||
| 			cmd = Paste | ||||
| 		} | ||||
|  | ||||
| 		switch msg.String() { | ||||
| 		case "i", "I", "a", "A", "e", "w", "W", "b", "B", "h", "j", "k", "l", "p", "$", "^": | ||||
| 		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: | ||||
| 				rowDelta := m.command.Range.End.Row - m.command.Range.Start.Row | ||||
| 				if rowDelta > 0 { | ||||
| 					for i := 0; i < rowDelta; i++ { | ||||
| 						m.CursorDown() | ||||
| 					} | ||||
| 				} else if rowDelta < 0 { | ||||
| 					for i := 0; i < -rowDelta; i++ { | ||||
| 						m.CursorUp() | ||||
| 					} | ||||
| 				} else { | ||||
| 					m.SetCursor(m.command.Range.End.Col) | ||||
| 				} | ||||
| 				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{} | ||||
| 		} | ||||
| @@ -148,3 +197,92 @@ func (m *Model) normalUpdate(msg tea.Msg) tea.Cmd { | ||||
|  | ||||
| 	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) | ||||
| } | ||||
|   | ||||
| @@ -572,31 +572,6 @@ func (m *Model) wordRight() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // findWordEnd 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) findWordEnd(count int, onlySpaces bool) Position { | ||||
| 	_ = count | ||||
| 	_ = onlySpaces | ||||
| 	row, col := m.row, m.col | ||||
|  | ||||
| 	for col < len(m.value[row]) { | ||||
| 		if !unicode.IsSpace(m.value[row][col]) { | ||||
| 			col++ | ||||
| 		} else { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return Position{Row: row, Col: col} | ||||
| } | ||||
|  | ||||
| // findWordStart 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) findWordStart(count int, onlySpaces bool) Position { | ||||
| 	return m.findWordEnd(count, onlySpaces) | ||||
| } | ||||
|  | ||||
| // LineInfo returns the number of characters from the start of the | ||||
| // (soft-wrapped) line and the (soft-wrapped) line width. | ||||
| func (m Model) LineInfo() LineInfo { | ||||
| @@ -733,9 +708,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { | ||||
| 	return m, tea.Batch(cmds...) | ||||
| } | ||||
|  | ||||
| func (m *Model) deleteRange(r Range) { | ||||
| } | ||||
|  | ||||
| // View renders the text area in its current state. | ||||
| func (m Model) View() string { | ||||
| 	if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user