feat: implemented the command window! Not tested. Maybe we need some?
All checks were successful
Run Test Suite / test (push) Successful in 13s
All checks were successful
Run Test Suite / test (push) Successful in 13s
This commit is contained in:
parent
edbed61949
commit
10e37b82af
@ -12,8 +12,7 @@ type ExitCommandMode struct{}
|
||||
func (a ExitCommandMode) Execute(m Model) tea.Cmd {
|
||||
m.SetCommandCursor(0)
|
||||
m.SetCommand("")
|
||||
m.SetCommandOutput("")
|
||||
m.SetCommandError(nil)
|
||||
m.SetCommandOutput(&core.CommandOutput{})
|
||||
m.SetMode(core.NormalMode)
|
||||
return nil
|
||||
}
|
||||
@ -128,7 +127,6 @@ func (a CommandExecute) Execute(m Model) tea.Cmd {
|
||||
|
||||
// Clear command state and return to normal mode
|
||||
m.SetCommandCursor(0)
|
||||
m.SetCommandError(nil)
|
||||
m.SetMode(core.NormalMode)
|
||||
|
||||
if a.Registry == nil || cmdLine == "" {
|
||||
@ -137,7 +135,12 @@ func (a CommandExecute) Execute(m Model) tea.Cmd {
|
||||
|
||||
cmd, err := a.Registry.Execute(m, cmdLine)
|
||||
if err != nil {
|
||||
m.SetCommandError(err)
|
||||
out := core.CommandOutput{
|
||||
Lines: []string{err.Error()},
|
||||
Inline: true,
|
||||
IsError: true,
|
||||
}
|
||||
m.SetCommandOutput(&out)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -20,8 +20,7 @@ type mockModel struct {
|
||||
insertKeys []string
|
||||
command string
|
||||
commandCursor int
|
||||
commandError error
|
||||
commandOutput string
|
||||
commandOutput *core.CommandOutput
|
||||
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) CommandCursor() int { return m.commandCursor }
|
||||
func (m *mockModel) SetCommandCursor(cur int) { m.commandCursor = cur }
|
||||
func (m *mockModel) CommandError() error { return m.commandError }
|
||||
func (m *mockModel) SetCommandError(err error) { m.commandError = err }
|
||||
func (m *mockModel) CommandOutput() string { return m.commandOutput }
|
||||
func (m *mockModel) SetCommandOutput(out string) { m.commandOutput = out }
|
||||
func (m *mockModel) CommandOutput() *core.CommandOutput { return m.commandOutput }
|
||||
func (m *mockModel) SetCommandOutput(out *core.CommandOutput) { m.commandOutput = out }
|
||||
|
||||
// Editor-wide State
|
||||
func (m *mockModel) Mode() core.Mode { return m.mode }
|
||||
|
||||
@ -37,10 +37,8 @@ type Model interface {
|
||||
SetCommand(cmd string)
|
||||
CommandCursor() int
|
||||
SetCommandCursor(cur int)
|
||||
CommandError() error
|
||||
SetCommandError(err error)
|
||||
CommandOutput() string
|
||||
SetCommandOutput(out string)
|
||||
CommandOutput() *core.CommandOutput
|
||||
SetCommandOutput(out *core.CommandOutput)
|
||||
|
||||
// ==================================================
|
||||
// Editor-wide State
|
||||
|
||||
@ -20,8 +20,7 @@ type EnterComandMode struct{}
|
||||
func (a EnterComandMode) Execute(m Model) tea.Cmd {
|
||||
m.SetMode(core.CommandMode)
|
||||
m.SetCommand("")
|
||||
m.SetCommandOutput("")
|
||||
m.SetCommandError(nil)
|
||||
m.SetCommandOutput(&core.CommandOutput{})
|
||||
m.SetCommandCursor(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
@ -21,7 +20,12 @@ func (a Paste) Execute(m Model) tea.Cmd {
|
||||
// Get reg
|
||||
reg, found := m.GetRegister('"')
|
||||
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
|
||||
}
|
||||
|
||||
@ -55,7 +59,12 @@ func (a Paste) Execute(m Model) tea.Cmd {
|
||||
|
||||
// Shouldn't happen, just a check
|
||||
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
|
||||
}
|
||||
|
||||
@ -73,7 +82,12 @@ func (a Paste) Execute(m Model) tea.Cmd {
|
||||
win.SetCursorCol(x + len(cnt))
|
||||
}
|
||||
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
|
||||
@ -100,7 +114,12 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
|
||||
// Get reg
|
||||
reg, found := m.GetRegister('"')
|
||||
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
|
||||
}
|
||||
|
||||
@ -125,7 +144,12 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
|
||||
|
||||
// Shouldn't happen, just a check
|
||||
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
|
||||
}
|
||||
|
||||
@ -143,7 +167,12 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
|
||||
win.SetCursorCol(x + len(cnt))
|
||||
}
|
||||
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
|
||||
@ -167,7 +196,12 @@ func (a VisualPaste) Execute(m Model) tea.Cmd {
|
||||
// Get register content to paste
|
||||
reg, found := m.GetRegister('"')
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -38,7 +38,11 @@ func cmdQuit(m action.Model, args []string, force bool) tea.Cmd {
|
||||
// Cannot exit if any buffer has unsaved changes
|
||||
for _, buf := range bufs {
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
@ -62,7 +66,11 @@ 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.SetCommandError(err)
|
||||
m.SetCommandOutput(&core.CommandOutput{
|
||||
Lines: []string{err.Error()},
|
||||
Inline: true,
|
||||
IsError: true,
|
||||
})
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@ -76,7 +84,11 @@ func cmdWriteAll(m action.Model, args []string, force bool) tea.Cmd {
|
||||
if buf.Modified {
|
||||
cmd, err := writeBuffer(m, buf, args, force)
|
||||
if err != nil {
|
||||
m.SetCommandError(err)
|
||||
m.SetCommandOutput(&core.CommandOutput{
|
||||
Lines: []string{err.Error()},
|
||||
Inline: true,
|
||||
IsError: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
@ -91,7 +103,11 @@ 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.SetCommandError(err)
|
||||
m.SetCommandOutput(&core.CommandOutput{
|
||||
Lines: []string{err.Error()},
|
||||
Inline: true,
|
||||
IsError: true,
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -108,7 +124,11 @@ func cmdWriteQuitAll(m action.Model, args []string, force bool) tea.Cmd {
|
||||
if buf.Modified {
|
||||
cmd, err := writeBuffer(m, buf, args, force)
|
||||
if err != nil {
|
||||
m.SetCommandError(err)
|
||||
m.SetCommandOutput(&core.CommandOutput{
|
||||
Lines: []string{err.Error()},
|
||||
Inline: true,
|
||||
IsError: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
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 {
|
||||
// must have arguments, cant edit nothing
|
||||
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
|
||||
}
|
||||
|
||||
@ -156,7 +180,11 @@ func cmdEdit(m action.Model, args []string, force bool) tea.Cmd {
|
||||
notFound := errors.Is(err, os.ErrNotExist)
|
||||
|
||||
if err != nil && !notFound {
|
||||
m.SetCommandError(err)
|
||||
m.SetCommandOutput(&core.CommandOutput{
|
||||
Lines: []string{err.Error()},
|
||||
Inline: true,
|
||||
IsError: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
if file != nil {
|
||||
@ -217,27 +245,65 @@ func cmdEdit(m action.Model, args []string, force bool) tea.Cmd {
|
||||
|
||||
// cmdRegisters: Handles :register command (debug - displays register content).
|
||||
func cmdRegisters(m action.Model, args []string, force bool) tea.Cmd {
|
||||
// TODO: This is temporary, for debugging
|
||||
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
|
||||
}
|
||||
|
||||
if len(args[0]) != 1 {
|
||||
m.SetCommandError(fmt.Errorf("Name should be a single character."))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
name := rune(args[0][0])
|
||||
reg, found := m.GetRegister(name)
|
||||
if !found {
|
||||
m.SetCommandError(fmt.Errorf("Could not find register '%c'.", name))
|
||||
return nil
|
||||
}
|
||||
|
||||
content := strings.Join(reg.Content, "\\n")
|
||||
t := reg.Type
|
||||
m.SetCommandOutput(fmt.Sprintf("Type: %d Name: \"%c Content: %s", t, name, content))
|
||||
m.SetMode(core.CommandOutputMode)
|
||||
m.SetCommandOutput(&core.CommandOutput{
|
||||
Title: ":reg",
|
||||
Lines: lines,
|
||||
Inline: false,
|
||||
IsError: false,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -255,14 +321,20 @@ func cmdRegisters(m action.Model, args []string, force bool) tea.Cmd {
|
||||
// :set ts=4 - set tabstop to 4 (abbreviation)
|
||||
func cmdSet(m action.Model, args []string, force bool) tea.Cmd {
|
||||
if len(args) == 0 {
|
||||
out := fmt.Sprintf("%+v", m.Settings())
|
||||
m.SetCommandOutput(out)
|
||||
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.SetCommandError(err)
|
||||
m.SetCommandOutput(&core.CommandOutput{
|
||||
Lines: []string{err.Error()},
|
||||
Inline: true,
|
||||
IsError: true,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
output := fmt.Sprintf("'%s', %dL %db written", filename, buf.LineCount(), bytes)
|
||||
m.SetCommandOutput(output)
|
||||
m.SetCommandOutput(&core.CommandOutput{
|
||||
Lines: []string{fmt.Sprintf("'%s', %dL %db written", filename, buf.LineCount(), bytes)},
|
||||
Inline: true,
|
||||
})
|
||||
buf.SetModified(false)
|
||||
|
||||
return nil, nil
|
||||
|
||||
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
|
||||
InsertMode
|
||||
CommandMode
|
||||
CommandOutputMode
|
||||
VisualMode
|
||||
VisualLineMode
|
||||
VisualBlockMode
|
||||
|
||||
@ -11,6 +11,18 @@ const (
|
||||
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
|
||||
// pasted. The Type determines paste behavior and Content holds the text lines.
|
||||
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")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.commandError == nil {
|
||||
t.Error("expected commandError to be set for unknown command")
|
||||
if m.commandOutput == nil || !m.commandOutput.IsError {
|
||||
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")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.commandError != nil {
|
||||
t.Errorf("expected commandError to be nil, got %v", m.commandError)
|
||||
if m.commandOutput != nil && m.commandOutput.IsError {
|
||||
t.Errorf("expected no error output, got %v", m.commandOutput.Lines)
|
||||
}
|
||||
})
|
||||
|
||||
@ -332,8 +332,8 @@ func TestCommandModeErrors(t *testing.T) {
|
||||
sendKeys(tm, ":", "esc")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.commandError != nil {
|
||||
t.Errorf("expected commandError to be nil after esc, got %v", m.commandError)
|
||||
if m.commandOutput != nil && m.commandOutput.IsError {
|
||||
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")
|
||||
|
||||
m := getFinalModel(t, tm)
|
||||
if m.commandError == nil {
|
||||
t.Error("expected commandError to be set for edit without args")
|
||||
if m.commandOutput == nil || !m.commandOutput.IsError {
|
||||
t.Error("expected commandOutput with IsError to be set for edit without args")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -39,8 +39,7 @@ type Model struct {
|
||||
// Command line state
|
||||
command string
|
||||
commandCursor int
|
||||
commandError error
|
||||
commandOutput string
|
||||
commandOutput *core.CommandOutput
|
||||
|
||||
// Global settings
|
||||
settings core.EditorSettings
|
||||
@ -272,19 +271,11 @@ func (m *Model) SetCommandCursor(cur int) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) CommandError() error {
|
||||
return m.commandError
|
||||
}
|
||||
|
||||
func (m *Model) SetCommandError(err error) {
|
||||
m.commandError = err
|
||||
}
|
||||
|
||||
func (m *Model) CommandOutput() string {
|
||||
func (m *Model) CommandOutput() *core.CommandOutput {
|
||||
return m.commandOutput
|
||||
}
|
||||
|
||||
func (m *Model) SetCommandOutput(out string) {
|
||||
func (m *Model) SetCommandOutput(out *core.CommandOutput) {
|
||||
m.commandOutput = out
|
||||
}
|
||||
|
||||
|
||||
@ -25,8 +25,7 @@ func NewModelBuilder() *ModelBuilder {
|
||||
insertAction: nil,
|
||||
command: "",
|
||||
commandCursor: 0,
|
||||
commandError: nil,
|
||||
commandOutput: "",
|
||||
commandOutput: nil,
|
||||
settings: core.NewDefaultSettings(),
|
||||
registers: core.DefaultRegisters(),
|
||||
styles: style.DefaultStyles(),
|
||||
@ -116,15 +115,9 @@ func (mb *ModelBuilder) WithCommandCursor(cursor int) *ModelBuilder {
|
||||
return mb
|
||||
}
|
||||
|
||||
// ModelBuilder.WithCommandError: Set the command line error state.
|
||||
func (mb *ModelBuilder) WithCommandError(err error) *ModelBuilder {
|
||||
mb.model.commandError = err
|
||||
return mb
|
||||
}
|
||||
|
||||
// ModelBuilder.WithCommandOutput: Set the command line output text.
|
||||
func (mb *ModelBuilder) WithCommandOutput(output string) *ModelBuilder {
|
||||
mb.model.commandOutput = output
|
||||
// ModelBuilder.WithCommandOutput: Set the command line output.
|
||||
func (mb *ModelBuilder) WithCommandOutput(out *core.CommandOutput) *ModelBuilder {
|
||||
mb.model.commandOutput = out
|
||||
return mb
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package editor
|
||||
|
||||
import (
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
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 {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
// Keep cursor in view after any update
|
||||
win := m.ActiveWindow()
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Model.View: Renders the complete editor view including buffer content, line
|
||||
@ -26,7 +27,16 @@ func (m Model) View() string {
|
||||
|
||||
// Command bar is seperate
|
||||
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.
|
||||
@ -139,44 +149,6 @@ func drawGutter(w *core.Window, styles style.Styles, options core.WinOptions, cu
|
||||
}
|
||||
|
||||
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,
|
||||
@ -185,7 +157,7 @@ func drawStatusBar(w *core.Window, mode core.Mode) string {
|
||||
left := leftBar(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
|
||||
if diff <= 0 {
|
||||
@ -237,13 +209,13 @@ func drawCommandBar(m Model) string {
|
||||
var leftBar string
|
||||
if m.Mode() == core.CommandMode {
|
||||
leftBar = ":"
|
||||
cmd := m.Command()
|
||||
cmd := []rune(m.Command())
|
||||
cur := m.CommandCursor()
|
||||
for i := 0; i < len(cmd); i++ {
|
||||
for i, r := range cmd {
|
||||
if i == cur {
|
||||
leftBar += m.Styles().CursorStyle(m.Mode()).Render(string(cmd[i]))
|
||||
leftBar += m.Styles().CursorStyle(m.Mode()).Render(string(r))
|
||||
} else {
|
||||
leftBar += string(cmd[i])
|
||||
leftBar += string(r)
|
||||
}
|
||||
}
|
||||
// Cursor at end of command
|
||||
@ -251,10 +223,14 @@ func drawCommandBar(m Model) string {
|
||||
leftBar += m.Styles().CursorStyle(m.Mode()).Render(" ")
|
||||
}
|
||||
// bar = fmt.Sprintf("%s %d", bar, cur)
|
||||
} else if m.CommandError() != nil {
|
||||
leftBar = m.Styles().CommandError.Render(m.CommandError().Error())
|
||||
} else if strings.TrimSpace(m.CommandOutput()) != "" {
|
||||
leftBar = m.CommandOutput()
|
||||
} else if out := m.CommandOutput(); out != nil && len(out.Lines) > 0 && out.Inline {
|
||||
// TODO: This is not perfect, temporary
|
||||
text := strings.Join(out.Lines, " ")
|
||||
if out.IsError {
|
||||
leftBar = m.Styles().CommandError.Render(text)
|
||||
} else {
|
||||
leftBar = text
|
||||
}
|
||||
} else if strings.TrimSpace(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())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -317,3 +293,38 @@ func posInsideSelection(w *core.Window, mode core.Mode, col, line int) bool {
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
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:
|
||||
yankNormalMode(m, start, end, mtype)
|
||||
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)
|
||||
@ -65,10 +67,14 @@ func yankNormalMode(m action.Model, start, end core.Position, mtype core.MotionT
|
||||
switch {
|
||||
case mtype.IsCharwise():
|
||||
// This shouldn't happen
|
||||
if start.Line != end.Line {
|
||||
m.SetCommandError(fmt.Errorf("Start line and end line must match for charwise yank operations."))
|
||||
return
|
||||
}
|
||||
// if start.Line != end.Line {
|
||||
// m.SetCommandOutput(&core.CommandOutput{
|
||||
// Lines: []string{"Start line and end line must match for charwise yank operations."},
|
||||
// Inline: true,
|
||||
// IsError: true,
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
|
||||
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:
|
||||
// This shouldn't happen
|
||||
if start.Col != end.Col {
|
||||
m.SetCommandError(fmt.Errorf("Start column and end column must match for linewise yank operations."))
|
||||
return
|
||||
}
|
||||
// if start.Col != end.Col {
|
||||
// m.SetCommandOutput(&core.CommandOutput{
|
||||
// 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
|
||||
startY := min(start.Line, end.Line)
|
||||
@ -146,10 +156,14 @@ func yankVisualLineMode(m action.Model, start, end core.Position) {
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
// This shouldn't happen
|
||||
if start.Col != end.Col {
|
||||
m.SetCommandError(fmt.Errorf("Start column and end column must match for linewise yank operations."))
|
||||
return
|
||||
}
|
||||
// if start.Col != end.Col {
|
||||
// m.SetCommandOutput(&core.CommandOutput{
|
||||
// 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
|
||||
startY := min(start.Line, end.Line)
|
||||
|
||||
@ -26,6 +26,8 @@ type Styles struct {
|
||||
|
||||
// Command line
|
||||
CommandError lipgloss.Style
|
||||
CommandOutputBorder lipgloss.Style
|
||||
CommandContinueMessage lipgloss.Style
|
||||
}
|
||||
|
||||
// DefaultStyles returns the default editor color scheme.
|
||||
@ -59,6 +61,12 @@ func DefaultStyles() Styles {
|
||||
|
||||
CommandError: lipgloss.NewStyle().
|
||||
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