feat: created (tested) program_builder.
All checks were successful
Run Test Suite / test (push) Successful in 15s

Also adjusted some of the IO tests for writing and force writing.
This commit is contained in:
Hayden Hargreaves 2026-03-10 14:18:20 -07:00
parent 8364d8b880
commit d4980c5532
6 changed files with 2143 additions and 36 deletions

View File

@ -1,49 +1,32 @@
package main package main
import ( import (
"fmt" "os"
"git.gophernest.net/azpect/TextEditor/internal/core" "git.gophernest.net/azpect/TextEditor/internal/program"
"git.gophernest.net/azpect/TextEditor/internal/editor"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
// main: Entry point for the Gim text editor. Creates a buffer and window, // main: Entry point for the Gim text editor. Creates a buffer and window,
// initializes the editor model, and runs the BubbleTea TUI program. // initializes the editor model, and runs the BubbleTea TUI program.
func main() { func main() {
// TODO: Read OS args and open file // <exe> <filename>
args := os.Args[1:]
// TODO: Need to implement the force bang handling!!! Opencode has this
buf := core.NewBufferBuilder().
ReadOnly().
Build()
win := core.NewWindowBuilder().
WithBuffer(&buf).
WithOptions(core.NewDefaultWinOptions()).
Build()
model := editor.NewModelBuilder().
AddBuffer(&buf).
AddWindow(&win).
WithActiveWindowId(win.Id).
Build()
m, _ := tea.NewProgram(model, tea.WithAltScreen()).Run()
final, ok := m.(*editor.Model)
if ok {
fmt.Printf("PRINTING WINDOWS: %+v\n", final.Windows())
fmt.Printf("PRINTING ACTIVE WINDOW: %+v\n", final.ActiveWindow())
for _, win := range final.Windows() {
fmt.Printf("\t%+v\n", *win.Buffer)
}
fmt.Printf("PRINTING BUFFERS: %+v\n", final.Buffers())
fmt.Printf("PRINTING ACTIVE BUFFER: %+v\n", final.ActiveBuffer())
var prog *tea.Program
if len(args) < 1 {
prog = program.NewProgramBuilder().
EmptyProgram().
WithOpt(tea.WithAltScreen()).
Build()
} else { } else {
fmt.Printf("PRINTING ALL: %+v\n", m) prog = program.NewProgramBuilder().
FileProgram(args[0]).
WithOpt(tea.WithAltScreen()).
Build()
}
if _, err := prog.Run(); err != nil {
panic(err)
} }
} }

View File

@ -184,6 +184,8 @@ func cmdEdit(m action.Model, args []string, force bool) tea.Cmd {
var lines []string var lines []string
// BUG: We are unable to open and edit files owned by root. How do we handle that?
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()

View File

@ -2945,3 +2945,939 @@ func TestCmdWriteQuitAll(t *testing.T) {
} }
}) })
} }
// ==================================================
// 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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError != nil {
t.Fatalf("unexpected error with force: %v", m.commandError)
}
// 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.commandError == nil {
t.Error("expected error when no filename provided")
}
if !strings.Contains(m.commandError.Error(), "no file name") {
t.Errorf("error should mention no file name: %v", m.commandError)
}
})
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.commandError == nil {
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.commandError == nil {
t.Error("expected error for readonly without force flag")
}
if !strings.Contains(m.commandError.Error(), "readonly") {
t.Errorf("error should mention readonly: %v", m.commandError)
}
})
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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError == nil {
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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError == nil {
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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError == nil {
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.commandError == nil {
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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
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.commandError == nil {
t.Error("expected error without force")
}
// With force - should succeed
m.commandError = nil
cmdWrite(m, []string{}, true)
if m.commandError != nil {
t.Errorf("force write failed: %v", m.commandError)
}
// 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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError != nil {
t.Fatalf("unexpected error: %v", m.commandError)
}
// 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.commandError != nil {
t.Fatalf("first write failed: %v", m.commandError)
}
// Modify buffer
buf.SetLine(0, "version 2")
buf.SetModified(true)
// Second write
cmdWrite(m, []string{}, false)
if m.commandError != nil {
t.Fatalf("second write failed: %v", m.commandError)
}
// 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.commandError != nil {
t.Errorf("unexpected error: %v", m.commandError)
}
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)")
}
})
}

