Gim/internal/command/handlers_test.go
Hayden Hargreaves 10e37b82af
All checks were successful
Run Test Suite / test (push) Successful in 13s
feat: implemented the command window! Not tested. Maybe we need some?
2026-03-14 23:13:59 -07:00

3886 lines
102 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package command
import (
"os"
"path/filepath"
"strings"
"testing"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// ==================================================
// Mock Model Implementation
// ==================================================
type mockModel struct {
windows []*core.Window
activeWindow *core.Window
buffers []*core.Buffer
settings core.EditorSettings
mode core.Mode
registers map[rune]core.Register
insertKeys []string
command string
commandCursor int
commandOutput *core.CommandOutput
lastFind core.LastFindCommand
}
func newMockModel() *mockModel {
buf := core.NewBufferBuilder().
WithLines([]string{""}).
Build()
win := core.NewWindowBuilder().
WithBuffer(&buf).
WithHeight(24).
WithWidth(80).
Build()
return &mockModel{
windows: []*core.Window{&win},
activeWindow: &win,
buffers: []*core.Buffer{&buf},
settings: core.NewDefaultSettings(),
mode: core.NormalMode,
registers: core.DefaultRegisters(),
}
}
func newMockModelWithBuffer(buf *core.Buffer) *mockModel {
win := core.NewWindowBuilder().
WithBuffer(buf).
WithHeight(24).
WithWidth(80).
Build()
return &mockModel{
windows: []*core.Window{&win},
activeWindow: &win,
buffers: []*core.Buffer{buf},
settings: core.NewDefaultSettings(),
mode: core.NormalMode,
registers: core.DefaultRegisters(),
}
}
// Core Data Access
func (m *mockModel) Windows() []*core.Window { return m.windows }
func (m *mockModel) ActiveWindow() *core.Window { return m.activeWindow }
func (m *mockModel) Buffers() []*core.Buffer { return m.buffers }
func (m *mockModel) SetBuffers(bufs []*core.Buffer) { m.buffers = bufs }
func (m *mockModel) ActiveBuffer() *core.Buffer { return m.activeWindow.Buffer }
// Insert Mode State
func (m *mockModel) InsertKeys() []string { return m.insertKeys }
func (m *mockModel) SetInsertKeys(keys []string) { m.insertKeys = keys }
func (m *mockModel) SetInsertRecording(count int, action action.Action) {}
func (m *mockModel) ExitInsertMode() {}
func (m *mockModel) SetLastFind(char string, forward, inclusive bool) {
m.lastFind = core.LastFindCommand{Char: char, Forward: forward, Inclusive: inclusive}
}
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) 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 }
func (m *mockModel) SetMode(mode core.Mode) { m.mode = mode }
func (m *mockModel) Settings() core.EditorSettings { return m.settings }
func (m *mockModel) SetSettings(s core.EditorSettings) { m.settings = s }
// Registers
func (m *mockModel) Registers() map[rune]core.Register { return m.registers }
func (m *mockModel) GetRegister(name rune) (core.Register, bool) {
reg, ok := m.registers[name]
return reg, ok
}
func (m *mockModel) SetRegister(name rune, t core.RegisterType, cnt []string) error {
m.registers[name] = core.Register{Type: t, Content: cnt}
return nil
}
func (m *mockModel) UpdateDefaultRegister(t core.RegisterType, cnt []string) {
m.registers['"'] = core.Register{Type: t, Content: cnt}
}
// ==================================================
// cmdWrite Tests
// ==================================================
func TestCmdWrite(t *testing.T) {
t.Run("writes buffer content to file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"line 1", "line 2", "line 3"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
content, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("file not written: %v", err)
}
expected := "line 1\nline 2\nline 3\n"
if string(content) != expected {
t.Errorf("got %q, want %q", string(content), expected)
}
})
t.Run("writes to argument filename instead of buffer filename", func(t *testing.T) {
tmpDir := t.TempDir()
originalFile := filepath.Join(tmpDir, "original.txt")
newFile := filepath.Join(tmpDir, "new.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(originalFile).
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{newFile}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Verify new file was created
if _, err := os.Stat(newFile); os.IsNotExist(err) {
t.Error("new file not created")
}
// Verify original file was NOT created
if _, err := os.Stat(originalFile); !os.IsNotExist(err) {
t.Error("original file should not exist")
}
content, _ := os.ReadFile(newFile)
expected := "content\n"
if string(content) != expected {
t.Errorf("got %q, want %q", string(content), expected)
}
})
t.Run("truncates existing file when overwriting", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
// Write longer content first
originalContent := "this is very long original content\nwith multiple lines\nand more text\n"
os.WriteFile(filename, []byte(originalContent), 0644)
// Create buffer with shorter content
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"short"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
content, _ := os.ReadFile(filename)
expected := "short\n"
if string(content) != expected {
t.Errorf("file not truncated properly: got %q, want %q", string(content), expected)
}
})
t.Run("refuses to write readonly buffer", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"content"}).
ReadOnly().
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for readonly buffer")
}
if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") {
t.Errorf("error should mention readonly: %v", m.commandOutput.Lines)
}
// Verify file was NOT created
if _, err := os.Stat(filename); !os.IsNotExist(err) {
t.Error("file should not be created for readonly buffer")
}
})
t.Run("refuses to write scratch buffer", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename(filename).
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for scratch buffer")
}
if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "ScratchBuffer") {
t.Errorf("error should mention ScratchBuffer: %v", m.commandOutput.Lines)
}
})
t.Run("errors when no filename and buffer has no filename", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error when no filename available")
}
if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "no file name") {
t.Errorf("error should mention no file name: %v", m.commandOutput.Lines)
}
})
t.Run("errors on invalid path", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("/invalid/nonexistent/path/file.txt").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for invalid path")
}
})
t.Run("writes empty buffer", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "empty.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
content, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("file not created: %v", err)
}
// Empty buffer should produce empty file
if string(content) != "" {
t.Errorf("expected empty file, got %q", string(content))
}
})
t.Run("writes buffer with empty lines", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"line 1", "", "line 3", ""}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
content, _ := os.ReadFile(filename)
expected := "line 1\n\nline 3\n\n"
if string(content) != expected {
t.Errorf("got %q, want %q", string(content), expected)
}
})
t.Run("writes buffer with special characters", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "special.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{
"hello\tworld",
"unicode: \u00e9\u00e8\u00ea",
"symbols: !@#$%^&*()",
}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
content, _ := os.ReadFile(filename)
expected := "hello\tworld\nunicode: \u00e9\u00e8\u00ea\nsymbols: !@#$%^&*()\n"
if string(content) != expected {
t.Errorf("got %q, want %q", string(content), expected)
}
})
t.Run("writes buffer with unicode content", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "unicode.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{
"Japanese: \u3053\u3093\u306b\u3061\u306f",
"Chinese: \u4f60\u597d",
"Emoji: \U0001F600\U0001F389\U0001F680",
"Russian: \u041f\u0440\u0438\u0432\u0435\u0442",
}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
content, _ := os.ReadFile(filename)
lines := strings.Split(strings.TrimSuffix(string(content), "\n"), "\n")
if len(lines) != 4 {
t.Errorf("expected 4 lines, got %d", len(lines))
}
if !strings.Contains(lines[0], "\u3053\u3093\u306b\u3061\u306f") {
t.Errorf("Japanese content not preserved: %q", lines[0])
}
})
t.Run("sets correct output message", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"line 1", "line 2"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
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.Lines[0], filename) {
t.Errorf("output should contain filename: %q", m.commandOutput.Lines[0])
}
if !strings.Contains(m.commandOutput.Lines[0], "2L") {
t.Errorf("output should contain line count: %q", m.commandOutput.Lines[0])
}
})
t.Run("writes large file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "large.txt")
// Create buffer with many lines
lines := make([]string, 10000)
for i := range lines {
lines[i] = strings.Repeat("x", 100)
}
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines(lines).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
info, err := os.Stat(filename)
if err != nil {
t.Fatalf("file not created: %v", err)
}
// Each line is 100 chars + newline = 101 bytes * 10000 lines
expectedSize := int64(101 * 10000)
if info.Size() != expectedSize {
t.Errorf("expected size %d, got %d", expectedSize, info.Size())
}
})
t.Run("writes to file with spaces in path", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "file with spaces.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Error("file with spaces not created")
}
})
t.Run("creates parent directories via argument", func(t *testing.T) {
tmpDir := t.TempDir()
// Note: cmdWrite doesn't create parent dirs, so this should fail
filename := filepath.Join(tmpDir, "nonexistent", "nested", "file.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("original.txt").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{filename}, false)
// Current implementation doesn't create parent dirs, so expect error
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error when parent directories don't exist")
}
})
t.Run("clears modified flag after successful write", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
if !m.ActiveBuffer().Modified {
t.Fatal("precondition: buffer should be modified before write")
}
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if m.ActiveBuffer().Modified {
t.Error("buffer should not be modified after successful write")
}
})
t.Run("clears modified flag even when writing to different file", func(t *testing.T) {
tmpDir := t.TempDir()
originalFile := filepath.Join(tmpDir, "original.txt")
newFile := filepath.Join(tmpDir, "copy.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(originalFile).
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{newFile}, false)
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
if m.ActiveBuffer().Modified {
t.Error("buffer modified flag should be cleared after write")
}
// Buffer filename should NOT change (vim behavior)
if m.ActiveBuffer().Filename != originalFile {
t.Errorf("buffer filename should remain %q, got %q",
originalFile, m.ActiveBuffer().Filename)
}
})
t.Run("does not clear modified flag on write error", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("/invalid/nonexistent/path/file.txt").
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Fatal("expected error for invalid path")
}
// Modified flag should remain true on error
if !m.ActiveBuffer().Modified {
t.Error("buffer should still be modified after failed write")
}
})
}
// ==================================================
// cmdEdit Tests
// ==================================================
func TestCmdEdit(t *testing.T) {
t.Run("errors when no argument provided", func(t *testing.T) {
m := newMockModel()
cmdEdit(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error when no argument")
}
if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "requires an argument") {
t.Errorf("error should mention requires argument: %v", m.commandOutput.Lines)
}
})
t.Run("creates new buffer for nonexistent file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "newfile.txt")
m := newMockModel()
initialBufferCount := len(m.buffers)
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Should have added a new buffer
if len(m.buffers) != initialBufferCount+1 {
t.Errorf("expected %d buffers, got %d", initialBufferCount+1, len(m.buffers))
}
// Active window should point to new buffer
if m.activeWindow.Buffer.Filename != filename {
t.Errorf("active buffer filename = %q, want %q",
m.activeWindow.Buffer.Filename, filename)
}
// New buffer should be empty
if m.activeWindow.Buffer.LineCount() != 1 || m.activeWindow.Buffer.Line(0) != "" {
t.Errorf("new buffer should have one empty line")
}
// Buffer should be FileBuffer type
if m.activeWindow.Buffer.Type != core.FileBuffer {
t.Errorf("buffer type = %v, want FileBuffer", m.activeWindow.Buffer.Type)
}
})
t.Run("loads existing file content", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "existing.txt")
// Create file with content
content := "line 1\nline 2\nline 3\n"
os.WriteFile(filename, []byte(content), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
buf := m.activeWindow.Buffer
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
if buf.Line(0) != "line 1" {
t.Errorf("line 0 = %q, want %q", buf.Line(0), "line 1")
}
if buf.Line(1) != "line 2" {
t.Errorf("line 1 = %q, want %q", buf.Line(1), "line 2")
}
if buf.Line(2) != "line 3" {
t.Errorf("line 2 = %q, want %q", buf.Line(2), "line 3")
}
})
t.Run("loads file without trailing newline", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "notrailing.txt")
// File without trailing newline
content := "line 1\nline 2"
os.WriteFile(filename, []byte(content), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
buf := m.activeWindow.Buffer
if buf.LineCount() != 2 {
t.Errorf("expected 2 lines, got %d", buf.LineCount())
}
})
t.Run("loads empty file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "empty.txt")
os.WriteFile(filename, []byte(""), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
buf := m.activeWindow.Buffer
// Empty file should have 0 lines (scanner produces no lines)
if buf.LineCount() != 0 {
t.Errorf("expected 0 lines for empty file, got %d", buf.LineCount())
}
})
t.Run("sets correct filetype from extension", func(t *testing.T) {
tmpDir := t.TempDir()
tests := []struct {
filename string
expected string
}{
{"test.go", ".go"},
{"test.py", ".py"},
{"test.js", ".js"},
{"test.txt", ".txt"},
{"Makefile", ""},
{"test.tar.gz", ".gz"},
}
for _, tt := range tests {
t.Run(tt.filename, func(t *testing.T) {
filename := filepath.Join(tmpDir, tt.filename)
os.WriteFile(filename, []byte("content"), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if m.activeWindow.Buffer.Filetype != tt.expected {
t.Errorf("filetype = %q, want %q", m.activeWindow.Buffer.Filetype, tt.expected)
}
})
}
})
t.Run("converts tabs to spaces based on tabstop", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "tabs.txt")
// File with tabs
content := "\tindented\n\t\tdouble"
os.WriteFile(filename, []byte(content), 0644)
m := newMockModel()
m.settings.TabStop = 4
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
buf := m.activeWindow.Buffer
// Tab should be converted to 4 spaces
expected0 := " indented"
if buf.Line(0) != expected0 {
t.Errorf("line 0 = %q, want %q", buf.Line(0), expected0)
}
expected1 := " double"
if buf.Line(1) != expected1 {
t.Errorf("line 1 = %q, want %q", buf.Line(1), expected1)
}
})
t.Run("handles different tabstop values", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "tabs.txt")
content := "\ttest"
os.WriteFile(filename, []byte(content), 0644)
tests := []struct {
tabstop int
expected string
}{
{2, " test"},
{4, " test"},
{8, " test"},
}
for _, tt := range tests {
t.Run(string(rune('0'+tt.tabstop)), func(t *testing.T) {
m := newMockModel()
m.settings.TabStop = tt.tabstop
cmdEdit(m, []string{filename}, false)
if m.activeWindow.Buffer.Line(0) != tt.expected {
t.Errorf("with tabstop=%d: got %q, want %q",
tt.tabstop, m.activeWindow.Buffer.Line(0), tt.expected)
}
})
}
})
t.Run("loads file with unicode content", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "unicode.txt")
content := "Hello \u4e16\u754c\n\u3053\u3093\u306b\u3061\u306f\nEmoji: \U0001F600\n"
os.WriteFile(filename, []byte(content), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
buf := m.activeWindow.Buffer
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
if !strings.Contains(buf.Line(0), "\u4e16\u754c") {
t.Errorf("unicode not preserved in line 0: %q", buf.Line(0))
}
})
t.Run("loads large file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "large.txt")
// Create large file
var content strings.Builder
for range 10000 {
content.WriteString(strings.Repeat("x", 100))
content.WriteString("\n")
}
os.WriteFile(filename, []byte(content.String()), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
buf := m.activeWindow.Buffer
if buf.LineCount() != 10000 {
t.Errorf("expected 10000 lines, got %d", buf.LineCount())
}
})
t.Run("sets buffer as loaded and listed", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
os.WriteFile(filename, []byte("content"), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
buf := m.activeWindow.Buffer
if !buf.Loaded {
t.Error("buffer should be marked as loaded")
}
if !buf.Listed {
t.Error("buffer should be marked as listed")
}
})
t.Run("new file buffer is loaded and listed", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "newfile.txt")
m := newMockModel()
cmdEdit(m, []string{filename}, false)
buf := m.activeWindow.Buffer
if !buf.Loaded {
t.Error("new buffer should be marked as loaded")
}
if !buf.Listed {
t.Error("new buffer should be marked as listed")
}
})
t.Run("errors on permission denied", func(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("test requires non-root user")
}
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "noperm.txt")
// Create file then remove read permission
os.WriteFile(filename, []byte("content"), 0644)
os.Chmod(filename, 0000)
defer os.Chmod(filename, 0644) // Cleanup
m := newMockModel()
cmdEdit(m, []string{filename}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for permission denied")
}
})
t.Run("handles file with long lines", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "longlines.txt")
// Use 50000 chars - within bufio.Scanner's default 64KB limit
longLine := strings.Repeat("x", 50000)
content := longLine + "\nshort\n" + longLine
os.WriteFile(filename, []byte(content), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
buf := m.activeWindow.Buffer
if len(buf.Line(0)) != 50000 {
t.Errorf("long line not preserved: got %d chars", len(buf.Line(0)))
}
})
t.Run("bufio.Scanner limitation with extremely long lines", func(t *testing.T) {
// NOTE: This documents a known limitation
// bufio.Scanner has a default max token size of ~64KB
// Lines longer than this will cause scanner.Scan() to return false
// with scanner.Err() returning bufio.ErrTooLong
//
// A future improvement would be to use bufio.Reader.ReadString
// or increase scanner buffer size with scanner.Buffer()
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "verylonglines.txt")
// Create line exceeding 64KB limit
veryLongLine := strings.Repeat("x", 100000)
os.WriteFile(filename, []byte(veryLongLine+"\n"), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
// Currently this will result in 0 lines loaded due to scanner limitation
// This test documents the current behavior
buf := m.activeWindow.Buffer
if buf.LineCount() > 0 && len(buf.Line(0)) == 100000 {
t.Log("Scanner handled very long line - limitation may be fixed")
} else {
t.Log("Known limitation: bufio.Scanner cannot handle lines > 64KB")
}
})
t.Run("strips carriage return from CRLF line endings", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "crlf.txt")
// Windows-style line endings
content := "line 1\r\nline 2\r\nline 3\r\n"
os.WriteFile(filename, []byte(content), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
buf := m.activeWindow.Buffer
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
// Verify \r is stripped from all lines
for i := 0; i < buf.LineCount(); i++ {
line := buf.Line(i)
if strings.HasSuffix(line, "\r") {
t.Errorf("line %d has trailing \\r: %q", i, line)
}
if strings.Contains(line, "\r") {
t.Errorf("line %d contains \\r: %q", i, line)
}
}
// Verify content is correct
if buf.Line(0) != "line 1" {
t.Errorf("line 0 = %q, want %q", buf.Line(0), "line 1")
}
if buf.Line(1) != "line 2" {
t.Errorf("line 1 = %q, want %q", buf.Line(1), "line 2")
}
})
t.Run("handles mixed line endings", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "mixed.txt")
// Mix of Unix (\n) and Windows (\r\n) line endings
content := "unix line\nwindows line\r\nunix again\n"
os.WriteFile(filename, []byte(content), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
buf := m.activeWindow.Buffer
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
// All lines should be clean (no \r)
for i := 0; i < buf.LineCount(); i++ {
if strings.Contains(buf.Line(i), "\r") {
t.Errorf("line %d contains \\r: %q", i, buf.Line(i))
}
}
})
t.Run("does not panic when editing nonexistent file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "does_not_exist.txt")
m := newMockModel()
// This should NOT panic (tests the nil pointer fix)
cmdEdit(m, []string{filename}, false)
// Should succeed - creates new buffer
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Buffer should be created with the filename
if m.activeWindow.Buffer.Filename != filename {
t.Errorf("buffer filename = %q, want %q",
m.activeWindow.Buffer.Filename, filename)
}
})
t.Run("adds buffer to buffer list", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
os.WriteFile(filename, []byte("content"), 0644)
m := newMockModel()
initialCount := len(m.buffers)
cmdEdit(m, []string{filename}, false)
if len(m.buffers) != initialCount+1 {
t.Errorf("buffer not added: expected %d, got %d", initialCount+1, len(m.buffers))
}
// Find the new buffer in the list
found := false
for _, buf := range m.buffers {
if buf.Filename == filename {
found = true
break
}
}
if !found {
t.Error("new buffer not found in buffer list")
}
})
t.Run("handles path with spaces", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "file with spaces.txt")
os.WriteFile(filename, []byte("content"), 0644)
m := newMockModel()
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if m.activeWindow.Buffer.Filename != filename {
t.Errorf("filename = %q, want %q", m.activeWindow.Buffer.Filename, filename)
}
})
t.Run("switches to existing buffer instead of creating duplicate", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "existing.txt")
os.WriteFile(filename, []byte("original content"), 0644)
m := newMockModel()
initialBufferCount := len(m.buffers)
// First edit - loads the file
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error on first edit: %v", m.commandOutput.Lines)
}
if len(m.buffers) != initialBufferCount+1 {
t.Errorf("expected %d buffers after first edit, got %d", initialBufferCount+1, len(m.buffers))
}
firstBuffer := m.activeWindow.Buffer
if firstBuffer.Filename != filename {
t.Fatalf("first buffer filename = %q, want %q", firstBuffer.Filename, filename)
}
// Modify the buffer to verify we get the same instance
firstBuffer.SetLine(0, "modified content")
bufferId := firstBuffer.Id
// Second edit - should switch to existing buffer, not create new one
cmdEdit(m, []string{filename}, false)
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
if len(m.buffers) != initialBufferCount+1 {
t.Errorf("expected buffer count to remain %d, got %d (created duplicate buffer)",
initialBufferCount+1, len(m.buffers))
}
// Should be the same buffer instance
if m.activeWindow.Buffer.Id != bufferId {
t.Errorf("expected to switch to existing buffer (ID %d), got different buffer (ID %d)",
bufferId, m.activeWindow.Buffer.Id)
}
// Should have our modifications, not reload from disk
if m.activeWindow.Buffer.Line(0) != "modified content" {
t.Errorf("expected modified buffer content %q, got %q (buffer was reloaded from disk)",
"modified content", m.activeWindow.Buffer.Line(0))
}
})
t.Run("switches to existing buffer even if file was modified on disk", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "file.txt")
os.WriteFile(filename, []byte("version 1"), 0644)
m := newMockModel()
// First edit
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
bufferId := m.activeWindow.Buffer.Id
originalLine := m.activeWindow.Buffer.Line(0)
// Modify file on disk
os.WriteFile(filename, []byte("version 2 - changed on disk"), 0644)
// Second edit - should switch to buffer, not reload from disk
cmdEdit(m, []string{filename}, false)
// Should still be the same buffer
if m.activeWindow.Buffer.Id != bufferId {
t.Errorf("expected same buffer ID %d, got %d", bufferId, m.activeWindow.Buffer.Id)
}
// Should have old content, not reload
if m.activeWindow.Buffer.Line(0) != originalLine {
t.Errorf("buffer was reloaded from disk, expected %q, got %q",
originalLine, m.activeWindow.Buffer.Line(0))
}
// Note: Real vim would warn about file change, but for now we just switch to buffer
})
t.Run("creates new buffer for different file even if similar name", func(t *testing.T) {
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "test.txt")
file2 := filepath.Join(tmpDir, "test2.txt")
os.WriteFile(file1, []byte("file 1"), 0644)
os.WriteFile(file2, []byte("file 2"), 0644)
m := newMockModel()
cmdEdit(m, []string{file1}, false)
bufferCount := len(m.buffers)
cmdEdit(m, []string{file2}, false)
// Should have created a new buffer for different file
if len(m.buffers) != bufferCount+1 {
t.Errorf("expected new buffer for different file, buffer count = %d", len(m.buffers))
}
if m.activeWindow.Buffer.Filename != file2 {
t.Errorf("active buffer filename = %q, want %q", m.activeWindow.Buffer.Filename, file2)
}
})
t.Run("matches buffer by absolute path", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "file.txt")
os.WriteFile(filename, []byte("content"), 0644)
m := newMockModel()
// First edit with absolute path
cmdEdit(m, []string{filename}, false)
bufferId := m.activeWindow.Buffer.Id
bufferCount := len(m.buffers)
// Second edit with same absolute path - should match
cmdEdit(m, []string{filename}, false)
if len(m.buffers) != bufferCount {
t.Error("created duplicate buffer for same absolute path")
}
if m.activeWindow.Buffer.Id != bufferId {
t.Error("did not switch to existing buffer")
}
})
}
// ==================================================
// Round-trip Tests (Edit then Write)
// ==================================================
func TestEditWriteRoundTrip(t *testing.T) {
t.Run("edit then write preserves content", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "roundtrip.txt")
original := "line 1\nline 2\nline 3\n"
os.WriteFile(filename, []byte(original), 0644)
m := newMockModel()
// Edit
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("edit error: %v", m.commandOutput.Lines)
}
// Write
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("write error: %v", m.commandOutput.Lines)
}
// Read back
content, _ := os.ReadFile(filename)
if string(content) != original {
t.Errorf("content not preserved:\ngot: %q\nwant: %q", string(content), original)
}
})
t.Run("edit new file then write creates file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "newfile.txt")
m := newMockModel()
// Edit (creates new buffer)
cmdEdit(m, []string{filename}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("edit error: %v", m.commandOutput.Lines)
}
// Add content to buffer
m.activeWindow.Buffer.InsertLine(0, "hello world")
// Write
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("write error: %v", m.commandOutput.Lines)
}
// Verify file exists
content, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("file not created: %v", err)
}
expected := "hello world\n\n" // Original empty line + inserted line
if string(content) != expected {
t.Errorf("got %q, want %q", string(content), expected)
}
})
t.Run("edit modifies write to different file", func(t *testing.T) {
tmpDir := t.TempDir()
original := filepath.Join(tmpDir, "original.txt")
newFile := filepath.Join(tmpDir, "copy.txt")
os.WriteFile(original, []byte("content\n"), 0644)
m := newMockModel()
// Edit original
cmdEdit(m, []string{original}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("edit error: %v", m.commandOutput.Lines)
}
// Modify
m.activeWindow.Buffer.SetLine(0, "modified content")
// Write to new file
cmdWrite(m, []string{newFile}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("write error: %v", m.commandOutput.Lines)
}
// Verify new file has modified content
content, _ := os.ReadFile(newFile)
if string(content) != "modified content\n" {
t.Errorf("new file content = %q", string(content))
}
// Verify original is unchanged
origContent, _ := os.ReadFile(original)
if string(origContent) != "content\n" {
t.Errorf("original was modified: %q", string(origContent))
}
})
}
// ==================================================
// cmdQuit Tests
// ==================================================
//
// Expected Vim behavior for quit commands with readonly buffers:
//
// :quit / :q
// - Quits if no buffers are modified (readonly or not)
// - Errors if any buffer is modified (readonly or not)
//
// :quitall / :qa
// - Same as :quit but for all windows
//
// :write / :w
// - Errors if buffer is readonly (even if not modified)
//
// :wall / :wa
// - Only writes MODIFIED buffers
// - Skips unmodified buffers (including readonly ones)
// - Errors if any MODIFIED buffer is readonly
//
// :wq
// - Writes current buffer and quits
// - Errors if buffer is readonly AND modified
// - If buffer is NOT modified, just quits (no write needed)
//
// :wqall / :wqa
// - Writes all MODIFIED buffers and quits
// - Skips unmodified buffers (including readonly ones)
// - Errors if any MODIFIED buffer is readonly
//
// Force variants (with !) override readonly protection
// ==================================================
func TestCmdQuit(t *testing.T) {
t.Run("quits when no buffers are modified", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithFilename("test.txt").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdQuit(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command, got nil")
}
// Execute the command and verify it returns quit message
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("errors when buffer is modified", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithFilename("unsaved.txt").
WithLines([]string{"unsaved content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdQuit(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for modified buffer")
}
if cmd != nil {
t.Error("should not return quit command when buffer is modified")
}
})
t.Run("error message includes filename of modified buffer", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithFilename("important_file.txt").
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdQuit(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Fatal("expected error")
}
if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "important_file.txt") {
t.Errorf("error should mention filename: %v", m.commandOutput.Lines)
}
})
t.Run("errors when any buffer in list is modified", func(t *testing.T) {
// Active buffer is not modified
activeBuf := core.NewBufferBuilder().
WithFilename("active.txt").
WithLines([]string{"content"}).
Build()
activeBuf.Modified = false
// Another buffer IS modified
modifiedBuf := core.NewBufferBuilder().
WithFilename("modified.txt").
WithLines([]string{"unsaved"}).
Build()
modifiedBuf.Modified = true
m := newMockModelWithBuffer(&activeBuf)
m.buffers = append(m.buffers, &modifiedBuf)
cmd := cmdQuit(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error when any buffer is modified")
}
if cmd != nil {
t.Error("should not return quit command")
}
})
t.Run("switches to modified buffer when quitting fails", func(t *testing.T) {
activeBuf := core.NewBufferBuilder().
WithFilename("active.txt").
WithLines([]string{"content"}).
Build()
activeBuf.Modified = false
modifiedBuf := core.NewBufferBuilder().
WithFilename("modified.txt").
WithLines([]string{"unsaved"}).
Build()
modifiedBuf.Modified = true
m := newMockModelWithBuffer(&activeBuf)
m.buffers = append(m.buffers, &modifiedBuf)
cmdQuit(m, []string{}, false)
// Should switch active window to show the modified buffer
if m.activeWindow.Buffer.Filename != "modified.txt" {
t.Errorf("should switch to modified buffer, got %q",
m.activeWindow.Buffer.Filename)
}
})
t.Run("handles buffer with no filename", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithFilename("").
WithLines([]string{"unsaved content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdQuit(m, []string{}, false)
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(strings.Join(m.commandOutput.Lines, " "), "unsaved") {
t.Logf("Note: error message for unnamed buffer: %v", m.commandOutput.Lines)
}
})
t.Run("quits with multiple unmodified buffers", func(t *testing.T) {
buf1 := core.NewBufferBuilder().
WithFilename("file1.txt").
WithLines([]string{"content"}).
Build()
buf1.Modified = false
buf2 := core.NewBufferBuilder().
WithFilename("file2.txt").
WithLines([]string{"content"}).
Build()
buf2.Modified = false
buf3 := core.NewBufferBuilder().
WithFilename("file3.txt").
WithLines([]string{"content"}).
Build()
buf3.Modified = false
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2, &buf3)
cmd := cmdQuit(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Error("expected quit command")
}
})
}
// ==================================================
// cmdQuitAll Tests
// ==================================================
func TestCmdQuitAll(t *testing.T) {
t.Run("quits when no buffers are modified", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithFilename("test.txt").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdQuitAll(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command, got nil")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("errors when any buffer is modified", func(t *testing.T) {
buf1 := core.NewBufferBuilder().
WithFilename("saved.txt").
WithLines([]string{"content"}).
Build()
buf1.Modified = false
buf2 := core.NewBufferBuilder().
WithFilename("unsaved.txt").
WithLines([]string{"modified content"}).
Build()
buf2.Modified = true
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmd := cmdQuitAll(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error when any buffer is modified")
}
if cmd != nil {
t.Error("should not return quit command")
}
})
t.Run("quits with multiple unmodified buffers", func(t *testing.T) {
buf1 := core.NewBufferBuilder().
WithFilename("file1.txt").
Build()
buf1.Modified = false
buf2 := core.NewBufferBuilder().
WithFilename("file2.txt").
Build()
buf2.Modified = false
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmd := cmdQuitAll(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Error("expected quit command")
}
})
t.Run("error mentions which buffer is modified", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithFilename("important_document.txt").
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdQuitAll(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Fatal("expected error")
}
if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "important_document.txt") {
t.Errorf("error should mention filename: %v", m.commandOutput.Lines)
}
})
t.Run("reports first modified buffer found", func(t *testing.T) {
buf1 := core.NewBufferBuilder().
WithFilename("first_modified.txt").
Build()
buf1.Modified = true
buf2 := core.NewBufferBuilder().
WithFilename("second_modified.txt").
Build()
buf2.Modified = true
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmdQuitAll(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Fatal("expected error")
}
// Should report first modified buffer
if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "first_modified.txt") {
t.Errorf("should report first modified buffer: %v", m.commandOutput.Lines)
}
})
}
// ==================================================
// cmdQuitForce Tests (q!)
// ==================================================
func TestCmdQuitForce(t *testing.T) {
t.Run("quits with modified buffer", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithFilename("unsaved.txt").
WithLines([]string{"unsaved content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdQuit(m, []string{}, true)
// Should NOT error
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Should return quit command
if cmd == nil {
t.Fatal("expected quit command, got nil")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with readonly modified buffer", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithFilename("readonly.txt").
WithLines([]string{"modified content"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdQuit(m, []string{}, true)
// Force quit should work even with readonly modified buffer
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with multiple modified buffers", func(t *testing.T) {
buf1 := core.NewBufferBuilder().
WithFilename("file1.txt").
WithLines([]string{"unsaved 1"}).
Modified().
Build()
buf2 := core.NewBufferBuilder().
WithFilename("file2.txt").
WithLines([]string{"unsaved 2"}).
Modified().
Build()
buf3 := core.NewBufferBuilder().
WithFilename("file3.txt").
WithLines([]string{"unsaved 3"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2, &buf3)
cmd := cmdQuit(m, []string{}, true)
// Should quit regardless of multiple modified buffers
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with unmodified buffers", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithFilename("test.txt").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdQuit(m, []string{}, false)
// Should work with unmodified buffers too
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with buffer without filename", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithFilename("").
WithLines([]string{"unsaved content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdQuit(m, []string{}, true)
// Force quit works even without filename
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with scratch buffer", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("scratch").
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdQuit(m, []string{}, true)
// Force quit works with scratch buffers
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with mix of modified and unmodified buffers", func(t *testing.T) {
modifiedBuf := core.NewBufferBuilder().
WithFilename("modified.txt").
WithLines([]string{"unsaved"}).
Modified().
Build()
unmodifiedBuf := core.NewBufferBuilder().
WithFilename("unmodified.txt").
WithLines([]string{"saved"}).
Build()
readonlyModifiedBuf := core.NewBufferBuilder().
WithFilename("readonly.txt").
WithLines([]string{"readonly unsaved"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&modifiedBuf)
m.buffers = append(m.buffers, &unmodifiedBuf, &readonlyModifiedBuf)
cmd := cmdQuit(m, []string{}, true)
// Should quit regardless of buffer states
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("does not switch buffers", func(t *testing.T) {
activeBuf := core.NewBufferBuilder().
WithFilename("active.txt").
WithLines([]string{"active"}).
Build()
modifiedBuf := core.NewBufferBuilder().
WithFilename("modified.txt").
WithLines([]string{"unsaved"}).
Modified().
Build()
m := newMockModelWithBuffer(&activeBuf)
m.buffers = append(m.buffers, &modifiedBuf)
// Remember active buffer
activeFilename := m.activeWindow.Buffer.Filename
cmdQuit(m, []string{}, true)
// Should NOT switch to modified buffer (unlike regular quit)
if m.activeWindow.Buffer.Filename != activeFilename {
t.Errorf("should not switch buffers, got %q", m.activeWindow.Buffer.Filename)
}
})
}
// ==================================================
// cmdQuitAllForce Tests (qall! / qa!)
// ==================================================
func TestCmdQuitAllForce(t *testing.T) {
t.Run("quits with all buffers modified", func(t *testing.T) {
buf1 := core.NewBufferBuilder().
WithFilename("file1.txt").
WithLines([]string{"unsaved 1"}).
Modified().
Build()
buf2 := core.NewBufferBuilder().
WithFilename("file2.txt").
WithLines([]string{"unsaved 2"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmd := cmdQuitAll(m, []string{}, true)
// Should quit even with all buffers modified
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command, got nil")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with mix of modified and unmodified", func(t *testing.T) {
buf1 := core.NewBufferBuilder().
WithFilename("saved.txt").
WithLines([]string{"content"}).
Build()
buf2 := core.NewBufferBuilder().
WithFilename("unsaved.txt").
WithLines([]string{"modified content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmd := cmdQuitAll(m, []string{}, true)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with readonly buffers", func(t *testing.T) {
buf1 := core.NewBufferBuilder().
WithFilename("readonly1.txt").
WithLines([]string{"content"}).
ReadOnly().
Modified().
Build()
buf2 := core.NewBufferBuilder().
WithFilename("readonly2.txt").
WithLines([]string{"content"}).
ReadOnly().
Build()
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmd := cmdQuitAll(m, []string{}, true)
// Force quit should work with readonly buffers
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with scratch buffers", func(t *testing.T) {
buf1 := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("scratch1").
WithLines([]string{"content"}).
Modified().
Build()
buf2 := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("scratch2").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmd := cmdQuitAll(m, []string{}, true)
// Should quit with scratch buffers
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with buffers without filenames", func(t *testing.T) {
buf1 := core.NewBufferBuilder().
WithFilename("").
WithLines([]string{"unsaved 1"}).
Modified().
Build()
buf2 := core.NewBufferBuilder().
WithFilename("").
WithLines([]string{"unsaved 2"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmd := cmdQuitAll(m, []string{}, true)
// Force quit works even without filenames
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with no buffers", func(t *testing.T) {
m := newMockModel()
// Clear buffers
m.buffers = []*core.Buffer{}
cmd := cmdQuitAll(m, []string{}, true)
// Should quit even with no buffers
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with complex buffer states", func(t *testing.T) {
// Mix of all different buffer states
buf1 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("file.txt").
WithLines([]string{"modified file"}).
Modified().
Build()
buf2 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("readonly.txt").
WithLines([]string{"readonly modified"}).
ReadOnly().
Modified().
Build()
buf3 := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("scratch").
WithLines([]string{"scratch modified"}).
Modified().
Build()
buf4 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("").
WithLines([]string{"no name"}).
Modified().
Build()
buf5 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("saved.txt").
WithLines([]string{"saved content"}).
Build()
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2, &buf3, &buf4, &buf5)
cmd := cmdQuitAll(m, []string{}, true)
// Force quit should work regardless of any buffer state
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("quits with all buffers unmodified", func(t *testing.T) {
buf1 := core.NewBufferBuilder().
WithFilename("file1.txt").
WithLines([]string{"content"}).
Build()
buf2 := core.NewBufferBuilder().
WithFilename("file2.txt").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmd := cmdQuitAll(m, []string{}, true)
// Should work with unmodified buffers too
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
}
// ==================================================
// cmdWriteAll Tests
// ==================================================
func TestCmdWriteAll(t *testing.T) {
t.Run("writes all modified buffers", func(t *testing.T) {
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "file1.txt")
file2 := filepath.Join(tmpDir, "file2.txt")
buf1 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file1).
WithLines([]string{"content 1"}).
Build()
buf1.Modified = true
buf2 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file2).
WithLines([]string{"content 2"}).
Build()
buf2.Modified = true
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmdWriteAll(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Verify both files were written
content1, err := os.ReadFile(file1)
if err != nil {
t.Errorf("file1 not written: %v", err)
} else if string(content1) != "content 1\n" {
t.Errorf("file1 content = %q", string(content1))
}
content2, err := os.ReadFile(file2)
if err != nil {
t.Errorf("file2 not written: %v", err)
} else if string(content2) != "content 2\n" {
t.Errorf("file2 content = %q", string(content2))
}
})
t.Run("skips unmodified buffers", func(t *testing.T) {
tmpDir := t.TempDir()
modifiedFile := filepath.Join(tmpDir, "modified.txt")
unmodifiedFile := filepath.Join(tmpDir, "unmodified.txt")
modifiedBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(modifiedFile).
WithLines([]string{"new content"}).
Build()
modifiedBuf.Modified = true
unmodifiedBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(unmodifiedFile).
WithLines([]string{"old content"}).
Build()
unmodifiedBuf.Modified = false
m := newMockModelWithBuffer(&modifiedBuf)
m.buffers = append(m.buffers, &unmodifiedBuf)
cmdWriteAll(m, []string{}, false)
// Modified file should be written
if _, err := os.Stat(modifiedFile); os.IsNotExist(err) {
t.Error("modified file should be written")
}
// Unmodified file should NOT be written (doesn't exist)
if _, err := os.Stat(unmodifiedFile); !os.IsNotExist(err) {
t.Error("unmodified file should not be written")
}
})
t.Run("clears modified flag on written buffers", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWriteAll(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if buf.Modified {
t.Error("modified flag should be cleared after write")
}
})
t.Run("errors on modified readonly buffer", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("readonly.txt").
WithLines([]string{"content"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWriteAll(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Fatal("expected error for modified readonly buffer")
}
if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") {
t.Errorf("error should mention readonly: %v", m.commandOutput.Lines)
}
})
t.Run("skips unmodified readonly buffer", func(t *testing.T) {
tmpDir := t.TempDir()
writableFile := filepath.Join(tmpDir, "writable.txt")
writableBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(writableFile).
WithLines([]string{"writable content"}).
Modified().
Build()
// Readonly but NOT modified - should be skipped
readonlyBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("readonly.txt").
WithLines([]string{"readonly content"}).
ReadOnly().
Build()
// NOT modified!
m := newMockModelWithBuffer(&writableBuf)
m.buffers = append(m.buffers, &readonlyBuf)
cmdWriteAll(m, []string{}, false)
// Should succeed - readonly buffer not modified, so skip it
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
content, _ := os.ReadFile(writableFile)
if string(content) != "writable content\n" {
t.Errorf("writable buffer not written: %q", string(content))
}
})
t.Run("errors on scratch buffer", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("scratch").
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWriteAll(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for scratch buffer")
}
})
t.Run("errors on buffer with no filename", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("").
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWriteAll(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for buffer without filename")
}
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)
}
})
t.Run("succeeds with no modified buffers", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("unchanged.txt").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWriteAll(m, []string{}, false)
// Should succeed even with nothing to write
if m.commandOutput != nil && m.commandOutput.IsError {
t.Errorf("unexpected error: %v", m.commandOutput.Lines)
}
})
t.Run("reports number of buffers written", func(t *testing.T) {
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "file1.txt")
file2 := filepath.Join(tmpDir, "file2.txt")
buf1 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file1).
WithLines([]string{"content"}).
Build()
buf1.Modified = true
buf2 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file2).
WithLines([]string{"content"}).
Build()
buf2.Modified = true
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmdWriteAll(m, []string{}, false)
// Should have some output indicating what was written
if m.commandOutput == nil || len(m.commandOutput.Lines) == 0 {
t.Log("Note: cmdWriteAll doesn't set output message")
} else if !strings.Contains(m.commandOutput.Lines[0], "2") {
t.Logf("Output message: %q", m.commandOutput.Lines[0])
}
})
t.Run("stops on first error", func(t *testing.T) {
tmpDir := t.TempDir()
validFile := filepath.Join(tmpDir, "valid.txt")
// First buffer - valid
validBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(validFile).
WithLines([]string{"content"}).
Build()
validBuf.Modified = true
// Second buffer - invalid (no filename)
invalidBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("").
WithLines([]string{"content"}).
Build()
invalidBuf.Modified = true
m := newMockModelWithBuffer(&invalidBuf) // Invalid first
m.buffers = append(m.buffers, &validBuf)
cmdWriteAll(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error")
}
// Valid file might or might not be written depending on implementation
// (stops on first error vs continues and reports all errors)
})
}
// ==================================================
// cmdWriteQuit Tests
// ==================================================
func TestCmdWriteQuit(t *testing.T) {
t.Run("writes buffer and quits", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"content to save"}).
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdWriteQuit(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Verify file was written
content, err := os.ReadFile(filename)
if err != nil {
t.Errorf("file not written: %v", err)
} else if string(content) != "content to save\n" {
t.Errorf("file content = %q", string(content))
}
// Verify quit command returned
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("errors on readonly buffer", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("readonly.txt").
WithLines([]string{"content"}).
ReadOnly().
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdWriteQuit(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for readonly buffer")
}
if cmd != nil {
t.Error("should not return quit command on error")
}
})
t.Run("errors on scratch buffer", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("scratch").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdWriteQuit(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for scratch buffer")
}
if cmd != nil {
t.Error("should not return quit command on error")
}
})
t.Run("errors on buffer with no filename", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdWriteQuit(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for buffer without filename")
}
if cmd != nil {
t.Error("should not return quit command on error")
}
})
t.Run("errors on invalid path", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("/invalid/nonexistent/path/file.txt").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdWriteQuit(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for invalid path")
}
if cmd != nil {
t.Error("should not return quit command on error")
}
})
t.Run("writes to argument filename if provided", func(t *testing.T) {
tmpDir := t.TempDir()
originalFile := filepath.Join(tmpDir, "original.txt")
newFile := filepath.Join(tmpDir, "new.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(originalFile).
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdWriteQuit(m, []string{newFile}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// New file should exist
if _, err := os.Stat(newFile); os.IsNotExist(err) {
t.Error("new file not created")
}
// Original should not exist
if _, err := os.Stat(originalFile); !os.IsNotExist(err) {
t.Error("original file should not exist")
}
if cmd == nil {
t.Error("expected quit command")
}
})
t.Run("clears modified flag before quit", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWriteQuit(m, []string{}, false)
if buf.Modified {
t.Error("modified flag should be cleared")
}
})
}
// ==================================================
// cmdWriteQuitAll Tests (wqall / wqa / xa)
// ==================================================
func TestCmdWriteQuitAll(t *testing.T) {
// Note: This command may not be implemented yet
// These tests define expected behavior
t.Run("writes all buffers and quits", func(t *testing.T) {
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "file1.txt")
file2 := filepath.Join(tmpDir, "file2.txt")
buf1 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file1).
WithLines([]string{"content 1"}).
Build()
buf1.Modified = true
buf2 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file2).
WithLines([]string{"content 2"}).
Build()
buf2.Modified = true
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmd := cmdWriteQuitAll(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Verify files were written
content1, _ := os.ReadFile(file1)
if string(content1) != "content 1\n" {
t.Errorf("file1 content = %q", string(content1))
}
content2, _ := os.ReadFile(file2)
if string(content2) != "content 2\n" {
t.Errorf("file2 content = %q", string(content2))
}
// Verify quit command
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("skips unmodified buffers", func(t *testing.T) {
tmpDir := t.TempDir()
modifiedFile := filepath.Join(tmpDir, "modified.txt")
unmodifiedFile := filepath.Join(tmpDir, "unmodified.txt")
modifiedBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(modifiedFile).
WithLines([]string{"new content"}).
Build()
modifiedBuf.Modified = true
unmodifiedBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(unmodifiedFile).
WithLines([]string{"unchanged"}).
Build()
unmodifiedBuf.Modified = false
m := newMockModelWithBuffer(&modifiedBuf)
m.buffers = append(m.buffers, &unmodifiedBuf)
cmd := cmdWriteQuitAll(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Should still quit even if some buffers unmodified
if cmd == nil {
t.Error("expected quit command")
}
})
t.Run("errors if readonly buffer is modified", func(t *testing.T) {
tmpDir := t.TempDir()
validFile := filepath.Join(tmpDir, "valid.txt")
validBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(validFile).
WithLines([]string{"content"}).
Modified().
Build()
// Readonly buffer that IS modified - should cause error
readonlyBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("readonly.txt").
WithLines([]string{"content"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&validBuf)
m.buffers = append(m.buffers, &readonlyBuf)
cmd := cmdWriteQuitAll(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for modified readonly buffer")
}
if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") {
t.Errorf("error should mention readonly: %v", m.commandOutput.Lines)
}
if cmd != nil {
t.Error("should not quit when readonly buffer has unsaved changes")
}
})
t.Run("quits with readonly buffer that is NOT modified", func(t *testing.T) {
tmpDir := t.TempDir()
writableFile := filepath.Join(tmpDir, "writable.txt")
writableBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(writableFile).
WithLines([]string{"modified content"}).
Modified().
Build()
// Readonly buffer that is NOT modified - should be skipped, not prevent quit
readonlyBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("readonly.txt").
WithLines([]string{"unchanged content"}).
ReadOnly().
Build()
// NOT modified - this is the key
m := newMockModelWithBuffer(&writableBuf)
m.buffers = append(m.buffers, &readonlyBuf)
cmd := cmdWriteQuitAll(m, []string{}, false)
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
content, _ := os.ReadFile(writableFile)
if string(content) != "modified content\n" {
t.Errorf("writable buffer not written: %q", string(content))
}
// Should return quit command
if cmd == nil {
t.Error("expected quit command (readonly buffer not modified)")
}
})
t.Run("errors if any buffer has no filename", func(t *testing.T) {
noNameBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("").
WithLines([]string{"content"}).
Build()
noNameBuf.Modified = true
m := newMockModelWithBuffer(&noNameBuf)
cmd := cmdWriteQuitAll(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for buffer without filename")
}
if cmd != nil {
t.Error("should not quit when write fails")
}
})
t.Run("quits with no modified buffers", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("unchanged.txt").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdWriteQuitAll(m, []string{}, false)
// Should still quit even if nothing to write
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Error("expected quit command")
}
})
t.Run("clears modified flags on all buffers", func(t *testing.T) {
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "file1.txt")
file2 := filepath.Join(tmpDir, "file2.txt")
buf1 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file1).
WithLines([]string{"content"}).
Build()
buf1.Modified = true
buf2 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file2).
WithLines([]string{"content"}).
Build()
buf2.Modified = true
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmdWriteQuitAll(m, []string{}, false)
if buf1.Modified {
t.Error("buf1 modified flag should be cleared")
}
if buf2.Modified {
t.Error("buf2 modified flag should be cleared")
}
})
}
// ==================================================
// Force Write Tests (w!)
// ==================================================
func TestCmdWriteForce(t *testing.T) {
t.Run("force writes readonly buffer to its own file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "readonly.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"forced content"}).
ReadOnly().
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, true)
// Force should bypass readonly check
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Verify file was written
content, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("file not written: %v", err)
}
expected := "forced content\n"
if string(content) != expected {
t.Errorf("got %q, want %q", string(content), expected)
}
})
t.Run("force writes readonly buffer to argument file", func(t *testing.T) {
tmpDir := t.TempDir()
originalFile := filepath.Join(tmpDir, "readonly_original.txt")
newFile := filepath.Join(tmpDir, "new_destination.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(originalFile).
WithLines([]string{"readonly content"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{newFile}, true)
// Should succeed with force
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Verify new file was created
content, err := os.ReadFile(newFile)
if err != nil {
t.Fatalf("new file not created: %v", err)
}
expected := "readonly content\n"
if string(content) != expected {
t.Errorf("got %q, want %q", string(content), expected)
}
// Original should not exist (never written)
if _, err := os.Stat(originalFile); !os.IsNotExist(err) {
t.Error("original file should not exist")
}
})
t.Run("force writes scratch buffer to argument file", func(t *testing.T) {
tmpDir := t.TempDir()
targetFile := filepath.Join(tmpDir, "from_scratch.txt")
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("scratch").
WithLines([]string{"scratch content", "line 2"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{targetFile}, true)
// Force with filename should work for scratch buffer
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Verify file was written
content, err := os.ReadFile(targetFile)
if err != nil {
t.Fatalf("file not written: %v", err)
}
expected := "scratch content\nline 2\n"
if string(content) != expected {
t.Errorf("got %q, want %q", string(content), expected)
}
})
t.Run("force writes readonly scratch buffer to argument file", func(t *testing.T) {
tmpDir := t.TempDir()
targetFile := filepath.Join(tmpDir, "from_readonly_scratch.txt")
// Readonly scratch buffer - double protection
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("scratch").
WithLines([]string{"protected content"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{targetFile}, true)
// Force should bypass BOTH readonly AND scratch checks
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error with force: %v", m.commandOutput.Lines)
}
// Verify file was written
content, err := os.ReadFile(targetFile)
if err != nil {
t.Fatalf("file not written: %v", err)
}
expected := "protected content\n"
if string(content) != expected {
t.Errorf("got %q, want %q", string(content), expected)
}
// Verify modified flag was cleared
if buf.Modified {
t.Error("modified flag should be cleared after successful write")
}
})
t.Run("force write scratch buffer without filename still errors", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, true)
// Force doesn't help if there's no filename to write to
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error when no filename provided")
}
if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "no file name") {
t.Errorf("error should mention no file name: %v", m.commandOutput.Lines)
}
})
t.Run("force write to invalid path still errors", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("/invalid/nonexistent/path/file.txt").
WithLines([]string{"content"}).
ReadOnly().
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, true)
// Force bypasses buffer checks but not OS-level checks
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for invalid path even with force")
}
})
t.Run("force write without force flag still errors on readonly", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "readonly.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"content"}).
ReadOnly().
Build()
m := newMockModelWithBuffer(&buf)
// force=false should still prevent write
cmdWrite(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for readonly without force flag")
}
if !strings.Contains(strings.Join(m.commandOutput.Lines, " "), "readonly") {
t.Errorf("error should mention readonly: %v", m.commandOutput.Lines)
}
})
t.Run("force write clears modified flag even for readonly buffer", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"content"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&buf)
if !buf.Modified {
t.Fatal("precondition: buffer should be modified")
}
cmdWrite(m, []string{}, true)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Modified flag should be cleared
if buf.Modified {
t.Error("modified flag should be cleared after force write")
}
})
t.Run("force write preserves buffer type", func(t *testing.T) {
tmpDir := t.TempDir()
targetFile := filepath.Join(tmpDir, "output.txt")
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("scratch").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{targetFile}, true)
// Buffer type should remain ScratchBuffer
if buf.Type != core.ScatchBuffer {
t.Errorf("buffer type changed to %v, should remain ScatchBuffer", buf.Type)
}
// Filename should NOT change (Vim behavior)
if buf.Filename != "scratch" {
t.Errorf("buffer filename changed to %q, should remain 'scratch'", buf.Filename)
}
})
t.Run("force write with empty scratch buffer", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "empty.txt")
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("").
WithLines([]string{}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{filename}, true)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Should create empty file
content, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("file not created: %v", err)
}
if string(content) != "" {
t.Errorf("expected empty file, got %q", string(content))
}
})
}
// ==================================================
// Force WriteAll Tests (wall!)
// ==================================================
func TestCmdWriteAllForce(t *testing.T) {
t.Run("force writes all modified readonly buffers", func(t *testing.T) {
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "readonly1.txt")
file2 := filepath.Join(tmpDir, "readonly2.txt")
buf1 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file1).
WithLines([]string{"content 1"}).
ReadOnly().
Modified().
Build()
buf2 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file2).
WithLines([]string{"content 2"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmdWriteAll(m, []string{}, true)
// Should succeed with force
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Both files should be written
content1, err := os.ReadFile(file1)
if err != nil {
t.Errorf("file1 not written: %v", err)
} else if string(content1) != "content 1\n" {
t.Errorf("file1 content = %q", string(content1))
}
content2, err := os.ReadFile(file2)
if err != nil {
t.Errorf("file2 not written: %v", err)
} else if string(content2) != "content 2\n" {
t.Errorf("file2 content = %q", string(content2))
}
})
t.Run("force writeall still errors on scratch buffer without filename argument", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("").
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWriteAll(m, []string{}, true)
// Force can bypass scratch type check, but if buffer has no filename
// it should still error with "no file name provided"
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for scratch buffer without filename")
}
})
t.Run("force writeall with mix of protected buffer types", func(t *testing.T) {
tmpDir := t.TempDir()
readonlyFile := filepath.Join(tmpDir, "readonly.txt")
normalFile := filepath.Join(tmpDir, "normal.txt")
readonlyBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(readonlyFile).
WithLines([]string{"readonly content"}).
ReadOnly().
Modified().
Build()
normalBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(normalFile).
WithLines([]string{"normal content"}).
Modified().
Build()
unmodifiedBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filepath.Join(tmpDir, "unmodified.txt")).
WithLines([]string{"unchanged"}).
Build()
m := newMockModelWithBuffer(&readonlyBuf)
m.buffers = append(m.buffers, &normalBuf, &unmodifiedBuf)
cmdWriteAll(m, []string{}, true)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Both modified files should be written
if _, err := os.Stat(readonlyFile); os.IsNotExist(err) {
t.Error("readonly file should be written with force")
}
if _, err := os.Stat(normalFile); os.IsNotExist(err) {
t.Error("normal file should be written")
}
// Unmodified should be skipped (doesn't exist)
unmodifiedFile := filepath.Join(tmpDir, "unmodified.txt")
if _, err := os.Stat(unmodifiedFile); !os.IsNotExist(err) {
t.Error("unmodified file should not be written")
}
})
t.Run("force writeall clears modified flags", func(t *testing.T) {
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "file1.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file1).
WithLines([]string{"content"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmdWriteAll(m, []string{}, true)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
if buf.Modified {
t.Error("modified flag should be cleared")
}
})
}
// ==================================================
// Force WriteQuit Tests (wq!)
// ==================================================
func TestCmdWriteQuitForce(t *testing.T) {
t.Run("force write-quit with readonly buffer", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "readonly.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"content"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdWriteQuit(m, []string{}, true)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// File should be written
content, err := os.ReadFile(filename)
if err != nil {
t.Errorf("file not written: %v", err)
} else if string(content) != "content\n" {
t.Errorf("content = %q", string(content))
}
// Should quit
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("force write-quit with scratch buffer and filename arg", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "output.txt")
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("scratch").
WithLines([]string{"scratch data"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdWriteQuit(m, []string{filename}, true)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// File should be written
content, err := os.ReadFile(filename)
if err != nil {
t.Errorf("file not written: %v", err)
} else if string(content) != "scratch data\n" {
t.Errorf("content = %q", string(content))
}
if cmd == nil {
t.Error("expected quit command")
}
})
t.Run("force write-quit without filename on scratch buffer still errors", func(t *testing.T) {
buf := core.NewBufferBuilder().
WithType(core.ScatchBuffer).
WithFilename("").
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdWriteQuit(m, []string{}, true)
// Can't write without a filename even with force
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error when no filename")
}
if cmd != nil {
t.Error("should not quit on error")
}
})
}
// ==================================================
// Force WriteQuitAll Tests (wqall! / wqa! / xa!)
// ==================================================
func TestCmdWriteQuitAllForce(t *testing.T) {
t.Run("force write-quit-all with readonly buffers", func(t *testing.T) {
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "readonly1.txt")
file2 := filepath.Join(tmpDir, "readonly2.txt")
buf1 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file1).
WithLines([]string{"content 1"}).
ReadOnly().
Modified().
Build()
buf2 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file2).
WithLines([]string{"content 2"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmd := cmdWriteQuitAll(m, []string{}, true)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Both files should be written
content1, _ := os.ReadFile(file1)
if string(content1) != "content 1\n" {
t.Errorf("file1 content = %q", string(content1))
}
content2, _ := os.ReadFile(file2)
if string(content2) != "content 2\n" {
t.Errorf("file2 content = %q", string(content2))
}
// Should quit
if cmd == nil {
t.Fatal("expected quit command")
}
msg := cmd()
if _, ok := msg.(tea.QuitMsg); !ok {
t.Errorf("expected tea.QuitMsg, got %T", msg)
}
})
t.Run("force write-quit-all with mix of buffer types", func(t *testing.T) {
tmpDir := t.TempDir()
readonlyFile := filepath.Join(tmpDir, "readonly.txt")
normalFile := filepath.Join(tmpDir, "normal.txt")
readonlyBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(readonlyFile).
WithLines([]string{"readonly"}).
ReadOnly().
Modified().
Build()
normalBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(normalFile).
WithLines([]string{"normal"}).
Modified().
Build()
unmodifiedBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("unmodified.txt").
WithLines([]string{"unchanged"}).
Build()
m := newMockModelWithBuffer(&readonlyBuf)
m.buffers = append(m.buffers, &normalBuf, &unmodifiedBuf)
cmd := cmdWriteQuitAll(m, []string{}, true)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Modified buffers should be written
if _, err := os.Stat(readonlyFile); os.IsNotExist(err) {
t.Error("readonly file should be written")
}
if _, err := os.Stat(normalFile); os.IsNotExist(err) {
t.Error("normal file should be written")
}
if cmd == nil {
t.Error("expected quit command")
}
})
t.Run("force write-quit-all still errors on buffer without filename", func(t *testing.T) {
tmpDir := t.TempDir()
validFile := filepath.Join(tmpDir, "valid.txt")
validBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(validFile).
WithLines([]string{"content"}).
Modified().
Build()
noNameBuf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename("").
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&validBuf)
m.buffers = append(m.buffers, &noNameBuf)
cmd := cmdWriteQuitAll(m, []string{}, true)
// Force can't help when there's no filename
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for buffer without filename")
}
if cmd != nil {
t.Error("should not quit when write fails")
}
})
t.Run("force write-quit-all clears all modified flags", func(t *testing.T) {
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "file1.txt")
file2 := filepath.Join(tmpDir, "file2.txt")
buf1 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file1).
WithLines([]string{"content"}).
ReadOnly().
Modified().
Build()
buf2 := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(file2).
WithLines([]string{"content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf1)
m.buffers = append(m.buffers, &buf2)
cmdWriteQuitAll(m, []string{}, true)
if buf1.Modified {
t.Error("buf1 modified flag should be cleared")
}
if buf2.Modified {
t.Error("buf2 modified flag should be cleared")
}
})
}
// ==================================================
// Edge Case Tests
// ==================================================
func TestEdgeCases(t *testing.T) {
t.Run("write to file that becomes readonly during editing", func(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("test requires non-root user")
}
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
// Create file
os.WriteFile(filename, []byte("original"), 0644)
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"modified content"}).
Modified().
Build()
// Make file readonly on filesystem (different from buffer readonly flag)
os.Chmod(filename, 0444)
defer os.Chmod(filename, 0644) // Cleanup
m := newMockModelWithBuffer(&buf)
// Regular write should fail (OS permission denied)
cmdWrite(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error for OS-level readonly file")
}
})
t.Run("write unmodified readonly buffer is allowed with force", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "readonly.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"content"}).
ReadOnly().
Build()
buf.Modified = false
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, true)
// Force write should work even for unmodified readonly
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
content, _ := os.ReadFile(filename)
if string(content) != "content\n" {
t.Errorf("file not written correctly")
}
})
t.Run("buffer with both readonly and modified flags", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"edited readonly content"}).
ReadOnly().
Modified().
Build()
m := newMockModelWithBuffer(&buf)
// Without force - should error
cmdWrite(m, []string{}, false)
if m.commandOutput == nil || !m.commandOutput.IsError {
t.Error("expected error without force")
}
// With force - should succeed
m.commandOutput = nil
cmdWrite(m, []string{}, true)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Errorf("force write failed: %v", m.commandOutput.Lines)
}
// Modified flag should be cleared
if buf.Modified {
t.Error("modified flag should be cleared")
}
})
t.Run("write buffer with very long filename", func(t *testing.T) {
tmpDir := t.TempDir()
// Create a long but valid filename (255 chars is typical limit)
longName := strings.Repeat("a", 200) + ".txt"
filename := filepath.Join(tmpDir, longName)
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"content"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Verify file exists
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Error("file with long name not created")
}
})
t.Run("write buffer with unicode in filename", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "テスト_файл_测试.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"unicode filename test"}).
Build()
m := newMockModelWithBuffer(&buf)
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("unexpected error: %v", m.commandOutput.Lines)
}
// Verify file exists and content is correct
content, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("file with unicode name not created: %v", err)
}
if string(content) != "unicode filename test\n" {
t.Errorf("content = %q", string(content))
}
})
t.Run("multiple sequential writes to same file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "multi_write.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"version 1"}).
Build()
m := newMockModelWithBuffer(&buf)
// First write
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("first write failed: %v", m.commandOutput.Lines)
}
// Modify buffer
buf.SetLine(0, "version 2")
buf.SetModified(true)
// Second write
cmdWrite(m, []string{}, false)
if m.commandOutput != nil && m.commandOutput.IsError {
t.Fatalf("second write failed: %v", m.commandOutput.Lines)
}
// Verify final content
content, _ := os.ReadFile(filename)
if string(content) != "version 2\n" {
t.Errorf("final content = %q", string(content))
}
})
t.Run("force quit does not write", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "not_written.txt")
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithLines([]string{"unsaved content"}).
Modified().
Build()
m := newMockModelWithBuffer(&buf)
cmd := cmdQuit(m, []string{}, true)
// Should quit without error
if m.commandOutput != nil && m.commandOutput.IsError {
t.Errorf("unexpected error: %v", m.commandOutput.Lines)
}
if cmd == nil {
t.Fatal("expected quit command")
}
// File should NOT be written
if _, err := os.Stat(filename); !os.IsNotExist(err) {
t.Error("file should not be written with force quit")
}
// Buffer should still be modified (not saved)
if !buf.Modified {
t.Error("buffer should still be modified (force quit doesn't save)")
}
})
}