feat: created (tested) program_builder.
All checks were successful
Run Test Suite / test (push) Successful in 15s
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:
parent
8364d8b880
commit
d4980c5532
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -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'")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
120
internal/program/program_builder.go
Normal file
120
internal/program/program_builder.go
Normal 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...)
|
||||||
|
}
|
||||||
1066
internal/program/program_builder_test.go
Normal file
1066
internal/program/program_builder_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user