feat: highlighting implemented! No tests yet
This commit is contained in:
parent
f2496f91dd
commit
f0f3f95e7b
@ -9,6 +9,9 @@ const (
|
||||
NormalMode Mode = iota
|
||||
InsertMode
|
||||
CommandMode
|
||||
VisualMode
|
||||
VisualLineMode
|
||||
VisualBlockMode
|
||||
)
|
||||
|
||||
// Model defines the interface for editor state that actions can modify
|
||||
@ -28,6 +31,12 @@ type Model interface {
|
||||
SetCursorY(y int)
|
||||
ClampCursorX()
|
||||
|
||||
// Anchor
|
||||
AnchorX() int
|
||||
AnchorY() int
|
||||
SetAnchorX(x int)
|
||||
SetAnchorY(y int)
|
||||
|
||||
// Mode
|
||||
Mode() Mode
|
||||
SetMode(mode Mode)
|
||||
|
||||
@ -16,3 +16,33 @@ func (a EnterComandMode) Execute(m Model) tea.Cmd {
|
||||
m.SetMode(CommandMode)
|
||||
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 {
|
||||
lines []string
|
||||
cursor cursor
|
||||
anchor cursor // starting point for visual modes
|
||||
mode action.Mode
|
||||
win_h int
|
||||
win_w int
|
||||
@ -106,6 +107,23 @@ func (m *Model) SetCursorY(y int) {
|
||||
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() {
|
||||
lineLen := len(m.lines[m.cursor.y])
|
||||
if lineLen == 0 {
|
||||
|
||||
@ -7,7 +7,10 @@ import (
|
||||
|
||||
func (m Model) cursorStyle() lipgloss.Style {
|
||||
switch m.mode {
|
||||
case action.NormalMode:
|
||||
case action.NormalMode,
|
||||
action.VisualMode,
|
||||
action.VisualBlockMode,
|
||||
action.VisualLineMode:
|
||||
// Block cursor for normal mode
|
||||
return lipgloss.NewStyle().Reverse(true)
|
||||
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 {
|
||||
bg := lipgloss.Color("236")
|
||||
fg := lipgloss.Color("243")
|
||||
if currentLine {
|
||||
fg = lipgloss.Color("#d69d00")
|
||||
}
|
||||
return lipgloss.NewStyle().
|
||||
Width(m.gutterSize).
|
||||
Background(lipgloss.Color("236")).
|
||||
Background(bg).
|
||||
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
|
||||
|
||||
case tea.KeyMsg:
|
||||
if msg.String() == "ctrl+c" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
switch m.mode {
|
||||
case action.NormalMode:
|
||||
case action.NormalMode,
|
||||
action.VisualMode,
|
||||
action.VisualBlockMode,
|
||||
action.VisualLineMode:
|
||||
return m, m.input.Handle(&m, msg.String())
|
||||
|
||||
// TODO: This should be handled elsewhere
|
||||
|
||||
@ -7,6 +7,61 @@ import (
|
||||
"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 {
|
||||
var view strings.Builder
|
||||
|
||||
@ -45,7 +100,16 @@ func (m Model) View() string {
|
||||
view.WriteString(m.cursorStyle().Render(" "))
|
||||
}
|
||||
} 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 {
|
||||
@ -65,11 +129,20 @@ func (m Model) View() string {
|
||||
modeString = "INSERT"
|
||||
case action.CommandMode:
|
||||
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
|
||||
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)
|
||||
} 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 {
|
||||
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
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type InputState int
|
||||
@ -27,19 +27,29 @@ type Handler struct {
|
||||
operatorKey string // track which key started operator (for dd, yy, cc)
|
||||
buffer string // for display (what user has typed)
|
||||
pending string // partial key sequence (e.g., "g" waiting for second key)
|
||||
keymap *Keymap
|
||||
|
||||
// Keymaps
|
||||
normalKeymap *Keymap
|
||||
visualKeymap *Keymap
|
||||
|
||||
currentKeymap *Keymap
|
||||
}
|
||||
|
||||
func NewHandler() *Handler {
|
||||
return &Handler{
|
||||
keymap: NewNormalKeymap(),
|
||||
// keymap: NewNormalKeymap(),
|
||||
normalKeymap: NewNormalKeymap(),
|
||||
visualKeymap: NewVisualKeymap(),
|
||||
currentKeymap: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||
// ESC always resets everything
|
||||
// TODO: This should prob be relocated
|
||||
if key == "esc" {
|
||||
h.Reset()
|
||||
m.SetMode(action.NormalMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -51,8 +61,19 @@ func (h *Handler) Handle(m action.Model, key string) tea.Cmd {
|
||||
// Build the sequence (pending + new 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
|
||||
kind, binding := h.keymap.Lookup(sequence)
|
||||
kind, binding := h.currentKeymap.Lookup(sequence)
|
||||
if kind != "" {
|
||||
h.pending = ""
|
||||
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?
|
||||
if h.keymap.HasPrefix(sequence) {
|
||||
if h.currentKeymap.HasPrefix(sequence) {
|
||||
h.pending = sequence
|
||||
h.buffer += key
|
||||
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
|
||||
if h.pending != "" {
|
||||
h.pending = ""
|
||||
kind, binding = h.keymap.Lookup(key)
|
||||
kind, binding = h.currentKeymap.Lookup(key)
|
||||
if kind != "" {
|
||||
h.buffer = key
|
||||
return h.dispatch(m, kind, binding, key)
|
||||
|
||||
@ -31,6 +31,9 @@ func NewNormalKeymap() *Keymap {
|
||||
// "d": DeleteOp{},
|
||||
// "c": ChangeOp{},
|
||||
// "y": YankOp{},
|
||||
// "p": PasteOp{},
|
||||
// "s": SubstitueOp{},
|
||||
// "~": SwapCaseOp{},
|
||||
},
|
||||
actions: map[string]action.Action{
|
||||
"i": action.EnterInsert{},
|
||||
@ -42,6 +45,41 @@ func NewNormalKeymap() *Keymap {
|
||||
"x": action.DeleteChar{Count: 1},
|
||||
"ctrl+c": action.Quit{},
|
||||
":": 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