Now we can load them in via JSON files at launch time. They are embded in the final exe though...
962 lines
22 KiB
Go
962 lines
22 KiB
Go
package command
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
// QuitMsg: Message signaling the application should quit.
|
|
type QuitMsg struct{}
|
|
|
|
// ErrorMsg: Message signaling an error to display.
|
|
type ErrorMsg struct {
|
|
Err error
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
// Quit Commands
|
|
// --------------------------------------------------
|
|
|
|
// cmdQuit: Handles :quit / :q command.
|
|
func cmdQuit(m action.Model, args []string, force bool) tea.Cmd {
|
|
// :q! forces quit, ignoring unsaved changes
|
|
if force {
|
|
return tea.Quit
|
|
}
|
|
|
|
bufs := m.Buffers()
|
|
|
|
// Cannot exit if any buffer has unsaved changes
|
|
for _, buf := range bufs {
|
|
if buf.Modified {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("unsaved changes to '%s'", buf.Filename)},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
m.ActiveWindow().SetBuffer(buf)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return tea.Quit
|
|
}
|
|
|
|
// cmdQuitAll: Handles :qall / :qa command.
|
|
func cmdQuitAll(m action.Model, args []string, force bool) tea.Cmd {
|
|
// TODO: Until splits are implemented, this is the same as cmdQuit
|
|
return cmdQuit(m, args, force)
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
// File Commands (write & edit)
|
|
// --------------------------------------------------
|
|
|
|
// cmdWrite: Handles :write / :w command
|
|
func cmdWrite(m action.Model, args []string, force bool) tea.Cmd {
|
|
buf := m.ActiveBuffer()
|
|
cmd, err := writeBuffer(m, buf, args, force)
|
|
if err != nil {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{err.Error()},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
// cmdWriteAll: Handles :wall / :wa command
|
|
func cmdWriteAll(m action.Model, args []string, force bool) tea.Cmd {
|
|
var cmds []tea.Cmd
|
|
|
|
bufs := m.Buffers()
|
|
for _, buf := range bufs {
|
|
if buf.Modified {
|
|
cmd, err := writeBuffer(m, buf, args, force)
|
|
if err != nil {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{err.Error()},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
}
|
|
|
|
return tea.Batch(cmds...)
|
|
}
|
|
|
|
// cmdWriteQuit: Handles :wq command
|
|
func cmdWriteQuit(m action.Model, args []string, force bool) tea.Cmd {
|
|
buf := m.ActiveBuffer()
|
|
cmd, err := writeBuffer(m, buf, args, force)
|
|
if err != nil {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{err.Error()},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return cmd
|
|
}
|
|
|
|
return tea.Batch(cmd, tea.Quit)
|
|
}
|
|
|
|
// cmdWriteQuitAll: Handles :wqall / :wqa / :xa command.
|
|
// Writes all modified buffers then quits.
|
|
func cmdWriteQuitAll(m action.Model, args []string, force bool) tea.Cmd {
|
|
var cmds []tea.Cmd
|
|
|
|
bufs := m.Buffers()
|
|
for _, buf := range bufs {
|
|
if buf.Modified {
|
|
cmd, err := writeBuffer(m, buf, args, force)
|
|
if err != nil {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{err.Error()},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
}
|
|
|
|
cmds = append(cmds, tea.Quit)
|
|
return tea.Batch(cmds...)
|
|
}
|
|
|
|
// cmdEdit: Handles :edit / :e
|
|
func cmdEdit(m action.Model, args []string, force bool) tea.Cmd {
|
|
// must have arguments, cant edit nothing
|
|
if len(args) < 1 {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{":edit requires an argument"},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// Vim's Approach:
|
|
// " When you do :edit filename.txt
|
|
// 1. Check if file exists and is readable (if not, open new buffer)
|
|
// 2. Detect file encoding (UTF-8, etc.)
|
|
// 3. Read entire file into memory
|
|
// 4. Split by line endings (respecting fileformat)
|
|
// 5. Create new buffer with these lines
|
|
// 6. Set buffer metadata:
|
|
// - buftype = "" (normal file)
|
|
// - modified = 0 (not modified)
|
|
// - fileformat = "unix" | "dos" | "mac"
|
|
// - fileencoding = "utf-8" (etc.)
|
|
|
|
filename := args[0]
|
|
ext := filepath.Ext(filename)
|
|
|
|
// If the buffer already exists, just switch to it.
|
|
bufs := m.Buffers()
|
|
for _, buf := range bufs {
|
|
if buf.Filename == filename {
|
|
m.ActiveWindow().SetBuffer(buf)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
file, err := os.Open(filename)
|
|
notFound := errors.Is(err, os.ErrNotExist)
|
|
|
|
if err != nil && !notFound {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{err.Error()},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
if file != nil {
|
|
defer file.Close()
|
|
}
|
|
|
|
// Create a buffer with the new file name, writing the file will
|
|
// handle the saving logic
|
|
if notFound {
|
|
buf := core.NewBufferBuilder().
|
|
WithType(core.FileBuffer).
|
|
WithFilename(filename).
|
|
WithFiletype(ext).
|
|
Listed().
|
|
Loaded().
|
|
Build()
|
|
|
|
m.SetBuffers(append(m.Buffers(), &buf))
|
|
m.ActiveWindow().SetBuffer(&buf)
|
|
|
|
// Need to adjust the cursor when we make a new file
|
|
m.ActiveWindow().ClampCursor()
|
|
return nil
|
|
}
|
|
|
|
var lines []string
|
|
|
|
// BUG: We are unable to open and edit files owned by root. How do we handle that?
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
line = strings.TrimSuffix(line, "\r")
|
|
|
|
// BUG: This is bad, we don't want to this, but we have to
|
|
cleaned := strings.ReplaceAll(line, "\t", strings.Repeat(" ", m.Settings().TabStop))
|
|
lines = append(lines, cleaned)
|
|
}
|
|
|
|
buf := core.NewBufferBuilder().
|
|
WithType(core.FileBuffer).
|
|
WithFilename(filename).
|
|
WithFiletype(ext).
|
|
WithLines(lines).
|
|
Listed().
|
|
Loaded().
|
|
Build()
|
|
|
|
m.SetBuffers(append(m.Buffers(), &buf))
|
|
m.ActiveWindow().SetBuffer(&buf)
|
|
|
|
return nil
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
// Register Commands
|
|
// --------------------------------------------------
|
|
|
|
// cmdRegisters: Handles :register command
|
|
func cmdRegisters(m action.Model, args []string, force bool) tea.Cmd {
|
|
if len(args) < 1 {
|
|
regs := m.Registers()
|
|
lines := []string{"Type Name Content"}
|
|
|
|
for name, reg := range regs {
|
|
if len(reg.Content) > 0 {
|
|
line := fmt.Sprintf(
|
|
" %s \"%c %s",
|
|
reg.Type.ToString(),
|
|
name,
|
|
strings.Join(reg.Content, "\\n"),
|
|
)
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
|
|
m.SetMode(core.CommandOutputMode)
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Title: ":reg",
|
|
Lines: lines,
|
|
Inline: false,
|
|
IsError: false,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// BUG: We can actually handle many now
|
|
// if len(args[0]) != 1 {
|
|
// m.SetCommandOutput(&core.CommandOutput{
|
|
// Lines: []string{"Name should be a single character."},
|
|
// Inline: true,
|
|
// IsError: true,
|
|
// })
|
|
// return nil
|
|
// }
|
|
|
|
names := []rune(args[0])
|
|
lines := []string{"Type Name Content"}
|
|
|
|
for _, name := range names {
|
|
reg, ok := m.GetRegister(name)
|
|
if ok && len(reg.Content) > 0 {
|
|
line := fmt.Sprintf(
|
|
" %s \"%c %s",
|
|
reg.Type.ToString(),
|
|
name,
|
|
strings.Join(reg.Content, "\\n"),
|
|
)
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
|
|
m.SetMode(core.CommandOutputMode)
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Title: ":reg",
|
|
Lines: lines,
|
|
Inline: false,
|
|
IsError: false,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
// History Commands
|
|
// --------------------------------------------------
|
|
func cmdHistory(m action.Model, args []string, force bool) tea.Cmd {
|
|
_, _ = args, force
|
|
|
|
history := m.CommandHistory()
|
|
reversed := slices.Clone(history)
|
|
slices.Reverse(reversed)
|
|
|
|
m.SetMode(core.CommandOutputMode)
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Title: ":history",
|
|
Lines: reversed,
|
|
Inline: false,
|
|
IsError: false,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
// Buffer Commands
|
|
// --------------------------------------------------
|
|
|
|
// Switching
|
|
// - :b <n> — switch to buffer number n
|
|
// - :b <name> — switch by partial filename match
|
|
// - :bn — next buffer (wraps)
|
|
// - :bp — previous buffer (wraps, currently panics on wrap-back)
|
|
// - :bf — first buffer
|
|
// - :bl — last buffer
|
|
|
|
// Opening / closing
|
|
// - :e <file> — open file into new buffer
|
|
// - :bd — delete (unload) current buffer
|
|
// - :bd <n> — delete buffer n
|
|
// - :bw — wipe buffer completely // TODO: Implement this
|
|
|
|
// cmdListBuffers: Handles :buffers & :ls command. Lists the active buffers.
|
|
func cmdListBuffers(m action.Model, args []string, force bool) tea.Cmd {
|
|
_, _ = args, force
|
|
|
|
// What we should display
|
|
// ------------------------------
|
|
// - % — current buffer
|
|
// - # — alternate buffer (last active)
|
|
// - a — active (loaded and visible)
|
|
// - h — hidden (loaded but not visible)
|
|
// - + — modified (unsaved changes)
|
|
// - - — not modifiable
|
|
|
|
curBuf := m.ActiveBuffer()
|
|
bufs := m.Buffers()
|
|
var lines []string
|
|
for _, buf := range bufs {
|
|
// Skip unlisted buffers
|
|
if !buf.Listed {
|
|
continue
|
|
}
|
|
var flags strings.Builder
|
|
if buf.Id == curBuf.Id {
|
|
flags.WriteRune('%')
|
|
} else {
|
|
flags.WriteRune(' ')
|
|
}
|
|
// TODO: Implement alternate buffer
|
|
|
|
// Cannot really display the a and h, since we don't have visible flags yet
|
|
// For now, we will have a loaded flag, 'l'
|
|
if buf.Loaded {
|
|
flags.WriteRune('l')
|
|
}
|
|
flags.WriteRune(' ')
|
|
if buf.Modified {
|
|
flags.WriteRune('+')
|
|
}
|
|
if buf.ReadOnly {
|
|
flags.WriteRune('-')
|
|
}
|
|
|
|
line := fmt.Sprintf("%3d %s \"%s\"", buf.Id, flags.String(), buf.Filename)
|
|
lines = append(lines, line)
|
|
}
|
|
|
|
m.SetMode(core.CommandOutputMode)
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Title: ":buffers",
|
|
Lines: lines,
|
|
Inline: false,
|
|
IsError: false,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// cmdNextBuffer: Handles :bn command. Moves to the next buffer based on ID.
|
|
func cmdNextBuffer(m action.Model, args []string, force bool) tea.Cmd {
|
|
_, _ = args, force
|
|
|
|
bufs := m.Buffers()
|
|
curBuf := m.ActiveBuffer()
|
|
|
|
ids := make([]int, len(bufs))
|
|
var curIndex int
|
|
for i, buf := range bufs {
|
|
if buf.Listed {
|
|
ids[i] = buf.Id
|
|
if buf.Id == curBuf.Id {
|
|
curIndex = i
|
|
}
|
|
}
|
|
}
|
|
|
|
nextId := (curIndex + 1) % len(ids)
|
|
|
|
m.ActiveWindow().SetBuffer(bufs[nextId])
|
|
|
|
return nil
|
|
}
|
|
|
|
// cmdPrevBuffer: Handles :bp command. Moves to the previous buffer based on ID.
|
|
func cmdPrevBuffer(m action.Model, args []string, force bool) tea.Cmd {
|
|
_, _ = args, force
|
|
|
|
bufs := m.Buffers()
|
|
curBuf := m.ActiveBuffer()
|
|
|
|
ids := make([]int, len(bufs))
|
|
var curIndex int
|
|
for i, buf := range bufs {
|
|
if buf.Listed {
|
|
ids[i] = buf.Id
|
|
if buf.Id == curBuf.Id {
|
|
curIndex = i
|
|
}
|
|
}
|
|
}
|
|
|
|
prevId := ((curIndex - 1) + len(ids)) % len(ids)
|
|
|
|
m.ActiveWindow().SetBuffer(bufs[prevId])
|
|
|
|
return nil
|
|
}
|
|
|
|
// cmdFirstBuffer: Handles :bf command. Moves to the first buffer based on ID.
|
|
func cmdFirstBuffer(m action.Model, args []string, force bool) tea.Cmd {
|
|
_, _ = args, force
|
|
|
|
bufs := m.Buffers()
|
|
|
|
ids := make([]int, len(bufs))
|
|
for i, buf := range bufs {
|
|
if buf.Listed {
|
|
ids[i] = buf.Id
|
|
}
|
|
}
|
|
|
|
m.ActiveWindow().SetBuffer(bufs[0])
|
|
|
|
return nil
|
|
}
|
|
|
|
// cmdLastBuffer: Handles :bf command. Moves to the last buffer based on ID.
|
|
func cmdLastBuffer(m action.Model, args []string, force bool) tea.Cmd {
|
|
_, _ = args, force
|
|
|
|
bufs := m.Buffers()
|
|
|
|
ids := make([]int, len(bufs))
|
|
for i, buf := range bufs {
|
|
if buf.Listed {
|
|
ids[i] = buf.Id
|
|
}
|
|
}
|
|
|
|
m.ActiveWindow().SetBuffer(bufs[len(bufs)-1])
|
|
|
|
return nil
|
|
}
|
|
|
|
// cmdSelectBuffer: Handles :b command. Moves to the selected buffer based on ID or filename.
|
|
func cmdSelectBuffer(m action.Model, args []string, force bool) tea.Cmd {
|
|
_ = force
|
|
|
|
// Cannot function without args
|
|
if len(args) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if len(args) > 1 {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("Trailing characters: %s", strings.Join(args[1:], " "))},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
bufs := m.Buffers()
|
|
|
|
// If we can parse the input as number, try an ID
|
|
tgtId, err := strconv.Atoi(args[0])
|
|
if err == nil {
|
|
for i, buf := range bufs {
|
|
if buf.Id == tgtId && buf.Listed {
|
|
m.ActiveWindow().SetBuffer(bufs[i])
|
|
return nil
|
|
}
|
|
}
|
|
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("Buffer id %d does not exist", tgtId)},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// Otherwise, try to match using filename
|
|
query := args[0]
|
|
var matches []int
|
|
for i, buf := range bufs {
|
|
if strings.Contains(buf.Filename, query) && buf.Listed {
|
|
matches = append(matches, i)
|
|
}
|
|
}
|
|
|
|
if len(matches) == 0 {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("No matches for for %s", query)},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
if len(matches) > 1 {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("More than one match for %s", query)},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
m.ActiveWindow().SetBuffer(bufs[matches[0]])
|
|
|
|
return nil
|
|
}
|
|
|
|
// cmdDeleteBuffer: Handles :bd command. Deletes (unloads) a buffer.
|
|
func cmdDeleteBuffer(m action.Model, args []string, force bool) tea.Cmd {
|
|
// This will be as dynamic as possible, just get a list of indexes, then unlist them all
|
|
var indexes []int
|
|
bufs := m.Buffers()
|
|
|
|
// If the deleted buffer was the active one, Vim switches to the most recent entry in the jump
|
|
// list that points into a loaded buffer. This is not simply "the previous buffer" — it's
|
|
// jump-list-based, so it could be any recently visited loaded buffer.
|
|
// THOUGH: I am not building vim, so it does not have to be the same
|
|
|
|
// Need to close any windows associated with the closed buffers. Once many windows are implemented.
|
|
|
|
// No args, unlist current buffer
|
|
if len(args) == 0 {
|
|
curBuf := m.ActiveBuffer()
|
|
|
|
if curBuf.Modified && !force {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("No write since last change to buffer %d (Add ! to continue)", curBuf.Id)},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
for i, buf := range bufs {
|
|
if buf.Id == curBuf.Id {
|
|
indexes = append(indexes, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(args) > 0 {
|
|
|
|
// Arg can be ID or name
|
|
ArgumentList:
|
|
for _, arg := range args {
|
|
// Try to get ID, if we can, move until we find it
|
|
id, err := strconv.Atoi(arg)
|
|
if err == nil {
|
|
for index, buf := range bufs {
|
|
if buf.Id == id && buf.Listed {
|
|
if buf.Modified && !force {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("No write since last change to buffer %d (Add ! to continue)", buf.Id)},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
indexes = append(indexes, index)
|
|
continue ArgumentList
|
|
}
|
|
}
|
|
continue ArgumentList
|
|
}
|
|
|
|
// Failed to parse, fuzzy match on names
|
|
var matches []int
|
|
for index, buf := range bufs {
|
|
if strings.Contains(buf.Filename, arg) && buf.Listed {
|
|
matches = append(matches, index)
|
|
}
|
|
}
|
|
if len(matches) > 1 {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("More than one match for %s", arg)},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
if len(matches) > 0 {
|
|
|
|
if bufs[matches[0]].Modified && !force {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf(
|
|
"No write since last change to buffer %d (Add ! to continue)",
|
|
bufs[matches[0]].Id),
|
|
},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
indexes = append(indexes, matches[0])
|
|
continue ArgumentList
|
|
}
|
|
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("No matching buffer for %s", arg)},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Simple error output
|
|
if len(indexes) == 0 {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{"No buffers were deleted"},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// Now we can delete the buffers
|
|
for _, i := range indexes {
|
|
bufs[i].SetListed(false)
|
|
}
|
|
|
|
// Switch to first listed buffer
|
|
// TODO: Switch to alternate buffer if available, once implemented
|
|
if !m.ActiveBuffer().Listed {
|
|
for _, buf := range bufs {
|
|
if buf.Listed {
|
|
m.ActiveWindow().SetBuffer(buf)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
// Settings Commands
|
|
// --------------------------------------------------
|
|
|
|
// cmdSet: Handles :set option[=value] command for configuring editor settings.
|
|
// Examples:
|
|
//
|
|
// :set number - enable number
|
|
// :set nonumber - disable number
|
|
// :set number! - toggle number
|
|
// :set tabstop=4 - set tabstop to 4
|
|
// :set ts=4 - set tabstop to 4 (abbreviation)
|
|
func cmdSet(m action.Model, args []string, force bool) tea.Cmd {
|
|
if len(args) == 0 {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("%+v", m.Settings())},
|
|
Inline: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
for _, arg := range args {
|
|
if err := parseSetOption(m, arg); err != nil {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{err.Error()},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Setting: Represents a configurable editor option.
|
|
type Setting struct {
|
|
Name string
|
|
ShortForm string
|
|
Type SettingType
|
|
Get func(m action.Model) any
|
|
Set func(m action.Model, val any)
|
|
}
|
|
|
|
// SettingType: Enumeration of setting value types.
|
|
type SettingType int
|
|
|
|
const (
|
|
BoolSetting SettingType = iota
|
|
IntSetting
|
|
StringSetting
|
|
)
|
|
|
|
// settingsMap defines all available settings (both global and window-local)
|
|
var settingsMap = []Setting{
|
|
// Global editor settings
|
|
{
|
|
Name: "tabstop",
|
|
ShortForm: "ts",
|
|
Type: IntSetting,
|
|
Get: func(m action.Model) any { return m.Settings().TabStop },
|
|
Set: func(m action.Model, val any) {
|
|
s := m.Settings()
|
|
s.TabStop = val.(int)
|
|
m.SetSettings(s)
|
|
},
|
|
},
|
|
// Window-local settings
|
|
{
|
|
Name: "number",
|
|
ShortForm: "nu",
|
|
Type: BoolSetting,
|
|
Get: func(m action.Model) any { return m.ActiveWindow().Options.Number },
|
|
Set: func(m action.Model, val any) {
|
|
w := m.ActiveWindow()
|
|
o := w.Options
|
|
o.Number = val.(bool)
|
|
w.SetOptions(o)
|
|
},
|
|
},
|
|
{
|
|
Name: "relativenumber",
|
|
ShortForm: "rnu",
|
|
Type: BoolSetting,
|
|
Get: func(m action.Model) any { return m.ActiveWindow().Options.RelativeNumber },
|
|
Set: func(m action.Model, val any) {
|
|
w := m.ActiveWindow()
|
|
o := w.Options
|
|
o.RelativeNumber = val.(bool)
|
|
w.SetOptions(o)
|
|
},
|
|
},
|
|
{
|
|
Name: "scrolloff",
|
|
ShortForm: "so",
|
|
Type: IntSetting,
|
|
Get: func(m action.Model) any { return m.ActiveWindow().Options.ScrollOff },
|
|
Set: func(m action.Model, val any) {
|
|
w := m.ActiveWindow()
|
|
o := w.Options
|
|
o.ScrollOff = val.(int)
|
|
w.SetOptions(o)
|
|
},
|
|
},
|
|
{
|
|
Name: "guttersize",
|
|
ShortForm: "gu",
|
|
Type: IntSetting,
|
|
Get: func(m action.Model) any { return m.ActiveWindow().Options.GutterSize },
|
|
Set: func(m action.Model, val any) {
|
|
w := m.ActiveWindow()
|
|
o := w.Options
|
|
o.GutterSize = val.(int)
|
|
w.SetOptions(o)
|
|
},
|
|
},
|
|
}
|
|
|
|
// lookupSetting: Finds a setting by name, short form, or prefix.
|
|
func lookupSetting(name string) *Setting {
|
|
for i := range settingsMap {
|
|
s := &settingsMap[i]
|
|
if name == s.Name || name == s.ShortForm {
|
|
return s
|
|
}
|
|
// Prefix matching
|
|
if len(name) >= len(s.ShortForm) && strings.HasPrefix(s.Name, name) {
|
|
return s
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseSetOption: Parses and applies a single :set option.
|
|
func parseSetOption(m action.Model, opt string) error {
|
|
// Handle toggle: option!
|
|
if name, ok := strings.CutSuffix(opt, "!"); ok {
|
|
setting := lookupSetting(name)
|
|
if setting != nil && setting.Type == BoolSetting {
|
|
currentVal := setting.Get(m).(bool)
|
|
setting.Set(m, !currentVal)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Handle disable: nooption
|
|
if name, ok := strings.CutPrefix(opt, "no"); ok {
|
|
setting := lookupSetting(name)
|
|
if setting != nil && setting.Type == BoolSetting {
|
|
setting.Set(m, false)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Handle assignment: option=value
|
|
if strings.Contains(opt, "=") {
|
|
parts := strings.SplitN(opt, "=", 2)
|
|
name, value := parts[0], parts[1]
|
|
|
|
setting := lookupSetting(name)
|
|
if setting != nil {
|
|
switch setting.Type {
|
|
case IntSetting:
|
|
intVal, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
setting.Set(m, intVal)
|
|
case StringSetting:
|
|
setting.Set(m, value)
|
|
case BoolSetting:
|
|
// Handle :set option=true / :set option=false
|
|
boolVal := value == "true" || value == "1" || value == "yes"
|
|
setting.Set(m, boolVal)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Handle enable: option (boolean only)
|
|
setting := lookupSetting(opt)
|
|
if setting != nil && setting.Type == BoolSetting {
|
|
setting.Set(m, true)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
// Colorscheme Commands
|
|
// --------------------------------------------------
|
|
|
|
// TODO: Implement this using the new colorschemes
|
|
func cmdColorscheme(m action.Model, args []string, force bool) tea.Cmd {
|
|
_ = force
|
|
|
|
// No args, just print the current scheme
|
|
if len(args) == 0 {
|
|
name, _ := m.Theme()
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{name},
|
|
Inline: true,
|
|
IsError: false,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// Theme switching is disabled while migrating away from Chroma.
|
|
name := strings.TrimSpace(strings.Join(args, " "))
|
|
_, found := m.Themes()[name]
|
|
|
|
if name == "" || !found {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("colorscheme not found: %s", name)},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
m.SetTheme(name)
|
|
return nil
|
|
}
|
|
|
|
func cmdListColorschemes(m action.Model, args []string, force bool) tea.Cmd {
|
|
_, _ = args, force
|
|
|
|
var colors []string
|
|
for k := range m.Themes() {
|
|
colors = append(colors, k)
|
|
}
|
|
|
|
slices.Sort(colors)
|
|
|
|
m.SetMode(core.CommandOutputMode)
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Title: ":colorschemes",
|
|
Lines: colors,
|
|
Inline: false,
|
|
IsError: false,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func cmdUndoList(m action.Model, args []string, force bool) tea.Cmd {
|
|
_, _ = args, force
|
|
|
|
lines := m.ActiveBuffer().UndoStack.List()
|
|
|
|
// For now, display an error when empty
|
|
if len(lines) == 0 {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{"Undo stack is empty"},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
m.SetMode(core.CommandOutputMode)
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Title: ":undo",
|
|
Lines: lines,
|
|
Inline: false,
|
|
IsError: false,
|
|
})
|
|
|
|
return nil
|
|
}
|