diff --git a/internal/action/command.go b/internal/action/command.go index e8e5ab3..969e340 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -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 } diff --git a/internal/action/find_test.go b/internal/action/find_test.go index e1a045f..7ec4315 100644 --- a/internal/action/find_test.go +++ b/internal/action/find_test.go @@ -20,8 +20,7 @@ type mockModel struct { insertKeys []string command string commandCursor int - commandError error - commandOutput string + commandOutput *core.CommandOutput lastFind core.LastFindCommand } @@ -92,14 +91,12 @@ func (m *mockModel) SetLastFind(char string, forward, inclusive bool) { func (m *mockModel) GetLastFind() *core.LastFindCommand { return &m.lastFind } // Command Mode State -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) 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) 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 } diff --git a/internal/action/interface.go b/internal/action/interface.go index 2608455..451c78b 100644 --- a/internal/action/interface.go +++ b/internal/action/interface.go @@ -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 diff --git a/internal/action/misc.go b/internal/action/misc.go index 554aff7..cdc1ad6 100644 --- a/internal/action/misc.go +++ b/internal/action/misc.go @@ -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 } diff --git a/internal/action/paste.go b/internal/action/paste.go index 12cf974..64bc4b5 100644 --- a/internal/action/paste.go +++ b/internal/action/paste.go @@ -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 } diff --git a/internal/command/handlers.go b/internal/command/handlers.go index 58892b5..bb261d4 100644 --- a/internal/command/handlers.go +++ b/internal/command/handlers.go @@ -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 } } diff --git a/internal/command/handlers_test.go b/internal/command/handlers_test.go index 64e7495..dcff65a 100644 --- a/internal/command/handlers_test.go +++ b/internal/command/handlers_test.go @@ -25,8 +25,7 @@ type mockModel struct { insertKeys []string command string commandCursor int - commandError error - commandOutput string + commandOutput *core.CommandOutput lastFind core.LastFindCommand } @@ -86,14 +85,12 @@ func (m *mockModel) SetLastFind(char string, forward, inclusive bool) { func (m *mockModel) GetLastFind() *core.LastFindCommand { return &m.lastFind } // Command Mode State -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) 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) 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 } @@ -134,8 +131,8 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, err := os.ReadFile(filename) @@ -164,8 +161,8 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{newFile}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify new file was created @@ -204,8 +201,8 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, _ := os.ReadFile(filename) @@ -230,12 +227,12 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for readonly buffer") } - if !strings.Contains(m.commandError.Error(), "readonly") { - t.Errorf("error should mention readonly: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") { + t.Errorf("error should mention readonly: %v", m.commandOutput.Lines) } // Verify file was NOT created @@ -258,12 +255,12 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for scratch buffer") } - if !strings.Contains(m.commandError.Error(), "ScratchBuffer") { - t.Errorf("error should mention ScratchBuffer: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "ScratchBuffer") { + t.Errorf("error should mention ScratchBuffer: %v", m.commandOutput.Lines) } }) @@ -278,12 +275,12 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when no filename available") } - if !strings.Contains(m.commandError.Error(), "no file name") { - t.Errorf("error should mention no file name: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "no file name") { + t.Errorf("error should mention no file name: %v", m.commandOutput.Lines) } }) @@ -298,7 +295,7 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for invalid path") } }) @@ -317,8 +314,8 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, err := os.ReadFile(filename) @@ -346,8 +343,8 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, _ := os.ReadFile(filename) @@ -375,8 +372,8 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, _ := os.ReadFile(filename) @@ -405,8 +402,8 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, _ := os.ReadFile(filename) @@ -435,16 +432,16 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandOutput == "" { + if m.commandOutput == nil || len(m.commandOutput.Lines) == 0 { t.Error("expected output message") } // Should contain filename and line count - if !strings.Contains(m.commandOutput, filename) { - t.Errorf("output should contain filename: %q", m.commandOutput) + if !strings.Contains(m.commandOutput.Lines[0], filename) { + t.Errorf("output should contain filename: %q", m.commandOutput.Lines[0]) } - if !strings.Contains(m.commandOutput, "2L") { - t.Errorf("output should contain line count: %q", m.commandOutput) + if !strings.Contains(m.commandOutput.Lines[0], "2L") { + t.Errorf("output should contain line count: %q", m.commandOutput.Lines[0]) } }) @@ -468,8 +465,8 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } info, err := os.Stat(filename) @@ -498,8 +495,8 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if _, err := os.Stat(filename); os.IsNotExist(err) { @@ -523,7 +520,7 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{filename}, false) // Current implementation doesn't create parent dirs, so expect error - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when parent directories don't exist") } }) @@ -547,8 +544,8 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if m.ActiveBuffer().Modified { @@ -572,8 +569,8 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{newFile}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Modified flag is cleared even when writing to a different file @@ -600,7 +597,7 @@ func TestCmdWrite(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Fatal("expected error for invalid path") } @@ -621,12 +618,12 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when no argument") } - if !strings.Contains(m.commandError.Error(), "requires an argument") { - t.Errorf("error should mention requires argument: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "requires an argument") { + t.Errorf("error should mention requires argument: %v", m.commandOutput.Lines) } }) @@ -639,8 +636,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Should have added a new buffer @@ -677,8 +674,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer @@ -709,8 +706,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer @@ -729,8 +726,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer @@ -763,8 +760,8 @@ func TestCmdEdit(t *testing.T) { m := newMockModel() cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if m.activeWindow.Buffer.Filetype != tt.expected { @@ -787,8 +784,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer @@ -847,8 +844,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer @@ -877,8 +874,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer @@ -939,7 +936,7 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for permission denied") } }) @@ -957,8 +954,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer @@ -1008,8 +1005,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer @@ -1049,8 +1046,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } buf := m.activeWindow.Buffer @@ -1076,8 +1073,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) // Should succeed - creates new buffer - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Buffer should be created with the filename @@ -1123,8 +1120,8 @@ func TestCmdEdit(t *testing.T) { cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if m.activeWindow.Buffer.Filename != filename { @@ -1143,8 +1140,8 @@ func TestCmdEdit(t *testing.T) { // First edit - loads the file cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error on first edit: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error on first edit: %v", m.commandOutput.Lines) } if len(m.buffers) != initialBufferCount+1 { @@ -1163,8 +1160,8 @@ func TestCmdEdit(t *testing.T) { // Second edit - should switch to existing buffer, not create new one cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error on second edit: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error on second edit: %v", m.commandOutput.Lines) } // Should not have created a new buffer @@ -1196,8 +1193,8 @@ func TestCmdEdit(t *testing.T) { // First edit cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } bufferId := m.activeWindow.Buffer.Id @@ -1290,14 +1287,14 @@ func TestEditWriteRoundTrip(t *testing.T) { // Edit cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("edit error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("edit error: %v", m.commandOutput.Lines) } // Write cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("write error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("write error: %v", m.commandOutput.Lines) } // Read back @@ -1315,8 +1312,8 @@ func TestEditWriteRoundTrip(t *testing.T) { // Edit (creates new buffer) cmdEdit(m, []string{filename}, false) - if m.commandError != nil { - t.Fatalf("edit error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("edit error: %v", m.commandOutput.Lines) } // Add content to buffer @@ -1324,8 +1321,8 @@ func TestEditWriteRoundTrip(t *testing.T) { // Write cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("write error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("write error: %v", m.commandOutput.Lines) } // Verify file exists @@ -1351,8 +1348,8 @@ func TestEditWriteRoundTrip(t *testing.T) { // Edit original cmdEdit(m, []string{original}, false) - if m.commandError != nil { - t.Fatalf("edit error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("edit error: %v", m.commandOutput.Lines) } // Modify @@ -1360,8 +1357,8 @@ func TestEditWriteRoundTrip(t *testing.T) { // Write to new file cmdWrite(m, []string{newFile}, false) - if m.commandError != nil { - t.Fatalf("write error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("write error: %v", m.commandOutput.Lines) } // Verify new file has modified content @@ -1423,8 +1420,8 @@ func TestCmdQuit(t *testing.T) { cmd := cmdQuit(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -1449,7 +1446,7 @@ func TestCmdQuit(t *testing.T) { cmd := cmdQuit(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for modified buffer") } @@ -1469,12 +1466,12 @@ func TestCmdQuit(t *testing.T) { cmdQuit(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Fatal("expected error") } - if !strings.Contains(m.commandError.Error(), "important_file.txt") { - t.Errorf("error should mention filename: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "important_file.txt") { + t.Errorf("error should mention filename: %v", m.commandOutput.Lines) } }) @@ -1498,7 +1495,7 @@ func TestCmdQuit(t *testing.T) { cmd := cmdQuit(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when any buffer is modified") } @@ -1543,13 +1540,13 @@ func TestCmdQuit(t *testing.T) { cmdQuit(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for modified buffer without filename") } // Error message should still be meaningful - if !strings.Contains(m.commandError.Error(), "unsaved") { - t.Logf("Note: error message for unnamed buffer: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "unsaved") { + t.Logf("Note: error message for unnamed buffer: %v", m.commandOutput.Lines) } }) @@ -1577,8 +1574,8 @@ func TestCmdQuit(t *testing.T) { cmd := cmdQuit(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -1602,8 +1599,8 @@ func TestCmdQuitAll(t *testing.T) { cmd := cmdQuitAll(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -1634,7 +1631,7 @@ func TestCmdQuitAll(t *testing.T) { cmd := cmdQuitAll(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when any buffer is modified") } @@ -1659,8 +1656,8 @@ func TestCmdQuitAll(t *testing.T) { cmd := cmdQuitAll(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -1679,12 +1676,12 @@ func TestCmdQuitAll(t *testing.T) { cmdQuitAll(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Fatal("expected error") } - if !strings.Contains(m.commandError.Error(), "important_document.txt") { - t.Errorf("error should mention filename: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "important_document.txt") { + t.Errorf("error should mention filename: %v", m.commandOutput.Lines) } }) @@ -1704,13 +1701,13 @@ func TestCmdQuitAll(t *testing.T) { cmdQuitAll(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Fatal("expected error") } // Should report first modified buffer - if !strings.Contains(m.commandError.Error(), "first_modified.txt") { - t.Errorf("should report first modified buffer: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "first_modified.txt") { + t.Errorf("should report first modified buffer: %v", m.commandOutput.Lines) } }) } @@ -1732,8 +1729,8 @@ func TestCmdQuitForce(t *testing.T) { cmd := cmdQuit(m, []string{}, true) // Should NOT error - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Should return quit command @@ -1760,8 +1757,8 @@ func TestCmdQuitForce(t *testing.T) { cmd := cmdQuit(m, []string{}, true) // Force quit should work even with readonly modified buffer - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -1799,8 +1796,8 @@ func TestCmdQuitForce(t *testing.T) { cmd := cmdQuit(m, []string{}, true) // Should quit regardless of multiple modified buffers - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -1824,8 +1821,8 @@ func TestCmdQuitForce(t *testing.T) { cmd := cmdQuit(m, []string{}, false) // Should work with unmodified buffers too - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -1850,8 +1847,8 @@ func TestCmdQuitForce(t *testing.T) { cmd := cmdQuit(m, []string{}, true) // Force quit works even without filename - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -1877,8 +1874,8 @@ func TestCmdQuitForce(t *testing.T) { cmd := cmdQuit(m, []string{}, true) // Force quit works with scratch buffers - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -1916,8 +1913,8 @@ func TestCmdQuitForce(t *testing.T) { cmd := cmdQuit(m, []string{}, true) // Should quit regardless of buffer states - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -1981,8 +1978,8 @@ func TestCmdQuitAllForce(t *testing.T) { cmd := cmdQuitAll(m, []string{}, true) // Should quit even with all buffers modified - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -2012,8 +2009,8 @@ func TestCmdQuitAllForce(t *testing.T) { cmd := cmdQuitAll(m, []string{}, true) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -2046,8 +2043,8 @@ func TestCmdQuitAllForce(t *testing.T) { cmd := cmdQuitAll(m, []string{}, true) // Force quit should work with readonly buffers - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -2080,8 +2077,8 @@ func TestCmdQuitAllForce(t *testing.T) { cmd := cmdQuitAll(m, []string{}, true) // Should quit with scratch buffers - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -2113,8 +2110,8 @@ func TestCmdQuitAllForce(t *testing.T) { cmd := cmdQuitAll(m, []string{}, true) // Force quit works even without filenames - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -2135,8 +2132,8 @@ func TestCmdQuitAllForce(t *testing.T) { cmd := cmdQuitAll(m, []string{}, true) // Should quit even with no buffers - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -2192,8 +2189,8 @@ func TestCmdQuitAllForce(t *testing.T) { cmd := cmdQuitAll(m, []string{}, true) // Force quit should work regardless of any buffer state - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -2223,8 +2220,8 @@ func TestCmdQuitAllForce(t *testing.T) { cmd := cmdQuitAll(m, []string{}, true) // Should work with unmodified buffers too - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -2267,8 +2264,8 @@ func TestCmdWriteAll(t *testing.T) { cmdWriteAll(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify both files were written @@ -2337,8 +2334,8 @@ func TestCmdWriteAll(t *testing.T) { cmdWriteAll(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if buf.Modified { @@ -2359,12 +2356,12 @@ func TestCmdWriteAll(t *testing.T) { cmdWriteAll(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Fatal("expected error for modified readonly buffer") } - if !strings.Contains(m.commandError.Error(), "readonly") { - t.Errorf("error should mention readonly: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") { + t.Errorf("error should mention readonly: %v", m.commandOutput.Lines) } }) @@ -2394,8 +2391,8 @@ func TestCmdWriteAll(t *testing.T) { cmdWriteAll(m, []string{}, false) // Should succeed - readonly buffer not modified, so skip it - if m.commandError != nil { - t.Fatalf("should not error for unmodified readonly buffer: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("should not error for unmodified readonly buffer: %v", m.commandOutput.Lines) } // Writable file should be written @@ -2417,7 +2414,7 @@ func TestCmdWriteAll(t *testing.T) { cmdWriteAll(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for scratch buffer") } }) @@ -2434,13 +2431,13 @@ func TestCmdWriteAll(t *testing.T) { cmdWriteAll(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for buffer without filename") } - if !strings.Contains(m.commandError.Error(), "no file name") || - !strings.Contains(strings.ToLower(m.commandError.Error()), "name") { - t.Logf("error message: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "no file name") || + !strings.Contains(strings.ToLower(strings.Join(m.commandOutput.Lines, " ")), "name") { + t.Logf("error message: %v", m.commandOutput.Lines) } }) @@ -2456,8 +2453,8 @@ func TestCmdWriteAll(t *testing.T) { cmdWriteAll(m, []string{}, false) // Should succeed even with nothing to write - if m.commandError != nil { - t.Errorf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Errorf("unexpected error: %v", m.commandOutput.Lines) } }) @@ -2486,10 +2483,10 @@ func TestCmdWriteAll(t *testing.T) { cmdWriteAll(m, []string{}, false) // Should have some output indicating what was written - if m.commandOutput == "" { + if m.commandOutput == nil || len(m.commandOutput.Lines) == 0 { t.Log("Note: cmdWriteAll doesn't set output message") - } else if !strings.Contains(m.commandOutput, "2") { - t.Logf("Output message: %q", m.commandOutput) + } else if !strings.Contains(m.commandOutput.Lines[0], "2") { + t.Logf("Output message: %q", m.commandOutput.Lines[0]) } }) @@ -2518,7 +2515,7 @@ func TestCmdWriteAll(t *testing.T) { cmdWriteAll(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error") } @@ -2546,8 +2543,8 @@ func TestCmdWriteQuit(t *testing.T) { cmd := cmdWriteQuit(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify file was written @@ -2581,7 +2578,7 @@ func TestCmdWriteQuit(t *testing.T) { cmd := cmdWriteQuit(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for readonly buffer") } @@ -2601,7 +2598,7 @@ func TestCmdWriteQuit(t *testing.T) { cmd := cmdWriteQuit(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for scratch buffer") } @@ -2621,7 +2618,7 @@ func TestCmdWriteQuit(t *testing.T) { cmd := cmdWriteQuit(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for buffer without filename") } @@ -2641,7 +2638,7 @@ func TestCmdWriteQuit(t *testing.T) { cmd := cmdWriteQuit(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for invalid path") } @@ -2665,8 +2662,8 @@ func TestCmdWriteQuit(t *testing.T) { cmd := cmdWriteQuit(m, []string{newFile}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // New file should exist @@ -2737,8 +2734,8 @@ func TestCmdWriteQuitAll(t *testing.T) { cmd := cmdWriteQuitAll(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify files were written @@ -2787,8 +2784,8 @@ func TestCmdWriteQuitAll(t *testing.T) { cmd := cmdWriteQuitAll(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Should still quit even if some buffers unmodified @@ -2822,12 +2819,12 @@ func TestCmdWriteQuitAll(t *testing.T) { cmd := cmdWriteQuitAll(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for modified readonly buffer") } - if !strings.Contains(m.commandError.Error(), "readonly") { - t.Errorf("error should mention readonly: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") { + t.Errorf("error should mention readonly: %v", m.commandOutput.Lines) } if cmd != nil { @@ -2860,8 +2857,8 @@ func TestCmdWriteQuitAll(t *testing.T) { cmd := cmdWriteQuitAll(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("should not error for unmodified readonly buffer: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("should not error for unmodified readonly buffer: %v", m.commandOutput.Lines) } // Should write the modified writable buffer @@ -2888,7 +2885,7 @@ func TestCmdWriteQuitAll(t *testing.T) { cmd := cmdWriteQuitAll(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for buffer without filename") } @@ -2909,8 +2906,8 @@ func TestCmdWriteQuitAll(t *testing.T) { cmd := cmdWriteQuitAll(m, []string{}, false) // Should still quit even if nothing to write - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { @@ -2972,8 +2969,8 @@ func TestCmdWriteForce(t *testing.T) { cmdWrite(m, []string{}, true) // Force should bypass readonly check - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify file was written @@ -3006,8 +3003,8 @@ func TestCmdWriteForce(t *testing.T) { cmdWrite(m, []string{newFile}, true) // Should succeed with force - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify new file was created @@ -3043,8 +3040,8 @@ func TestCmdWriteForce(t *testing.T) { cmdWrite(m, []string{targetFile}, true) // Force with filename should work for scratch buffer - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify file was written @@ -3077,8 +3074,8 @@ func TestCmdWriteForce(t *testing.T) { cmdWrite(m, []string{targetFile}, true) // Force should bypass BOTH readonly AND scratch checks - if m.commandError != nil { - t.Fatalf("unexpected error with force: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error with force: %v", m.commandOutput.Lines) } // Verify file was written @@ -3110,12 +3107,12 @@ func TestCmdWriteForce(t *testing.T) { cmdWrite(m, []string{}, true) // Force doesn't help if there's no filename to write to - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when no filename provided") } - if !strings.Contains(m.commandError.Error(), "no file name") { - t.Errorf("error should mention no file name: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "no file name") { + t.Errorf("error should mention no file name: %v", m.commandOutput.Lines) } }) @@ -3132,7 +3129,7 @@ func TestCmdWriteForce(t *testing.T) { cmdWrite(m, []string{}, true) // Force bypasses buffer checks but not OS-level checks - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for invalid path even with force") } }) @@ -3153,12 +3150,12 @@ func TestCmdWriteForce(t *testing.T) { // force=false should still prevent write cmdWrite(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for readonly without force flag") } - if !strings.Contains(m.commandError.Error(), "readonly") { - t.Errorf("error should mention readonly: %v", m.commandError) + if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") { + t.Errorf("error should mention readonly: %v", m.commandOutput.Lines) } }) @@ -3182,8 +3179,8 @@ func TestCmdWriteForce(t *testing.T) { cmdWrite(m, []string{}, true) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Modified flag should be cleared @@ -3231,8 +3228,8 @@ func TestCmdWriteForce(t *testing.T) { cmdWrite(m, []string{filename}, true) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Should create empty file @@ -3279,8 +3276,8 @@ func TestCmdWriteAllForce(t *testing.T) { cmdWriteAll(m, []string{}, true) // Should succeed with force - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Both files should be written @@ -3313,7 +3310,7 @@ func TestCmdWriteAllForce(t *testing.T) { // Force can bypass scratch type check, but if buffer has no filename // it should still error with "no file name provided" - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for scratch buffer without filename") } }) @@ -3349,8 +3346,8 @@ func TestCmdWriteAllForce(t *testing.T) { cmdWriteAll(m, []string{}, true) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Both modified files should be written @@ -3384,8 +3381,8 @@ func TestCmdWriteAllForce(t *testing.T) { cmdWriteAll(m, []string{}, true) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } if buf.Modified { @@ -3415,8 +3412,8 @@ func TestCmdWriteQuitForce(t *testing.T) { cmd := cmdWriteQuit(m, []string{}, true) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // File should be written @@ -3453,8 +3450,8 @@ func TestCmdWriteQuitForce(t *testing.T) { cmd := cmdWriteQuit(m, []string{filename}, true) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // File should be written @@ -3482,7 +3479,7 @@ func TestCmdWriteQuitForce(t *testing.T) { cmd := cmdWriteQuit(m, []string{}, true) // Can't write without a filename even with force - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error when no filename") } @@ -3523,8 +3520,8 @@ func TestCmdWriteQuitAllForce(t *testing.T) { cmd := cmdWriteQuitAll(m, []string{}, true) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Both files should be written @@ -3580,8 +3577,8 @@ func TestCmdWriteQuitAllForce(t *testing.T) { cmd := cmdWriteQuitAll(m, []string{}, true) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Modified buffers should be written @@ -3621,7 +3618,7 @@ func TestCmdWriteQuitAllForce(t *testing.T) { cmd := cmdWriteQuitAll(m, []string{}, true) // Force can't help when there's no filename - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for buffer without filename") } @@ -3696,7 +3693,7 @@ func TestEdgeCases(t *testing.T) { // Regular write should fail (OS permission denied) cmdWrite(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error for OS-level readonly file") } }) @@ -3718,8 +3715,8 @@ func TestEdgeCases(t *testing.T) { cmdWrite(m, []string{}, true) // Force write should work even for unmodified readonly - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } content, _ := os.ReadFile(filename) @@ -3744,15 +3741,15 @@ func TestEdgeCases(t *testing.T) { // Without force - should error cmdWrite(m, []string{}, false) - if m.commandError == nil { + if m.commandOutput == nil || !m.commandOutput.IsError { t.Error("expected error without force") } // With force - should succeed - m.commandError = nil + m.commandOutput = nil cmdWrite(m, []string{}, true) - if m.commandError != nil { - t.Errorf("force write failed: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Errorf("force write failed: %v", m.commandOutput.Lines) } // Modified flag should be cleared @@ -3777,8 +3774,8 @@ func TestEdgeCases(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify file exists @@ -3801,8 +3798,8 @@ func TestEdgeCases(t *testing.T) { cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("unexpected error: %v", m.commandOutput.Lines) } // Verify file exists and content is correct @@ -3830,8 +3827,8 @@ func TestEdgeCases(t *testing.T) { // First write cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("first write failed: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("first write failed: %v", m.commandOutput.Lines) } // Modify buffer @@ -3840,8 +3837,8 @@ func TestEdgeCases(t *testing.T) { // Second write cmdWrite(m, []string{}, false) - if m.commandError != nil { - t.Fatalf("second write failed: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Fatalf("second write failed: %v", m.commandOutput.Lines) } // Verify final content @@ -3867,8 +3864,8 @@ func TestEdgeCases(t *testing.T) { cmd := cmdQuit(m, []string{}, true) // Should quit without error - if m.commandError != nil { - t.Errorf("unexpected error: %v", m.commandError) + if m.commandOutput != nil && m.commandOutput.IsError { + t.Errorf("unexpected error: %v", m.commandOutput.Lines) } if cmd == nil { diff --git a/internal/command/io.go b/internal/command/io.go index 952efc7..84bad6b 100644 --- a/internal/command/io.go +++ b/internal/command/io.go @@ -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 diff --git a/internal/core/command.go b/internal/core/command.go new file mode 100644 index 0000000..b378789 --- /dev/null +++ b/internal/core/command.go @@ -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 +} diff --git a/internal/core/mode.go b/internal/core/mode.go index 39bf5ac..6131c3e 100644 --- a/internal/core/mode.go +++ b/internal/core/mode.go @@ -7,6 +7,7 @@ const ( NormalMode Mode = iota InsertMode CommandMode + CommandOutputMode VisualMode VisualLineMode VisualBlockMode diff --git a/internal/core/register.go b/internal/core/register.go index 7f6bc22..fb223d8 100644 --- a/internal/core/register.go +++ b/internal/core/register.go @@ -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 { diff --git a/internal/editor/integration_command_test.go b/internal/editor/integration_command_test.go index 6bbb9e3..7c04a9d 100644 --- a/internal/editor/integration_command_test.go +++ b/internal/editor/integration_command_test.go @@ -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") } }) } diff --git a/internal/editor/model.go b/internal/editor/model.go index 6b7249d..69cb41a 100644 --- a/internal/editor/model.go +++ b/internal/editor/model.go @@ -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 } diff --git a/internal/editor/model_builder.go b/internal/editor/model_builder.go index 066e60c..12fe923 100644 --- a/internal/editor/model_builder.go +++ b/internal/editor/model_builder.go @@ -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 } diff --git a/internal/editor/update.go b/internal/editor/update.go index 93c6f88..5797d36 100644 --- a/internal/editor/update.go +++ b/internal/editor/update.go @@ -1,6 +1,7 @@ package editor import ( + "git.gophernest.net/azpect/TextEditor/internal/core" tea "github.com/charmbracelet/bubbletea" ) @@ -54,7 +55,18 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Type == tea.KeyCtrlC { return m, tea.Quit } - cmd = m.input.Handle(m, msg.String()) + + // 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 diff --git a/internal/editor/view.go b/internal/editor/view.go index 723eeba..ec30a68 100644 --- a/internal/editor/view.go +++ b/internal/editor/view.go @@ -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") +} diff --git a/internal/operator/yank.go b/internal/operator/yank.go index 0d46299..a3132bd 100644 --- a/internal/operator/yank.go +++ b/internal/operator/yank.go @@ -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) diff --git a/internal/style/style.go b/internal/style/style.go index bcdeb43..247f041 100644 --- a/internal/style/style.go +++ b/internal/style/style.go @@ -25,7 +25,9 @@ type Styles struct { StatusBarActive lipgloss.Style // Command line - CommandError lipgloss.Style + 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")), } }