Compare commits
3 Commits
edbed61949
...
6b8845b245
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b8845b245 | ||
|
|
d5ad3dcdcd | ||
|
|
10e37b82af |
@ -12,8 +12,7 @@ type ExitCommandMode struct{}
|
|||||||
func (a ExitCommandMode) Execute(m Model) tea.Cmd {
|
func (a ExitCommandMode) Execute(m Model) tea.Cmd {
|
||||||
m.SetCommandCursor(0)
|
m.SetCommandCursor(0)
|
||||||
m.SetCommand("")
|
m.SetCommand("")
|
||||||
m.SetCommandOutput("")
|
m.SetCommandOutput(&core.CommandOutput{})
|
||||||
m.SetCommandError(nil)
|
|
||||||
m.SetMode(core.NormalMode)
|
m.SetMode(core.NormalMode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -128,7 +127,6 @@ func (a CommandExecute) Execute(m Model) tea.Cmd {
|
|||||||
|
|
||||||
// Clear command state and return to normal mode
|
// Clear command state and return to normal mode
|
||||||
m.SetCommandCursor(0)
|
m.SetCommandCursor(0)
|
||||||
m.SetCommandError(nil)
|
|
||||||
m.SetMode(core.NormalMode)
|
m.SetMode(core.NormalMode)
|
||||||
|
|
||||||
if a.Registry == nil || cmdLine == "" {
|
if a.Registry == nil || cmdLine == "" {
|
||||||
@ -137,7 +135,12 @@ func (a CommandExecute) Execute(m Model) tea.Cmd {
|
|||||||
|
|
||||||
cmd, err := a.Registry.Execute(m, cmdLine)
|
cmd, err := a.Registry.Execute(m, cmdLine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.SetCommandError(err)
|
out := core.CommandOutput{
|
||||||
|
Lines: []string{err.Error()},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
m.SetCommandOutput(&out)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,8 +20,7 @@ type mockModel struct {
|
|||||||
insertKeys []string
|
insertKeys []string
|
||||||
command string
|
command string
|
||||||
commandCursor int
|
commandCursor int
|
||||||
commandError error
|
commandOutput *core.CommandOutput
|
||||||
commandOutput string
|
|
||||||
lastFind core.LastFindCommand
|
lastFind core.LastFindCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,10 +95,8 @@ func (m *mockModel) Command() string { return m.command }
|
|||||||
func (m *mockModel) SetCommand(cmd string) { m.command = cmd }
|
func (m *mockModel) SetCommand(cmd string) { m.command = cmd }
|
||||||
func (m *mockModel) CommandCursor() int { return m.commandCursor }
|
func (m *mockModel) CommandCursor() int { return m.commandCursor }
|
||||||
func (m *mockModel) SetCommandCursor(cur int) { m.commandCursor = cur }
|
func (m *mockModel) SetCommandCursor(cur int) { m.commandCursor = cur }
|
||||||
func (m *mockModel) CommandError() error { return m.commandError }
|
func (m *mockModel) CommandOutput() *core.CommandOutput { return m.commandOutput }
|
||||||
func (m *mockModel) SetCommandError(err error) { m.commandError = err }
|
func (m *mockModel) SetCommandOutput(out *core.CommandOutput) { m.commandOutput = out }
|
||||||
func (m *mockModel) CommandOutput() string { return m.commandOutput }
|
|
||||||
func (m *mockModel) SetCommandOutput(out string) { m.commandOutput = out }
|
|
||||||
|
|
||||||
// Editor-wide State
|
// Editor-wide State
|
||||||
func (m *mockModel) Mode() core.Mode { return m.mode }
|
func (m *mockModel) Mode() core.Mode { return m.mode }
|
||||||
|
|||||||
@ -37,10 +37,8 @@ type Model interface {
|
|||||||
SetCommand(cmd string)
|
SetCommand(cmd string)
|
||||||
CommandCursor() int
|
CommandCursor() int
|
||||||
SetCommandCursor(cur int)
|
SetCommandCursor(cur int)
|
||||||
CommandError() error
|
CommandOutput() *core.CommandOutput
|
||||||
SetCommandError(err error)
|
SetCommandOutput(out *core.CommandOutput)
|
||||||
CommandOutput() string
|
|
||||||
SetCommandOutput(out string)
|
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
// Editor-wide State
|
// Editor-wide State
|
||||||
|
|||||||
@ -20,8 +20,7 @@ type EnterComandMode struct{}
|
|||||||
func (a EnterComandMode) Execute(m Model) tea.Cmd {
|
func (a EnterComandMode) Execute(m Model) tea.Cmd {
|
||||||
m.SetMode(core.CommandMode)
|
m.SetMode(core.CommandMode)
|
||||||
m.SetCommand("")
|
m.SetCommand("")
|
||||||
m.SetCommandOutput("")
|
m.SetCommandOutput(&core.CommandOutput{})
|
||||||
m.SetCommandError(nil)
|
|
||||||
m.SetCommandCursor(0)
|
m.SetCommandCursor(0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package action
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
@ -21,7 +20,12 @@ func (a Paste) Execute(m Model) tea.Cmd {
|
|||||||
// Get reg
|
// Get reg
|
||||||
reg, found := m.GetRegister('"')
|
reg, found := m.GetRegister('"')
|
||||||
if !found {
|
if !found {
|
||||||
m.SetCommandError(fmt.Errorf("\"\" register is broken. Uh oh."))
|
out := core.CommandOutput{
|
||||||
|
Lines: []string{"\"\" register is broken. Uh oh."},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
m.SetCommandOutput(&out)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +59,12 @@ func (a Paste) Execute(m Model) tea.Cmd {
|
|||||||
|
|
||||||
// Shouldn't happen, just a check
|
// Shouldn't happen, just a check
|
||||||
if len(lines) != 1 {
|
if len(lines) != 1 {
|
||||||
m.SetCommandError(fmt.Errorf("Charwise register should only have a single line of content."))
|
out := core.CommandOutput{
|
||||||
|
Lines: []string{"Charwise register should only have a single line of content."},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
m.SetCommandOutput(&out)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +82,12 @@ func (a Paste) Execute(m Model) tea.Cmd {
|
|||||||
win.SetCursorCol(x + len(cnt))
|
win.SetCursorCol(x + len(cnt))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
m.SetCommandError(fmt.Errorf("core.Register type is not implemented."))
|
out := core.CommandOutput{
|
||||||
|
Lines: []string{"core.Register type is not implemented."},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
m.SetCommandOutput(&out)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -100,7 +114,12 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
|
|||||||
// Get reg
|
// Get reg
|
||||||
reg, found := m.GetRegister('"')
|
reg, found := m.GetRegister('"')
|
||||||
if !found {
|
if !found {
|
||||||
m.SetCommandError(fmt.Errorf("\"\" register is broken. Uh oh."))
|
out := core.CommandOutput{
|
||||||
|
Lines: []string{"\"\" register is broken. Uh oh."},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
m.SetCommandOutput(&out)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +144,12 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
|
|||||||
|
|
||||||
// Shouldn't happen, just a check
|
// Shouldn't happen, just a check
|
||||||
if len(lines) != 1 {
|
if len(lines) != 1 {
|
||||||
m.SetCommandError(fmt.Errorf("Charwise register should only have a single line of content."))
|
out := core.CommandOutput{
|
||||||
|
Lines: []string{"Charwise register should only have a single line of content."},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
m.SetCommandOutput(&out)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +167,12 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
|
|||||||
win.SetCursorCol(x + len(cnt))
|
win.SetCursorCol(x + len(cnt))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
m.SetCommandError(fmt.Errorf("core.Register type is not implemented."))
|
out := core.CommandOutput{
|
||||||
|
Lines: []string{"core.Register type is not implemented."},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
m.SetCommandOutput(&out)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -167,7 +196,12 @@ func (a VisualPaste) Execute(m Model) tea.Cmd {
|
|||||||
// Get register content to paste
|
// Get register content to paste
|
||||||
reg, found := m.GetRegister('"')
|
reg, found := m.GetRegister('"')
|
||||||
if !found {
|
if !found {
|
||||||
m.SetCommandError(fmt.Errorf("\"\" register is broken. Uh oh."))
|
out := core.CommandOutput{
|
||||||
|
Lines: []string{"\"\" register is broken. Uh oh."},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
m.SetCommandOutput(&out)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,11 @@ func cmdQuit(m action.Model, args []string, force bool) tea.Cmd {
|
|||||||
// Cannot exit if any buffer has unsaved changes
|
// Cannot exit if any buffer has unsaved changes
|
||||||
for _, buf := range bufs {
|
for _, buf := range bufs {
|
||||||
if buf.Modified {
|
if buf.Modified {
|
||||||
m.SetCommandError(fmt.Errorf("unsaved changes to '%s'", buf.Filename))
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Lines: []string{fmt.Sprintf("unsaved changes to '%s'", buf.Filename)},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
})
|
||||||
m.ActiveWindow().SetBuffer(buf)
|
m.ActiveWindow().SetBuffer(buf)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -62,7 +66,11 @@ func cmdWrite(m action.Model, args []string, force bool) tea.Cmd {
|
|||||||
buf := m.ActiveBuffer()
|
buf := m.ActiveBuffer()
|
||||||
cmd, err := writeBuffer(m, buf, args, force)
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.SetCommandError(err)
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Lines: []string{err.Error()},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@ -76,7 +84,11 @@ func cmdWriteAll(m action.Model, args []string, force bool) tea.Cmd {
|
|||||||
if buf.Modified {
|
if buf.Modified {
|
||||||
cmd, err := writeBuffer(m, buf, args, force)
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.SetCommandError(err)
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Lines: []string{err.Error()},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
@ -91,7 +103,11 @@ func cmdWriteQuit(m action.Model, args []string, force bool) tea.Cmd {
|
|||||||
buf := m.ActiveBuffer()
|
buf := m.ActiveBuffer()
|
||||||
cmd, err := writeBuffer(m, buf, args, force)
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.SetCommandError(err)
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Lines: []string{err.Error()},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
})
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +124,11 @@ func cmdWriteQuitAll(m action.Model, args []string, force bool) tea.Cmd {
|
|||||||
if buf.Modified {
|
if buf.Modified {
|
||||||
cmd, err := writeBuffer(m, buf, args, force)
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.SetCommandError(err)
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Lines: []string{err.Error()},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
@ -123,7 +143,11 @@ func cmdWriteQuitAll(m action.Model, args []string, force bool) tea.Cmd {
|
|||||||
func cmdEdit(m action.Model, args []string, force bool) tea.Cmd {
|
func cmdEdit(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
// must have arguments, cant edit nothing
|
// must have arguments, cant edit nothing
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
m.SetCommandError(fmt.Errorf(":edit requires an argument"))
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Lines: []string{":edit requires an argument"},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +180,11 @@ func cmdEdit(m action.Model, args []string, force bool) tea.Cmd {
|
|||||||
notFound := errors.Is(err, os.ErrNotExist)
|
notFound := errors.Is(err, os.ErrNotExist)
|
||||||
|
|
||||||
if err != nil && !notFound {
|
if err != nil && !notFound {
|
||||||
m.SetCommandError(err)
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Lines: []string{err.Error()},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if file != nil {
|
if file != nil {
|
||||||
@ -215,29 +243,427 @@ func cmdEdit(m action.Model, args []string, force bool) tea.Cmd {
|
|||||||
// Register Commands
|
// Register Commands
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
// cmdRegisters: Handles :register command (debug - displays register content).
|
// cmdRegisters: Handles :register command
|
||||||
func cmdRegisters(m action.Model, args []string, force bool) tea.Cmd {
|
func cmdRegisters(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
// TODO: This is temporary, for debugging
|
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
m.SetCommandError(fmt.Errorf("Please provide a name. Register dump not yet implemented."))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(args[0]) != 1 {
|
// BUG: We can actually handle many now
|
||||||
m.SetCommandError(fmt.Errorf("Name should be a single character."))
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
name := rune(args[0][0])
|
// --------------------------------------------------
|
||||||
reg, found := m.GetRegister(name)
|
// Buffer Commands
|
||||||
if !found {
|
// --------------------------------------------------
|
||||||
m.SetCommandError(fmt.Errorf("Could not find register '%c'.", name))
|
|
||||||
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
content := strings.Join(reg.Content, "\\n")
|
// cmdNextBuffer: Handles :bn command. Moves to the next buffer based on ID.
|
||||||
t := reg.Type
|
func cmdNextBuffer(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
m.SetCommandOutput(fmt.Sprintf("Type: %d Name: \"%c Content: %s", t, name, content))
|
_, _ = 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,14 +681,20 @@ func cmdRegisters(m action.Model, args []string, force bool) tea.Cmd {
|
|||||||
// :set ts=4 - set tabstop to 4 (abbreviation)
|
// :set ts=4 - set tabstop to 4 (abbreviation)
|
||||||
func cmdSet(m action.Model, args []string, force bool) tea.Cmd {
|
func cmdSet(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
out := fmt.Sprintf("%+v", m.Settings())
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
m.SetCommandOutput(out)
|
Lines: []string{fmt.Sprintf("%+v", m.Settings())},
|
||||||
|
Inline: true,
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
if err := parseSetOption(m, arg); err != nil {
|
if err := parseSetOption(m, arg); err != nil {
|
||||||
m.SetCommandError(err)
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Lines: []string{err.Error()},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -75,8 +75,10 @@ func writeBuffer(m action.Model, buf *core.Buffer, args []string, force bool) (t
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
output := fmt.Sprintf("'%s', %dL %db written", filename, buf.LineCount(), bytes)
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
m.SetCommandOutput(output)
|
Lines: []string{fmt.Sprintf("'%s', %dL %db written", filename, buf.LineCount(), bytes)},
|
||||||
|
Inline: true,
|
||||||
|
})
|
||||||
buf.SetModified(false)
|
buf.SetModified(false)
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
@ -162,6 +162,55 @@ func (r *Registry) registerDefaults() {
|
|||||||
Handler: cmdRegisters,
|
Handler: cmdRegisters,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Buffer commands
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "buffers",
|
||||||
|
ShortForm: "buffers",
|
||||||
|
Handler: cmdListBuffers,
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "ls",
|
||||||
|
ShortForm: "ls",
|
||||||
|
Handler: cmdListBuffers,
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "bn",
|
||||||
|
ShortForm: "bn",
|
||||||
|
Handler: cmdNextBuffer,
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "bp",
|
||||||
|
ShortForm: "bp",
|
||||||
|
Handler: cmdPrevBuffer,
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "bf",
|
||||||
|
ShortForm: "bf",
|
||||||
|
Handler: cmdFirstBuffer,
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "bl",
|
||||||
|
ShortForm: "bl",
|
||||||
|
Handler: cmdLastBuffer,
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "b",
|
||||||
|
ShortForm: "b",
|
||||||
|
Handler: cmdSelectBuffer,
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "bdelete",
|
||||||
|
ShortForm: "bd",
|
||||||
|
Handler: cmdDeleteBuffer,
|
||||||
|
})
|
||||||
|
|
||||||
// File commands
|
// File commands
|
||||||
r.Register(Command{
|
r.Register(Command{
|
||||||
Name: "edit",
|
Name: "edit",
|
||||||
|
|||||||
39
internal/core/command.go
Normal file
39
internal/core/command.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const CommandOutputExitMessage = "Press ENTER to continue"
|
||||||
|
|
||||||
|
type CommandOutput struct {
|
||||||
|
Title string
|
||||||
|
Lines []string
|
||||||
|
ScrollOffset int // Not implemented yet
|
||||||
|
// Height is computing via lines and title
|
||||||
|
Inline bool // Show inline instead of the window
|
||||||
|
IsError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandOutput.Height: Compute the height (in lines) based on the line count, and title.
|
||||||
|
func (c *CommandOutput) Height() int {
|
||||||
|
if c.Inline {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var h int
|
||||||
|
h += len(c.Lines)
|
||||||
|
if strings.TrimSpace(c.Title) != "" {
|
||||||
|
h++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding:
|
||||||
|
// +1 for 'enter key...' message
|
||||||
|
// +1 for top bar (border)
|
||||||
|
h += 2
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandOutput.IsActive: Is the command output in a state that should be displayed.
|
||||||
|
func (c *CommandOutput) IsActive() bool {
|
||||||
|
return len(c.Lines) > 0
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ const (
|
|||||||
NormalMode Mode = iota
|
NormalMode Mode = iota
|
||||||
InsertMode
|
InsertMode
|
||||||
CommandMode
|
CommandMode
|
||||||
|
CommandOutputMode
|
||||||
VisualMode
|
VisualMode
|
||||||
VisualLineMode
|
VisualLineMode
|
||||||
VisualBlockMode
|
VisualBlockMode
|
||||||
|
|||||||
@ -11,6 +11,18 @@ const (
|
|||||||
BlockwiseRegister
|
BlockwiseRegister
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (r RegisterType) ToString() string {
|
||||||
|
switch r {
|
||||||
|
case CharwiseRegister:
|
||||||
|
return "c"
|
||||||
|
case LinewiseRegister:
|
||||||
|
return "l"
|
||||||
|
case BlockwiseRegister:
|
||||||
|
return "b"
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
// Register: Stores yanked or deleted text with metadata about how it should be
|
// Register: Stores yanked or deleted text with metadata about how it should be
|
||||||
// pasted. The Type determines paste behavior and Content holds the text lines.
|
// pasted. The Type determines paste behavior and Content holds the text lines.
|
||||||
type Register struct {
|
type Register struct {
|
||||||
|
|||||||
@ -302,8 +302,8 @@ func TestCommandModeErrors(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "u", "n", "k", "n", "o", "w", "n", "c", "m", "d", "enter")
|
sendKeys(tm, ":", "u", "n", "k", "n", "o", "w", "n", "c", "m", "d", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.commandError == nil {
|
if m.commandOutput == nil || !m.commandOutput.IsError {
|
||||||
t.Error("expected commandError to be set for unknown command")
|
t.Error("expected commandOutput with IsError to be set for unknown command")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -317,8 +317,8 @@ func TestCommandModeErrors(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.commandError != nil {
|
if m.commandOutput != nil && m.commandOutput.IsError {
|
||||||
t.Errorf("expected commandError to be nil, got %v", m.commandError)
|
t.Errorf("expected no error output, got %v", m.commandOutput.Lines)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -332,8 +332,8 @@ func TestCommandModeErrors(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "esc")
|
sendKeys(tm, ":", "esc")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.commandError != nil {
|
if m.commandOutput != nil && m.commandOutput.IsError {
|
||||||
t.Errorf("expected commandError to be nil after esc, got %v", m.commandError)
|
t.Errorf("expected no error output after esc, got %v", m.commandOutput.Lines)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -345,8 +345,8 @@ func TestCommandEdit(t *testing.T) {
|
|||||||
sendKeys(tm, "enter")
|
sendKeys(tm, "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.commandError == nil {
|
if m.commandOutput == nil || !m.commandOutput.IsError {
|
||||||
t.Error("expected commandError to be set for edit without args")
|
t.Error("expected commandOutput with IsError to be set for edit without args")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,8 +39,7 @@ type Model struct {
|
|||||||
// Command line state
|
// Command line state
|
||||||
command string
|
command string
|
||||||
commandCursor int
|
commandCursor int
|
||||||
commandError error
|
commandOutput *core.CommandOutput
|
||||||
commandOutput string
|
|
||||||
|
|
||||||
// Global settings
|
// Global settings
|
||||||
settings core.EditorSettings
|
settings core.EditorSettings
|
||||||
@ -272,19 +271,11 @@ func (m *Model) SetCommandCursor(cur int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) CommandError() error {
|
func (m *Model) CommandOutput() *core.CommandOutput {
|
||||||
return m.commandError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetCommandError(err error) {
|
|
||||||
m.commandError = err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) CommandOutput() string {
|
|
||||||
return m.commandOutput
|
return m.commandOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) SetCommandOutput(out string) {
|
func (m *Model) SetCommandOutput(out *core.CommandOutput) {
|
||||||
m.commandOutput = out
|
m.commandOutput = out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,8 +25,7 @@ func NewModelBuilder() *ModelBuilder {
|
|||||||
insertAction: nil,
|
insertAction: nil,
|
||||||
command: "",
|
command: "",
|
||||||
commandCursor: 0,
|
commandCursor: 0,
|
||||||
commandError: nil,
|
commandOutput: nil,
|
||||||
commandOutput: "",
|
|
||||||
settings: core.NewDefaultSettings(),
|
settings: core.NewDefaultSettings(),
|
||||||
registers: core.DefaultRegisters(),
|
registers: core.DefaultRegisters(),
|
||||||
styles: style.DefaultStyles(),
|
styles: style.DefaultStyles(),
|
||||||
@ -116,15 +115,9 @@ func (mb *ModelBuilder) WithCommandCursor(cursor int) *ModelBuilder {
|
|||||||
return mb
|
return mb
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModelBuilder.WithCommandError: Set the command line error state.
|
// ModelBuilder.WithCommandOutput: Set the command line output.
|
||||||
func (mb *ModelBuilder) WithCommandError(err error) *ModelBuilder {
|
func (mb *ModelBuilder) WithCommandOutput(out *core.CommandOutput) *ModelBuilder {
|
||||||
mb.model.commandError = err
|
mb.model.commandOutput = out
|
||||||
return mb
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModelBuilder.WithCommandOutput: Set the command line output text.
|
|
||||||
func (mb *ModelBuilder) WithCommandOutput(output string) *ModelBuilder {
|
|
||||||
mb.model.commandOutput = output
|
|
||||||
return mb
|
return mb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package editor
|
package editor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,8 +55,19 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if msg.Type == tea.KeyCtrlC {
|
if msg.Type == tea.KeyCtrlC {
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This is not great
|
||||||
|
// TODO: Any vim action should exit also
|
||||||
|
// Simple override for command output mode for now
|
||||||
|
if m.Mode() == core.CommandOutputMode {
|
||||||
|
if msg.Type == tea.KeyEnter {
|
||||||
|
m.SetMode(core.NormalMode)
|
||||||
|
m.SetCommandOutput(&core.CommandOutput{})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
cmd = m.input.Handle(m, msg.String())
|
cmd = m.input.Handle(m, msg.String())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Keep cursor in view after any update
|
// Keep cursor in view after any update
|
||||||
win := m.ActiveWindow()
|
win := m.ActiveWindow()
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Model.View: Renders the complete editor view including buffer content, line
|
// Model.View: Renders the complete editor view including buffer content, line
|
||||||
@ -26,7 +27,16 @@ func (m Model) View() string {
|
|||||||
|
|
||||||
// Command bar is seperate
|
// Command bar is seperate
|
||||||
cmdBar := drawCommandBar(m)
|
cmdBar := drawCommandBar(m)
|
||||||
return view + cmdBar
|
view += cmdBar
|
||||||
|
|
||||||
|
// Handle command output, draw on top
|
||||||
|
// TODO: This is not idea, but it works for now
|
||||||
|
cmd := m.CommandOutput()
|
||||||
|
if cmd != nil && cmd.IsActive() && !cmd.Inline && cmd.Height() > 0 {
|
||||||
|
view = overlayCommandOutputWindow(view, cmd, styles, m.termWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
// viewWindow: Renders a single window's content including line numbers and buffer text.
|
// viewWindow: Renders a single window's content including line numbers and buffer text.
|
||||||
@ -139,44 +149,6 @@ func drawGutter(w *core.Window, styles style.Styles, options core.WinOptions, cu
|
|||||||
}
|
}
|
||||||
|
|
||||||
return view.String()
|
return view.String()
|
||||||
|
|
||||||
// if m.Settings().Number || m.Settings().RelativeNumber {
|
|
||||||
// var (
|
|
||||||
// gutter string
|
|
||||||
// currentLine bool = false
|
|
||||||
// lineNumber int
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
// if m.Settings().RelativeNumber {
|
|
||||||
// // Relative line numbers: show distance from cursor, current line shows absolute
|
|
||||||
// if i > win.Cursor.Line {
|
|
||||||
// lineNumber = i - win.Cursor.Line
|
|
||||||
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
|
||||||
// } else if i < win.Cursor.Line {
|
|
||||||
// lineNumber = win.Cursor.Line - i
|
|
||||||
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
|
||||||
// } else {
|
|
||||||
// // Current line: show absolute number if Number is also set, otherwise show 0
|
|
||||||
// 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 == win.Cursor.Line)
|
|
||||||
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
|
||||||
// }
|
|
||||||
// if currentLine {
|
|
||||||
// view.WriteString(m.Styles().GutterCurrentLine.Render(gutter))
|
|
||||||
// } else {
|
|
||||||
// view.WriteString(m.Styles().Gutter.Render(gutter))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawStatusBar: Renders the status bar with mode and cursor position,
|
// drawStatusBar: Renders the status bar with mode and cursor position,
|
||||||
@ -185,7 +157,7 @@ func drawStatusBar(w *core.Window, mode core.Mode) string {
|
|||||||
left := leftBar(w, mode)
|
left := leftBar(w, mode)
|
||||||
right := rightBar(w, mode)
|
right := rightBar(w, mode)
|
||||||
|
|
||||||
diff := w.Width - (len(left) + len(right))
|
diff := w.Width - (lipgloss.Width(left) + lipgloss.Width(right))
|
||||||
|
|
||||||
// This happens when the terminal spawns
|
// This happens when the terminal spawns
|
||||||
if diff <= 0 {
|
if diff <= 0 {
|
||||||
@ -237,13 +209,13 @@ func drawCommandBar(m Model) string {
|
|||||||
var leftBar string
|
var leftBar string
|
||||||
if m.Mode() == core.CommandMode {
|
if m.Mode() == core.CommandMode {
|
||||||
leftBar = ":"
|
leftBar = ":"
|
||||||
cmd := m.Command()
|
cmd := []rune(m.Command())
|
||||||
cur := m.CommandCursor()
|
cur := m.CommandCursor()
|
||||||
for i := 0; i < len(cmd); i++ {
|
for i, r := range cmd {
|
||||||
if i == cur {
|
if i == cur {
|
||||||
leftBar += m.Styles().CursorStyle(m.Mode()).Render(string(cmd[i]))
|
leftBar += m.Styles().CursorStyle(m.Mode()).Render(string(r))
|
||||||
} else {
|
} else {
|
||||||
leftBar += string(cmd[i])
|
leftBar += string(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Cursor at end of command
|
// Cursor at end of command
|
||||||
@ -251,10 +223,14 @@ func drawCommandBar(m Model) string {
|
|||||||
leftBar += m.Styles().CursorStyle(m.Mode()).Render(" ")
|
leftBar += m.Styles().CursorStyle(m.Mode()).Render(" ")
|
||||||
}
|
}
|
||||||
// bar = fmt.Sprintf("%s %d", bar, cur)
|
// bar = fmt.Sprintf("%s %d", bar, cur)
|
||||||
} else if m.CommandError() != nil {
|
} else if out := m.CommandOutput(); out != nil && len(out.Lines) > 0 && out.Inline {
|
||||||
leftBar = m.Styles().CommandError.Render(m.CommandError().Error())
|
// TODO: This is not perfect, temporary
|
||||||
} else if strings.TrimSpace(m.CommandOutput()) != "" {
|
text := strings.Join(out.Lines, " ")
|
||||||
leftBar = m.CommandOutput()
|
if out.IsError {
|
||||||
|
leftBar = m.Styles().CommandError.Render(text)
|
||||||
|
} else {
|
||||||
|
leftBar = text
|
||||||
|
}
|
||||||
} else if strings.TrimSpace(m.Command()) != "" {
|
} else if strings.TrimSpace(m.Command()) != "" {
|
||||||
leftBar = fmt.Sprintf(":%s", m.Command())
|
leftBar = fmt.Sprintf(":%s", m.Command())
|
||||||
}
|
}
|
||||||
@ -267,9 +243,9 @@ func drawCommandBar(m Model) string {
|
|||||||
rightBar = fmt.Sprintf("%-*s", width, m.input.Pending())
|
rightBar = fmt.Sprintf("%-*s", width, m.input.Pending())
|
||||||
}
|
}
|
||||||
|
|
||||||
dif := m.termWidth - (len(leftBar) + len(rightBar))
|
dif := m.termWidth - (lipgloss.Width(leftBar) + lipgloss.Width(rightBar))
|
||||||
|
|
||||||
bar := leftBar + strings.Repeat(" ", dif) + rightBar
|
bar := leftBar + strings.Repeat(" ", max(0, dif)) + rightBar
|
||||||
return bar
|
return bar
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,3 +293,38 @@ func posInsideSelection(w *core.Window, mode core.Mode, col, line int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// overlayCommandOutputWindow: Draw the overlay of the command output window. This will override
|
||||||
|
// (overlay) the displayed content, so it should be used only when needed.
|
||||||
|
func overlayCommandOutputWindow(view string, cmd *core.CommandOutput, styles style.Styles, termWidth int) string {
|
||||||
|
// Safety check
|
||||||
|
if cmd == nil {
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the lines and get the last few
|
||||||
|
lines := strings.Split(view, "\n")
|
||||||
|
|
||||||
|
// Build the overlay
|
||||||
|
var overlay []string
|
||||||
|
overlay = append(overlay, styles.CommandOutputBorder.Render(strings.Repeat(" ", termWidth)))
|
||||||
|
|
||||||
|
if strings.TrimSpace(cmd.Title) != "" {
|
||||||
|
overlay = append(overlay, cmd.Title)
|
||||||
|
}
|
||||||
|
for _, l := range cmd.Lines {
|
||||||
|
overlay = append(overlay, strings.ReplaceAll(l, "\n", "\\n"))
|
||||||
|
}
|
||||||
|
overlay = append(overlay, styles.CommandContinueMessage.Render(core.CommandOutputExitMessage))
|
||||||
|
|
||||||
|
// NOTE: strings.Split on "\n" is safe as long as no style uses .Width()/.Height()/.Padding()/.Margin(),
|
||||||
|
// which would cause Lipgloss to embed newlines internally and corrupt the line count.
|
||||||
|
// If block-level styles are ever added, this approach must be replaced.
|
||||||
|
|
||||||
|
// Remove 'h' lines from back of view and append overlay
|
||||||
|
h := len(overlay)
|
||||||
|
final := lines[:max(0, len(lines)-h)]
|
||||||
|
final = append(final, overlay...)
|
||||||
|
|
||||||
|
return strings.Join(final, "\n")
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
package operator
|
package operator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@ -25,7 +23,11 @@ func (o YankOperator) Operate(m action.Model, start, end core.Position, mtype co
|
|||||||
case core.NormalMode:
|
case core.NormalMode:
|
||||||
yankNormalMode(m, start, end, mtype)
|
yankNormalMode(m, start, end, mtype)
|
||||||
default:
|
default:
|
||||||
m.SetCommandError(fmt.Errorf("'y' operator not yet implemented."))
|
m.SetCommandOutput(&core.CommandOutput{
|
||||||
|
Lines: []string{"'y' operator not yet implemented."},
|
||||||
|
Inline: true,
|
||||||
|
IsError: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
win.SetCursorCol(start.Col)
|
win.SetCursorCol(start.Col)
|
||||||
@ -65,10 +67,14 @@ func yankNormalMode(m action.Model, start, end core.Position, mtype core.MotionT
|
|||||||
switch {
|
switch {
|
||||||
case mtype.IsCharwise():
|
case mtype.IsCharwise():
|
||||||
// This shouldn't happen
|
// This shouldn't happen
|
||||||
if start.Line != end.Line {
|
// if start.Line != end.Line {
|
||||||
m.SetCommandError(fmt.Errorf("Start line and end line must match for charwise yank operations."))
|
// m.SetCommandOutput(&core.CommandOutput{
|
||||||
return
|
// Lines: []string{"Start line and end line must match for charwise yank operations."},
|
||||||
}
|
// Inline: true,
|
||||||
|
// IsError: true,
|
||||||
|
// })
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
line := buf.Lines[start.Line]
|
line := buf.Lines[start.Line]
|
||||||
|
|
||||||
@ -87,10 +93,14 @@ func yankNormalMode(m action.Model, start, end core.Position, mtype core.MotionT
|
|||||||
|
|
||||||
case mtype == core.Linewise:
|
case mtype == core.Linewise:
|
||||||
// This shouldn't happen
|
// This shouldn't happen
|
||||||
if start.Col != end.Col {
|
// if start.Col != end.Col {
|
||||||
m.SetCommandError(fmt.Errorf("Start column and end column must match for linewise yank operations."))
|
// m.SetCommandOutput(&core.CommandOutput{
|
||||||
return
|
// Lines: []string{"Start column and end column must match for linewise yank operations."},
|
||||||
}
|
// Inline: true,
|
||||||
|
// IsError: true,
|
||||||
|
// })
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
// These don't need to be validated, they are validated before being passed into the function
|
// These don't need to be validated, they are validated before being passed into the function
|
||||||
startY := min(start.Line, end.Line)
|
startY := min(start.Line, end.Line)
|
||||||
@ -146,10 +156,14 @@ func yankVisualLineMode(m action.Model, start, end core.Position) {
|
|||||||
buf := m.ActiveBuffer()
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
// This shouldn't happen
|
// This shouldn't happen
|
||||||
if start.Col != end.Col {
|
// if start.Col != end.Col {
|
||||||
m.SetCommandError(fmt.Errorf("Start column and end column must match for linewise yank operations."))
|
// m.SetCommandOutput(&core.CommandOutput{
|
||||||
return
|
// Lines: []string{"Start column and end column must match for linewise yank operations."},
|
||||||
}
|
// Inline: true,
|
||||||
|
// IsError: true,
|
||||||
|
// })
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
// These don't need to be validated, they are validated before being passed into the function
|
// These don't need to be validated, they are validated before being passed into the function
|
||||||
startY := min(start.Line, end.Line)
|
startY := min(start.Line, end.Line)
|
||||||
|
|||||||
@ -26,6 +26,8 @@ type Styles struct {
|
|||||||
|
|
||||||
// Command line
|
// Command line
|
||||||
CommandError lipgloss.Style
|
CommandError lipgloss.Style
|
||||||
|
CommandOutputBorder lipgloss.Style
|
||||||
|
CommandContinueMessage lipgloss.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultStyles returns the default editor color scheme.
|
// DefaultStyles returns the default editor color scheme.
|
||||||
@ -59,6 +61,12 @@ func DefaultStyles() Styles {
|
|||||||
|
|
||||||
CommandError: lipgloss.NewStyle().
|
CommandError: lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#e3203a")),
|
Foreground(lipgloss.Color("#e3203a")),
|
||||||
|
|
||||||
|
CommandOutputBorder: lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("#000000")),
|
||||||
|
|
||||||
|
CommandContinueMessage: lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#546fba")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user