View File

@ -36,7 +36,7 @@ func writeBuffer(m action.Model, buf *core.Buffer, args []string, force bool) (t
return nil, fmt.Errorf("cannot write to 'readonly' buffer") return nil, fmt.Errorf("cannot write to 'readonly' buffer")
} }
if buf.Type == core.ScatchBuffer { if !force && buf.Type == core.ScatchBuffer {
return nil, fmt.Errorf("cannot write to buffer of type 'ScratchBuffer'") return nil, fmt.Errorf("cannot write to buffer of type 'ScratchBuffer'")
} }

View File

@ -0,0 +1,120 @@
package program
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"git.gophernest.net/azpect/TextEditor/internal/core"
"git.gophernest.net/azpect/TextEditor/internal/editor"
tea "github.com/charmbracelet/bubbletea"
)
type ProgramBuilder struct {
model *editor.Model
opts []tea.ProgramOption
}
func NewProgramBuilder() *ProgramBuilder {
return &ProgramBuilder{
model: &editor.Model{},
opts: []tea.ProgramOption{},
}
}
// ProgramBuilder.FileProgram: Sets the internal state of the builder to the required
// state to start the program (editor) with a filename. This is what will happen when
// a user runs 'gim <filename>'.
func (p *ProgramBuilder) FileProgram(filename string) *ProgramBuilder {
// Only difference: open the file
ext := filepath.Ext(filename)
file, err := os.Open(filename)
notFound := errors.Is(err, os.ErrNotExist)
if err != nil && !notFound {
// TODO: Handle this
panic(fmt.Errorf("Failed to find file: %w", err))
}
if file != nil {
defer file.Close()
}
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithFiletype(ext).
Build()
win := core.NewWindowBuilder().
WithBuffer(&buf).
WithOptions(core.NewDefaultWinOptions()).
Build()
p.model = editor.NewModelBuilder().
AddBuffer(&buf).
AddWindow(&win).
WithActiveWindowId(win.Id).
Build()
// If we did not find the file, all we need to do is set the filename and type
if notFound {
return p
}
// Otherwise we have to create everything, then read the file (since we need settings)
// COPIED FROM `internal/command/handlers.go`
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimSuffix(line, "\r")
// BUG: This is bad, we don't want to this, but we have to
cleaned := strings.ReplaceAll(line, "\t", strings.Repeat(" ", p.model.Settings().TabStop))
lines = append(lines, cleaned)
}
// Only setting lines if we found some
if len(lines) > 0 {
p.model.ActiveBuffer().SetLines(lines)
}
return p
}
// ProgramBuilder.EmptyProgram: Sets the internal state of the builder to the required
// state to start the program (editor) in the current directory. This is what will
// happen when a user runs 'gim'.
func (p *ProgramBuilder) EmptyProgram() *ProgramBuilder {
buf := core.NewBufferBuilder().
ReadOnly().
Build()
win := core.NewWindowBuilder().
WithBuffer(&buf).
WithOptions(core.NewDefaultWinOptions()).
Build()
p.model = editor.NewModelBuilder().
AddBuffer(&buf).
AddWindow(&win).
WithActiveWindowId(win.Id).
Build()
return p
}
// ProgramBuilder.WithOpt: Add an option to the list of options that will be used when
// the program is built.
func (p *ProgramBuilder) WithOpt(opt tea.ProgramOption) *ProgramBuilder {
p.opts = append(p.opts, opt)
return p
}
// ProgramBuilder.Build: Build and return the configured tea.Program instance.
func (p *ProgramBuilder) Build() *tea.Program {
return tea.NewProgram(p.model, p.opts...)
}

File diff suppressed because it is too large Load Diff