Compare commits
3 Commits
175ff1daa7
...
6b93fb2f6c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b93fb2f6c | ||
|
|
d5f0f2413a | ||
|
|
be46cae73d |
@ -17,10 +17,8 @@ func generateLines(n int) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
// lines := []string{"Hello world testing main.go", "line 2", "line 3", "line 4", "line 5"}
|
|
||||||
tea.NewProgram(
|
tea.NewProgram(
|
||||||
editor.NewModel(generateLines(32), action.Position{Line: 0, Col: 0}),
|
editor.NewModel(generateLines(64), action.Position{Line: 0, Col: 0}),
|
||||||
tea.WithAltScreen(),
|
tea.WithAltScreen(),
|
||||||
).Run()
|
).Run()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package action
|
package action
|
||||||
|
|
||||||
import tea "github.com/charmbracelet/bubbletea"
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
// Mode constants for editor mode
|
// Mode constants for editor mode
|
||||||
type Mode int
|
type Mode int
|
||||||
@ -14,6 +16,31 @@ const (
|
|||||||
VisualBlockMode
|
VisualBlockMode
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (m Mode) ToString() string {
|
||||||
|
switch m {
|
||||||
|
case NormalMode:
|
||||||
|
return "NORMAL"
|
||||||
|
case InsertMode:
|
||||||
|
return "INSERT"
|
||||||
|
case CommandMode:
|
||||||
|
return "COMMAND"
|
||||||
|
case VisualMode:
|
||||||
|
return "VISUAL"
|
||||||
|
case VisualLineMode:
|
||||||
|
return "V-LINE"
|
||||||
|
case VisualBlockMode:
|
||||||
|
return "V-BLOCK"
|
||||||
|
default:
|
||||||
|
return "-----"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Mode) IsVisualMode() bool {
|
||||||
|
return m == VisualMode ||
|
||||||
|
m == VisualLineMode ||
|
||||||
|
m == VisualBlockMode
|
||||||
|
}
|
||||||
|
|
||||||
// Model defines the interface for editor state that actions can modify
|
// Model defines the interface for editor state that actions can modify
|
||||||
type Model interface {
|
type Model interface {
|
||||||
// Text buffer
|
// Text buffer
|
||||||
@ -46,13 +73,11 @@ type Model interface {
|
|||||||
SetInsertKeys(keys []string)
|
SetInsertKeys(keys []string)
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
TabSize() int
|
Settings() Settings
|
||||||
ScrollOff() int
|
|
||||||
|
|
||||||
// Mode
|
// Mode
|
||||||
Mode() Mode
|
Mode() Mode
|
||||||
SetMode(mode Mode)
|
SetMode(mode Mode)
|
||||||
IsVisualMode() bool
|
|
||||||
|
|
||||||
// Insert recording (for count replay)
|
// Insert recording (for count replay)
|
||||||
SetInsertRecording(count int, action Action)
|
SetInsertRecording(count int, action Action)
|
||||||
|
|||||||
@ -204,7 +204,7 @@ type InsertTab struct{}
|
|||||||
func (a InsertTab) Execute(m Model) tea.Cmd {
|
func (a InsertTab) Execute(m Model) tea.Cmd {
|
||||||
x, y := m.CursorX(), m.CursorY()
|
x, y := m.CursorX(), m.CursorY()
|
||||||
l := m.Line(y)
|
l := m.Line(y)
|
||||||
tabs := strings.Repeat(" ", m.TabSize())
|
tabs := strings.Repeat(" ", m.Settings().TabSize)
|
||||||
if x < len(l) {
|
if x < len(l) {
|
||||||
m.SetLine(y, l[:x]+tabs+l[x:])
|
m.SetLine(y, l[:x]+tabs+l[x:])
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
20
internal/action/settings.go
Normal file
20
internal/action/settings.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
Number bool
|
||||||
|
RelativeNumber bool
|
||||||
|
GutterSize int
|
||||||
|
TabSize int
|
||||||
|
ScrollOff int
|
||||||
|
// TODO: Colors
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultSettings() Settings {
|
||||||
|
return Settings{
|
||||||
|
Number: true,
|
||||||
|
RelativeNumber: true,
|
||||||
|
GutterSize: 5,
|
||||||
|
TabSize: 2,
|
||||||
|
ScrollOff: 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -30,9 +30,7 @@ type Model struct {
|
|||||||
insertAction action.Action
|
insertAction action.Action
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
gutterSize int
|
settings action.Settings
|
||||||
tabSize int
|
|
||||||
scrollOff int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(lines []string, pos action.Position) Model {
|
func NewModel(lines []string, pos action.Position) Model {
|
||||||
@ -42,14 +40,11 @@ func NewModel(lines []string, pos action.Position) Model {
|
|||||||
x: pos.Col,
|
x: pos.Col,
|
||||||
y: pos.Line,
|
y: pos.Line,
|
||||||
},
|
},
|
||||||
scrollY: 0,
|
scrollY: 0,
|
||||||
mode: action.NormalMode,
|
mode: action.NormalMode,
|
||||||
command: "",
|
command: "",
|
||||||
input: input.NewHandler(),
|
input: input.NewHandler(),
|
||||||
|
settings: action.NewDefaultSettings(),
|
||||||
gutterSize: 5,
|
|
||||||
tabSize: 2,
|
|
||||||
scrollOff: 8,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,12 +134,8 @@ func (m *Model) SetInsertKeys(keys []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
func (m *Model) TabSize() int {
|
func (m *Model) Settings() action.Settings {
|
||||||
return m.tabSize
|
return m.settings
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ScrollOff() int {
|
|
||||||
return m.scrollOff
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window
|
// Window
|
||||||
@ -174,7 +165,7 @@ func (m *Model) AdjustScroll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Effective scrollOff (can't be more than half the viewport)
|
// Effective scrollOff (can't be more than half the viewport)
|
||||||
off := min(m.ScrollOff(), viewportHeight/2)
|
off := min(m.Settings().ScrollOff, viewportHeight/2)
|
||||||
|
|
||||||
// Cursor too close to top — scroll up
|
// Cursor too close to top — scroll up
|
||||||
if m.CursorY() < m.ScrollY()+off {
|
if m.CursorY() < m.ScrollY()+off {
|
||||||
@ -199,12 +190,6 @@ func (m *Model) SetMode(mode action.Mode) {
|
|||||||
m.mode = mode
|
m.mode = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) IsVisualMode() bool {
|
|
||||||
return m.mode == action.VisualMode ||
|
|
||||||
m.mode == action.VisualLineMode ||
|
|
||||||
m.mode == action.VisualBlockMode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetInsertRecording(count int, act action.Action) {
|
func (m *Model) SetInsertRecording(count int, act action.Action) {
|
||||||
m.insertCount = count
|
m.insertCount = count
|
||||||
m.insertKeys = []string{}
|
m.insertKeys = []string{}
|
||||||
@ -290,7 +275,7 @@ func (m *Model) processInsertKey(key string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "tab":
|
case "tab":
|
||||||
tabs := strings.Repeat(" ", m.tabSize)
|
tabs := strings.Repeat(" ", m.Settings().TabSize)
|
||||||
if x < len(l) {
|
if x < len(l) {
|
||||||
m.SetLine(y, l[:x]+tabs+l[x:])
|
m.SetLine(y, l[:x]+tabs+l[x:])
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -36,7 +36,7 @@ func (m Model) gutterStyle(currentLine bool) lipgloss.Style {
|
|||||||
fg = lipgloss.Color("#d69d00")
|
fg = lipgloss.Color("#d69d00")
|
||||||
}
|
}
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
Width(m.gutterSize).
|
Width(m.Settings().GutterSize).
|
||||||
Background(bg).
|
Background(bg).
|
||||||
Foreground(fg)
|
Foreground(fg)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,27 +67,39 @@ func (m Model) View() string {
|
|||||||
|
|
||||||
if i < m.LineCount() {
|
if i < m.LineCount() {
|
||||||
|
|
||||||
var (
|
if m.Settings().Number || m.Settings().RelativeNumber {
|
||||||
gutter string
|
var (
|
||||||
currentLine bool = false
|
gutter string
|
||||||
lineNumber int
|
currentLine bool = false
|
||||||
)
|
lineNumber int
|
||||||
if i > m.CursorY() {
|
)
|
||||||
lineNumber = i - m.CursorY()
|
|
||||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber)
|
if m.Settings().RelativeNumber {
|
||||||
} else if i < m.CursorY() {
|
// Relative line numbers: show distance from cursor, current line shows absolute
|
||||||
lineNumber = m.CursorY() - i
|
if i > m.CursorY() {
|
||||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber)
|
lineNumber = i - m.CursorY()
|
||||||
} else {
|
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
lineNumber = i + 1
|
} else if i < m.CursorY() {
|
||||||
currentLine = true
|
lineNumber = m.CursorY() - i
|
||||||
if lineNumber < 100 {
|
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-2, lineNumber)
|
} else {
|
||||||
} else {
|
// Current line: show absolute number if Number is also set, otherwise show 0
|
||||||
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber)
|
currentLine = true
|
||||||
|
if m.Settings().Number {
|
||||||
|
lineNumber = i + 1
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
|
} else {
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if m.Settings().Number {
|
||||||
|
// Absolute line numbers only
|
||||||
|
lineNumber = i + 1
|
||||||
|
currentLine = (i == m.CursorY())
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
}
|
}
|
||||||
|
view.WriteString(m.gutterStyle(currentLine).Render(gutter))
|
||||||
}
|
}
|
||||||
view.WriteString(m.gutterStyle(currentLine).Render(gutter))
|
|
||||||
|
|
||||||
runes := []rune(m.Line(i))
|
runes := []rune(m.Line(i))
|
||||||
for x := 0; x <= len(runes); x++ {
|
for x := 0; x <= len(runes); x++ {
|
||||||
@ -98,54 +110,67 @@ 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) {
|
||||||
if m.IsVisualMode() && posIsAnchor(m, x, i) {
|
if m.Mode().IsVisualMode() && posIsAnchor(m, x, i) {
|
||||||
view.WriteString(m.visualAnchorStyle().Render(string(runes[x])))
|
view.WriteString(m.visualAnchorStyle().Render(string(runes[x])))
|
||||||
} else if m.IsVisualMode() && posInsideSelection(m, x, i) {
|
} else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) {
|
||||||
view.WriteString(m.visualHighlightStyle().Render(string(runes[x])))
|
view.WriteString(m.visualHighlightStyle().Render(string(runes[x])))
|
||||||
} else {
|
} else {
|
||||||
view.WriteRune(runes[x])
|
view.WriteRune(runes[x])
|
||||||
}
|
}
|
||||||
// To highlight blank lines when in visual mode
|
// To highlight blank lines when in visual mode
|
||||||
} else if m.IsVisualMode() && posInsideSelection(m, x, i) {
|
} else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) {
|
||||||
view.WriteString(m.visualHighlightStyle().Render(" "))
|
view.WriteString(m.visualHighlightStyle().Render(" "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
format := fmt.Sprintf("%%-%ds ", m.gutterSize-1)
|
// Empty lines beyond file content
|
||||||
fmt.Fprintf(&view, format, "~")
|
if m.Settings().Number || m.Settings().RelativeNumber {
|
||||||
|
format := fmt.Sprintf("%%-%ds ", m.Settings().GutterSize-1)
|
||||||
|
fmt.Fprintf(&view, format, "~")
|
||||||
|
} else {
|
||||||
|
view.WriteString("~")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view.WriteString("\n")
|
view.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw status bar
|
bar := drawStatusBar(m)
|
||||||
var modeString string
|
|
||||||
switch m.Mode() {
|
|
||||||
case action.NormalMode:
|
|
||||||
modeString = "NORMAL"
|
|
||||||
case action.InsertMode:
|
|
||||||
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 {
|
|
||||||
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 m.IsVisualMode() {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
view.WriteString(bar)
|
view.WriteString(bar)
|
||||||
|
|
||||||
return view.String()
|
return view.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func drawStatusBar(m Model) string {
|
||||||
|
left := leftBar(m)
|
||||||
|
right := rightBar(m)
|
||||||
|
|
||||||
|
diff := m.win_w - (len(left) + len(right))
|
||||||
|
|
||||||
|
// This happens when the terminal spawns
|
||||||
|
if diff <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
middle := strings.Repeat(" ", diff)
|
||||||
|
return left + middle + right
|
||||||
|
}
|
||||||
|
|
||||||
|
func leftBar(m Model) (bar string) {
|
||||||
|
if m.Mode() == action.CommandMode {
|
||||||
|
bar = fmt.Sprintf(":%s", m.command)
|
||||||
|
} else {
|
||||||
|
bar = fmt.Sprintf(" %s", m.Mode().ToString())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func rightBar(m Model) (bar string) {
|
||||||
|
if m.Mode().IsVisualMode() {
|
||||||
|
lineCount := max(m.AnchorY(), m.CursorY()) - min(m.AnchorY(), m.CursorY()) + 1
|
||||||
|
bar = fmt.Sprintf("%d:%d <%d>", m.CursorY(), m.CursorX(), lineCount)
|
||||||
|
} else {
|
||||||
|
bar = fmt.Sprintf("%d:%d ", m.CursorY(), m.CursorX())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@ -139,7 +139,7 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
|
|||||||
case "operator":
|
case "operator":
|
||||||
op := binding.(action.Operator)
|
op := binding.(action.Operator)
|
||||||
// In visual mode, the selection is already defined — operate immediately
|
// In visual mode, the selection is already defined — operate immediately
|
||||||
if m.IsVisualMode() {
|
if m.Mode().IsVisualMode() {
|
||||||
start, end := normalizeVisualSelection(m)
|
start, end := normalizeVisualSelection(m)
|
||||||
// Visual line mode is linewise, others are charwise
|
// Visual line mode is linewise, others are charwise
|
||||||
mtype := action.Charwise
|
mtype := action.Charwise
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user