963 lines
22 KiB
Go
963 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"
|
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
|
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 {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{"default"},
|
|
Inline: true,
|
|
IsError: false,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// Theme switching is disabled while migrating away from Chroma.
|
|
name := strings.TrimSpace(strings.Join(args, " "))
|
|
if name == "" {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{"colorscheme not found: "},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
if name != "" && strings.ToLower(name) != "default" {
|
|
m.SetCommandOutput(&core.CommandOutput{
|
|
Lines: []string{fmt.Sprintf("colorscheme not found: %s", name)},
|
|
Inline: true,
|
|
IsError: true,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
m.SetStyles(style.DefaultStyles())
|
|
return nil
|
|
}
|
|
|
|
func cmdListColorschemes(m action.Model, args []string, force bool) tea.Cmd {
|
|
_, _ = args, force
|
|
|
|
colors := []string{"default"}
|
|
|
|
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
|
|
}
|