feat: highlighting implemented! No tests yet
This commit is contained in:
parent
f2496f91dd
commit
f0f3f95e7b
@ -9,6 +9,9 @@ const (
|
|||||||
NormalMode Mode = iota
|
NormalMode Mode = iota
|
||||||
InsertMode
|
InsertMode
|
||||||
CommandMode
|
CommandMode
|
||||||
|
VisualMode
|
||||||
|
VisualLineMode
|
||||||
|
VisualBlockMode
|
||||||
)
|
)
|
||||||
|
|
||||||
// Model defines the interface for editor state that actions can modify
|
// Model defines the interface for editor state that actions can modify
|
||||||
@ -28,6 +31,12 @@ type Model interface {
|
|||||||
SetCursorY(y int)
|
SetCursorY(y int)
|
||||||
ClampCursorX()
|
ClampCursorX()
|
||||||
|
|
||||||
|
// Anchor
|
||||||
|
AnchorX() int
|
||||||
|
AnchorY() int
|
||||||
|
SetAnchorX(x int)
|
||||||
|
SetAnchorY(y int)
|
||||||
|
|
||||||
// Mode
|
// Mode
|
||||||
Mode() Mode
|
Mode() Mode
|
||||||
SetMode(mode Mode)
|
SetMode(mode Mode)
|
||||||
|
|||||||
@ -16,3 +16,33 @@ func (a EnterComandMode) Execute(m Model) tea.Cmd {
|
|||||||
m.SetMode(CommandMode)
|
m.SetMode(CommandMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quit implements Action (v)
|
||||||
|
type EnterVisualMode struct{}
|
||||||
|
|
||||||
|
func (a EnterVisualMode) Execute(m Model) tea.Cmd {
|
||||||
|
m.SetAnchorX(m.CursorX())
|
||||||
|
m.SetAnchorY(m.CursorY())
|
||||||
|
m.SetMode(VisualMode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit implements Action (V)
|
||||||
|
type EnterVisualLineMode struct{}
|
||||||
|
|
||||||
|
func (a EnterVisualLineMode) Execute(m Model) tea.Cmd {
|
||||||
|
m.SetAnchorX(m.CursorX())
|
||||||
|
m.SetAnchorY(m.CursorY())
|
||||||
|
m.SetMode(VisualLineMode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit implements Action (ctrl+v)
|
||||||
|
type EnterVisualBlockMode struct{}
|
||||||
|
|
||||||
|
func (a EnterVisualBlockMode) Execute(m Model) tea.Cmd {
|
||||||
|
m.SetAnchorX(m.CursorX())
|
||||||
|
m.SetAnchorY(m.CursorY())
|
||||||
|
m.SetMode(VisualBlockMode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ type cursor struct {
|
|||||||
type Model struct {
|
type Model struct {
|
||||||
lines []string
|
lines []string
|
||||||
cursor cursor
|
cursor cursor
|
||||||
|
anchor cursor // starting point for visual modes
|
||||||
mode action.Mode
|
mode action.Mode
|
||||||
win_h int
|
win_h int
|
||||||
win_w int
|
win_w int
|
||||||
@ -106,6 +107,23 @@ func (m *Model) SetCursorY(y int) {
|
|||||||
m.cursor.y = y
|
m.cursor.y = y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Anchor methods
|
||||||
|
func (m *Model) AnchorX() int {
|
||||||
|
return m.anchor.x
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) AnchorY() int {
|
||||||
|
return m.anchor.y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) SetAnchorX(x int) {
|
||||||
|
m.anchor.x = x
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) SetAnchorY(y int) {
|
||||||
|
m.anchor.y = y
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) ClampCursorX() {
|
func (m *Model) ClampCursorX() {
|
||||||
lineLen := len(m.lines[m.cursor.y])
|
lineLen := len(m.lines[m.cursor.y])
|
||||||
if lineLen == 0 {
|
if lineLen == 0 {
|
||||||
|
|||||||
@ -7,7 +7,10 @@ import (
|
|||||||
|
|
||||||
func (m Model) cursorStyle() lipgloss.Style {
|
func (m Model) cursorStyle() lipgloss.Style {
|
||||||
switch m.mode {
|
switch m.mode {
|
||||||
case action.NormalMode:
|
case action.NormalMode,
|
||||||
|
action.VisualMode,
|
||||||
|
action.VisualBlockMode,
|
||||||
|
action.VisualLineMode:
|
||||||
// Block cursor for normal mode
|
// Block cursor for normal mode
|
||||||
return lipgloss.NewStyle().Reverse(true)
|
return lipgloss.NewStyle().Reverse(true)
|
||||||
case action.InsertMode:
|
case action.InsertMode:
|
||||||
@ -20,13 +23,25 @@ func (m Model) cursorStyle() lipgloss.Style {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DEBUGGING STYLE
|
||||||
|
func (m Model) visualAnchorStyle() lipgloss.Style {
|
||||||
|
bg := lipgloss.Color("#a89020")
|
||||||
|
return lipgloss.NewStyle().Background(bg)
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) gutterStyle(currentLine bool) lipgloss.Style {
|
func (m Model) gutterStyle(currentLine bool) lipgloss.Style {
|
||||||
|
bg := lipgloss.Color("236")
|
||||||
fg := lipgloss.Color("243")
|
fg := lipgloss.Color("243")
|
||||||
if currentLine {
|
if currentLine {
|
||||||
fg = lipgloss.Color("#d69d00")
|
fg = lipgloss.Color("#d69d00")
|
||||||
}
|
}
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
Width(m.gutterSize).
|
Width(m.gutterSize).
|
||||||
Background(lipgloss.Color("236")).
|
Background(bg).
|
||||||
Foreground(fg)
|
Foreground(fg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) visualHighlightStyle() lipgloss.Style {
|
||||||
|
bg := lipgloss.Color("#7a6a00")
|
||||||
|
return lipgloss.NewStyle().Background(bg)
|
||||||
|
}
|
||||||
|
|||||||
@ -13,8 +13,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.win_w = msg.Width
|
m.win_w = msg.Width
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
if msg.String() == "ctrl+c" {
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
switch m.mode {
|
switch m.mode {
|
||||||
case action.NormalMode:
|
case action.NormalMode,
|
||||||
|
action.VisualMode,
|
||||||
|
action.VisualBlockMode,
|
||||||
|
action.VisualLineMode:
|
||||||
return m, m.input.Handle(&m, msg.String())
|
return m, m.input.Handle(&m, msg.String())
|
||||||
|
|
||||||
// TODO: This should be handled elsewhere
|
// TODO: This should be handled elsewhere
|
||||||
|
|||||||
@ -7,6 +7,61 @@ import (
|
|||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func posInsideSelection(m Model, col, line int) bool {
|
||||||
|
switch m.Mode() {
|
||||||
|
case action.VisualLineMode:
|
||||||
|
startY := min(m.AnchorY(), m.CursorY())
|
||||||
|
endY := max(m.AnchorY(), m.CursorY())
|
||||||
|
return line >= startY && line <= endY
|
||||||
|
|
||||||
|
case action.VisualMode:
|
||||||
|
ax := m.AnchorX()
|
||||||
|
ay := m.AnchorY()
|
||||||
|
|
||||||
|
cx := m.CursorX()
|
||||||
|
cy := m.CursorY()
|
||||||
|
|
||||||
|
// Normalize so start is always before end in document order
|
||||||
|
var startX, startY, endX, endY int
|
||||||
|
if ay < cy || (ay == cy && ax <= cx) {
|
||||||
|
startX, startY = ax, ay
|
||||||
|
endX, endY = cx, cy
|
||||||
|
} else {
|
||||||
|
startX, startY = cx, cy
|
||||||
|
endX, endY = ax, ay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position is inside if it falls within [start, end] inclusive
|
||||||
|
afterStart := line > startY || (line == startY && col >= startX)
|
||||||
|
beforeEnd := line < endY || (line == endY && col <= endX)
|
||||||
|
return afterStart && beforeEnd
|
||||||
|
|
||||||
|
case action.VisualBlockMode:
|
||||||
|
startX := min(m.AnchorX(), m.CursorX())
|
||||||
|
startY := min(m.AnchorY(), m.CursorY())
|
||||||
|
endX := max(m.AnchorX(), m.CursorX())
|
||||||
|
endY := max(m.AnchorY(), m.CursorY())
|
||||||
|
|
||||||
|
return col >= startX && col <= endX &&
|
||||||
|
line >= startY && line <= endY
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func posIsAnchor(m Model, col, line int) bool {
|
||||||
|
ax := m.AnchorX()
|
||||||
|
ay := m.AnchorY()
|
||||||
|
return col == ax && line == ay
|
||||||
|
}
|
||||||
|
|
||||||
|
func isVisualMode(m action.Mode) bool {
|
||||||
|
return m == action.VisualMode ||
|
||||||
|
m == action.VisualLineMode ||
|
||||||
|
m == action.VisualBlockMode
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
var view strings.Builder
|
var view strings.Builder
|
||||||
|
|
||||||
@ -45,7 +100,16 @@ func (m Model) View() string {
|
|||||||
view.WriteString(m.cursorStyle().Render(" "))
|
view.WriteString(m.cursorStyle().Render(" "))
|
||||||
}
|
}
|
||||||
} else if x < len(runes) {
|
} else if x < len(runes) {
|
||||||
view.WriteRune(runes[x])
|
if isVisualMode(m.Mode()) && posIsAnchor(m, x, y) {
|
||||||
|
view.WriteString(m.visualAnchorStyle().Render(string(runes[x])))
|
||||||
|
} else if isVisualMode(m.Mode()) && posInsideSelection(m, x, y) {
|
||||||
|
view.WriteString(m.visualHighlightStyle().Render(string(runes[x])))
|
||||||
|
} else {
|
||||||
|
view.WriteRune(runes[x])
|
||||||
|
}
|
||||||
|
// To highlight blank lines when in visual mode
|
||||||
|
} else if isVisualMode(m.Mode()) && posInsideSelection(m, x, y) {
|
||||||
|
view.WriteString(m.visualHighlightStyle().Render(" "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -65,11 +129,20 @@ func (m Model) View() string {
|
|||||||
modeString = "INSERT"
|
modeString = "INSERT"
|
||||||
case action.CommandMode:
|
case action.CommandMode:
|
||||||
modeString = "COMMAND"
|
modeString = "COMMAND"
|
||||||
|
case action.VisualMode:
|
||||||
|
modeString = "VISUAL"
|
||||||
|
case action.VisualLineMode:
|
||||||
|
modeString = "V-LINE"
|
||||||
|
case action.VisualBlockMode:
|
||||||
|
modeString = "V-BLOCK"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DEBUG BAR! Def not the final bar
|
||||||
var bar string
|
var bar string
|
||||||
if m.mode == action.CommandMode {
|
if m.Mode() == action.CommandMode {
|
||||||
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) :%s ", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.command)
|
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) :%s ", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.command)
|
||||||
|
} else if isVisualMode(m.Mode()) {
|
||||||
|
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) <%d, %d> ", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.AnchorX(), m.AnchorY())
|
||||||
} else {
|
} else {
|
||||||
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) | %s | %+v | %d", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.input.Pending(), m.insertKeys, m.insertCount)
|
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) | %s | %+v | %d", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.input.Pending(), m.insertKeys, m.insertCount)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package input
|
package input
|
||||||
|
|
||||||
import (
|
import (
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InputState int
|
type InputState int
|
||||||
@ -27,19 +27,29 @@ type Handler struct {
|
|||||||
operatorKey string // track which key started operator (for dd, yy, cc)
|
operatorKey string // track which key started operator (for dd, yy, cc)
|
||||||
buffer string // for display (what user has typed)
|
buffer string // for display (what user has typed)
|
||||||
pending string // partial key sequence (e.g., "g" waiting for second key)
|
pending string // partial key sequence (e.g., "g" waiting for second key)
|
||||||
keymap *Keymap
|
|
||||||
|
// Keymaps
|
||||||
|
normalKeymap *Keymap
|
||||||
|
visualKeymap *Keymap
|
||||||
|
|
||||||
|
currentKeymap *Keymap
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler() *Handler {
|
func NewHandler() *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
keymap: NewNormalKeymap(),
|
// keymap: NewNormalKeymap(),
|
||||||
|
normalKeymap: NewNormalKeymap(),
|
||||||
|
visualKeymap: NewVisualKeymap(),
|
||||||
|
currentKeymap: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||||
// ESC always resets everything
|
// ESC always resets everything
|
||||||
|
// TODO: This should prob be relocated
|
||||||
if key == "esc" {
|
if key == "esc" {
|
||||||
h.Reset()
|
h.Reset()
|
||||||
|
m.SetMode(action.NormalMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,8 +61,19 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
|||||||
// Build the sequence (pending + new key)
|
// Build the sequence (pending + new key)
|
||||||
sequence := h.pending + key
|
sequence := h.pending + key
|
||||||
|
|
||||||
|
// Set working keymap
|
||||||
|
// TODO: Do we need to reset anywhere?
|
||||||
|
switch m.Mode() {
|
||||||
|
case action.NormalMode:
|
||||||
|
h.currentKeymap = h.normalKeymap
|
||||||
|
case action.VisualMode,
|
||||||
|
action.VisualLineMode,
|
||||||
|
action.VisualBlockMode:
|
||||||
|
h.currentKeymap = h.visualKeymap
|
||||||
|
}
|
||||||
|
|
||||||
// Check for exact match with full sequence
|
// Check for exact match with full sequence
|
||||||
kind, binding := h.keymap.Lookup(sequence)
|
kind, binding := h.currentKeymap.Lookup(sequence)
|
||||||
if kind != "" {
|
if kind != "" {
|
||||||
h.pending = ""
|
h.pending = ""
|
||||||
h.buffer += key
|
h.buffer += key
|
||||||
@ -60,7 +81,7 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No exact match - could this be a prefix of something?
|
// No exact match - could this be a prefix of something?
|
||||||
if h.keymap.HasPrefix(sequence) {
|
if h.currentKeymap.HasPrefix(sequence) {
|
||||||
h.pending = sequence
|
h.pending = sequence
|
||||||
h.buffer += key
|
h.buffer += key
|
||||||
return nil // wait for more keys
|
return nil // wait for more keys
|
||||||
@ -69,7 +90,7 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
|||||||
// Not a prefix either - if we had pending, try just the new key
|
// Not a prefix either - if we had pending, try just the new key
|
||||||
if h.pending != "" {
|
if h.pending != "" {
|
||||||
h.pending = ""
|
h.pending = ""
|
||||||
kind, binding = h.keymap.Lookup(key)
|
kind, binding = h.currentKeymap.Lookup(key)
|
||||||
if kind != "" {
|
if kind != "" {
|
||||||
h.buffer = key
|
h.buffer = key
|
||||||
return h.dispatch(m, kind, binding, key)
|
return h.dispatch(m, kind, binding, key)
|
||||||
|
|||||||
@ -31,6 +31,9 @@ func NewNormalKeymap() *Keymap {
|
|||||||
// "d": DeleteOp{},
|
// "d": DeleteOp{},
|
||||||
// "c": ChangeOp{},
|
// "c": ChangeOp{},
|
||||||
// "y": YankOp{},
|
// "y": YankOp{},
|
||||||
|
// "p": PasteOp{},
|
||||||
|
// "s": SubstitueOp{},
|
||||||
|
// "~": SwapCaseOp{},
|
||||||
},
|
},
|
||||||
actions: map[string]action.Action{
|
actions: map[string]action.Action{
|
||||||
"i": action.EnterInsert{},
|
"i": action.EnterInsert{},
|
||||||
@ -42,6 +45,41 @@ func NewNormalKeymap() *Keymap {
|
|||||||
"x": action.DeleteChar{Count: 1},
|
"x": action.DeleteChar{Count: 1},
|
||||||
"ctrl+c": action.Quit{},
|
"ctrl+c": action.Quit{},
|
||||||
":": action.EnterComandMode{},
|
":": action.EnterComandMode{},
|
||||||
|
"v": action.EnterVisualMode{},
|
||||||
|
"V": action.EnterVisualLineMode{},
|
||||||
|
"ctrl+v": action.EnterVisualBlockMode{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVisualKeymap() *Keymap {
|
||||||
|
return &Keymap{
|
||||||
|
motions: map[string]action.Motion{
|
||||||
|
"j": motion.MoveDown{Count: 1},
|
||||||
|
"k": motion.MoveUp{Count: 1},
|
||||||
|
"h": motion.MoveLeft{Count: 1},
|
||||||
|
"l": motion.MoveRight{Count: 1},
|
||||||
|
"G": motion.MoveToBottom{},
|
||||||
|
"gg": motion.MoveToTop{},
|
||||||
|
"0": motion.MoveToLineStart{},
|
||||||
|
"$": motion.MoveToLineEnd{},
|
||||||
|
"_": motion.MoveToLineContentStart{},
|
||||||
|
"w": motion.MoveForwardWord{Count: 1},
|
||||||
|
"e": motion.MoveForwardWordEnd{Count: 1},
|
||||||
|
"b": motion.MoveBackwardWord{Count: 1},
|
||||||
|
},
|
||||||
|
operators: map[string]action.Operator{
|
||||||
|
// "d": DeleteOp{},
|
||||||
|
// "c": ChangeOp{},
|
||||||
|
// "y": YankOp{},
|
||||||
|
// "p": PasteOp{},
|
||||||
|
// "s": SubstitueOp{},
|
||||||
|
// "x": DeleteOp{},
|
||||||
|
// "~": SwapCaseOp{},
|
||||||
|
},
|
||||||
|
actions: map[string]action.Action{
|
||||||
|
"ctrl+c": action.Quit{},
|
||||||
|
// ":": action.EnterComandMode{}, // Different OP
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user