Compare commits
2 Commits
c963d66e3b
...
d4980c5532
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4980c5532 | ||
|
|
8364d8b880 |
@ -1,40 +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() {
|
||||||
buf := core.NewBufferBuilder().
|
// <exe> <filename>
|
||||||
|
args := os.Args[1:]
|
||||||
|
|
||||||
|
var prog *tea.Program
|
||||||
|
if len(args) < 1 {
|
||||||
|
prog = program.NewProgramBuilder().
|
||||||
|
EmptyProgram().
|
||||||
|
WithOpt(tea.WithAltScreen()).
|
||||||
Build()
|
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)
|
|
||||||
}
|
|
||||||
} 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ type Model interface {
|
|||||||
Windows() []*core.Window
|
Windows() []*core.Window
|
||||||
ActiveWindow() *core.Window
|
ActiveWindow() *core.Window
|
||||||
Buffers() []*core.Buffer
|
Buffers() []*core.Buffer
|
||||||
|
SetBuffers(bufs []*core.Buffer)
|
||||||
ActiveBuffer() *core.Buffer
|
ActiveBuffer() *core.Buffer
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,44 +22,201 @@ type ErrorMsg struct {
|
|||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Quit Commands
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
// cmdQuit: Handles :quit / :q command.
|
// cmdQuit: Handles :quit / :q command.
|
||||||
func cmdQuit(m action.Model, args []string) tea.Cmd {
|
func cmdQuit(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
return func() tea.Msg {
|
// :q! forces quit, ignoring unsaved changes
|
||||||
return tea.Quit()
|
if force {
|
||||||
|
return tea.Quit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bufs := m.Buffers()
|
||||||
|
|
||||||
|
// Cannot exit if any buffer has unsaved changes
|
||||||
|
for _, buf := range bufs {
|
||||||
|
if buf.Modified {
|
||||||
|
m.SetCommandError(fmt.Errorf("unsaved changes to '%s'", buf.Filename))
|
||||||
|
m.ActiveWindow().SetBuffer(buf)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tea.Quit
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdQuitAll: Handles :qall / :qa command.
|
// cmdQuitAll: Handles :qall / :qa command.
|
||||||
func cmdQuitAll(m action.Model, args []string) tea.Cmd {
|
func cmdQuitAll(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
return func() tea.Msg {
|
// TODO: Until splits are implemented, this is the same as cmdQuit
|
||||||
return tea.Quit()
|
return cmdQuit(m, args, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// File Commands (write & edit)
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
// cmdWrite: Handles :write / :w command
|
||||||
|
func cmdWrite(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
|
if err != nil {
|
||||||
|
m.SetCommandError(err)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdWriteAll: Handles :wall / :wa command
|
||||||
|
func cmdWriteAll(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
bufs := m.Buffers()
|
||||||
|
for _, buf := range bufs {
|
||||||
|
if buf.Modified {
|
||||||
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
|
if err != nil {
|
||||||
|
m.SetCommandError(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdWrite: Handles :write / :w command (TODO: implement file saving).
|
return tea.Batch(cmds...)
|
||||||
func cmdWrite(m action.Model, args []string) tea.Cmd {
|
}
|
||||||
// TODO: Implement file saving
|
|
||||||
// If args provided, save to that filename
|
// cmdWriteQuit: Handles :wq command
|
||||||
// Otherwise save to current file
|
func cmdWriteQuit(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
|
if err != nil {
|
||||||
|
m.SetCommandError(err)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
return tea.Batch(cmd, tea.Quit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdWriteQuitAll: Handles :wqall / :wqa / :xa command.
|
||||||
|
// Writes all modified buffers then quits.
|
||||||
|
func cmdWriteQuitAll(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
bufs := m.Buffers()
|
||||||
|
for _, buf := range bufs {
|
||||||
|
if buf.Modified {
|
||||||
|
cmd, err := writeBuffer(m, buf, args, force)
|
||||||
|
if err != nil {
|
||||||
|
m.SetCommandError(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds = append(cmds, tea.Quit)
|
||||||
|
return tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdEdit: Handles :edit / :e
|
||||||
|
func cmdEdit(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
|
// must have arguments, cant edit nothing
|
||||||
|
if len(args) < 1 {
|
||||||
|
m.SetCommandError(fmt.Errorf(":edit requires an argument"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdWriteAll: Handles :wall / :wa command (TODO: implement saving all buffers).
|
// Vim's Approach:
|
||||||
func cmdWriteAll(m action.Model, args []string) tea.Cmd {
|
// " When you do :edit filename.txt
|
||||||
// TODO: Implement saving all buffers
|
// 1. Check if file exists and is readable (if not, open new buffer)
|
||||||
|
// 2. Detect file encoding (UTF-8, etc.)
|
||||||
|
// 3. Read entire file into memory
|
||||||
|
// 4. Split by line endings (respecting fileformat)
|
||||||
|
// 5. Create new buffer with these lines
|
||||||
|
// 6. Set buffer metadata:
|
||||||
|
// - buftype = "" (normal file)
|
||||||
|
// - modified = 0 (not modified)
|
||||||
|
// - fileformat = "unix" | "dos" | "mac"
|
||||||
|
// - fileencoding = "utf-8" (etc.)
|
||||||
|
|
||||||
|
filename := args[0]
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
|
||||||
|
// If the buffer already exists, just switch to it.
|
||||||
|
bufs := m.Buffers()
|
||||||
|
for _, buf := range bufs {
|
||||||
|
if buf.Filename == filename {
|
||||||
|
m.ActiveWindow().SetBuffer(buf)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
notFound := errors.Is(err, os.ErrNotExist)
|
||||||
|
|
||||||
|
if err != nil && !notFound {
|
||||||
|
m.SetCommandError(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if file != nil {
|
||||||
|
defer file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a buffer with the new file name, writing the file will
|
||||||
|
// handle the saving logic
|
||||||
|
if notFound {
|
||||||
|
buf := core.NewBufferBuilder().
|
||||||
|
WithType(core.FileBuffer).
|
||||||
|
WithFilename(filename).
|
||||||
|
WithFiletype(ext).
|
||||||
|
Listed().
|
||||||
|
Loaded().
|
||||||
|
Build()
|
||||||
|
|
||||||
|
m.SetBuffers(append(m.Buffers(), &buf))
|
||||||
|
m.ActiveWindow().SetBuffer(&buf)
|
||||||
|
|
||||||
|
// Need to adjust the cursor when we make a new file
|
||||||
|
m.ActiveWindow().ClampCursor()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdWriteQuit: Handles :wq command (TODO: save then quit).
|
var lines []string
|
||||||
func cmdWriteQuit(m action.Model, args []string) tea.Cmd {
|
|
||||||
// TODO: Save then quit
|
// BUG: We are unable to open and edit files owned by root. How do we handle that?
|
||||||
return func() tea.Msg {
|
|
||||||
return tea.Quit()
|
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(" ", m.Settings().TabStop))
|
||||||
|
lines = append(lines, cleaned)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buf := core.NewBufferBuilder().
|
||||||
|
WithType(core.FileBuffer).
|
||||||
|
WithFilename(filename).
|
||||||
|
WithFiletype(ext).
|
||||||
|
WithLines(lines).
|
||||||
|
Listed().
|
||||||
|
Loaded().
|
||||||
|
Build()
|
||||||
|
|
||||||
|
m.SetBuffers(append(m.Buffers(), &buf))
|
||||||
|
m.ActiveWindow().SetBuffer(&buf)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Register Commands
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
// cmdRegisters: Handles :register command (debug - displays register content).
|
// cmdRegisters: Handles :register command (debug - displays register content).
|
||||||
func cmdRegisters(m action.Model, args []string) tea.Cmd {
|
func cmdRegisters(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
// TODO: This is temporary, for debugging
|
// TODO: This is temporary, for debugging
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
m.SetCommandError(fmt.Errorf("Please provide a name. Register dump not yet implemented."))
|
m.SetCommandError(fmt.Errorf("Please provide a name. Register dump not yet implemented."))
|
||||||
@ -79,6 +241,10 @@ func cmdRegisters(m action.Model, args []string) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// Settings Commands
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
// cmdSet: Handles :set option[=value] command for configuring editor settings.
|
// cmdSet: Handles :set option[=value] command for configuring editor settings.
|
||||||
// Examples:
|
// Examples:
|
||||||
//
|
//
|
||||||
@ -87,7 +253,7 @@ func cmdRegisters(m action.Model, args []string) tea.Cmd {
|
|||||||
// :set number! - toggle number
|
// :set number! - toggle number
|
||||||
// :set tabstop=4 - set tabstop to 4
|
// :set tabstop=4 - set tabstop to 4
|
||||||
// :set ts=4 - set tabstop to 4 (abbreviation)
|
// :set ts=4 - set tabstop to 4 (abbreviation)
|
||||||
func cmdSet(m action.Model, args []string) tea.Cmd {
|
func cmdSet(m action.Model, args []string, force bool) tea.Cmd {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
out := fmt.Sprintf("%+v", m.Settings())
|
out := fmt.Sprintf("%+v", m.Settings())
|
||||||
m.SetCommandOutput(out)
|
m.SetCommandOutput(out)
|
||||||
|
|||||||
3883
internal/command/handlers_test.go
Normal file
3883
internal/command/handlers_test.go
Normal file
File diff suppressed because it is too large
Load Diff
83
internal/command/io.go
Normal file
83
internal/command/io.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeBuffer(m action.Model, buf *core.Buffer, args []string, force bool) (tea.Cmd, error) {
|
||||||
|
// Key Steps:
|
||||||
|
// - Backup Creation (if backup is set): Copy original to .bak file
|
||||||
|
// - Line Ending Application: Apply fileformat setting (unix=\n, dos=\r\n, mac=\r)
|
||||||
|
// - Character Encoding: Convert from internal representation to fileencoding
|
||||||
|
// - Atomic Write: Write to temporary file first (.swp or similar)
|
||||||
|
// - Atomic Rename: Rename temp file to target filename (atomic operation)
|
||||||
|
// - Metadata Update: Clear modified flag, update timestamp
|
||||||
|
// Safety Mechanisms:
|
||||||
|
// " Vim's write safety
|
||||||
|
// 1. Check file permissions
|
||||||
|
// 2. If file exists and 'writebackup' set:
|
||||||
|
// - Create backup (.bak)
|
||||||
|
// 3. Write to temporary file (.swp~)
|
||||||
|
// 4. Verify write succeeded
|
||||||
|
// 5. Rename temp to target (atomic)
|
||||||
|
// 6. Remove backup if 'backup' not set
|
||||||
|
// 7. Update buffer metadata
|
||||||
|
|
||||||
|
// TODO: Implement atomic and safe writes
|
||||||
|
|
||||||
|
// Check readonly flag ONLY if not forced with !
|
||||||
|
if !force && buf.ReadOnly {
|
||||||
|
return nil, fmt.Errorf("cannot write to 'readonly' buffer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !force && buf.Type == core.ScatchBuffer {
|
||||||
|
return nil, fmt.Errorf("cannot write to buffer of type 'ScratchBuffer'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the filename; differs by the type
|
||||||
|
var filename string
|
||||||
|
if len(args) > 0 {
|
||||||
|
filename = args[0]
|
||||||
|
} else {
|
||||||
|
if buf.Filename == "" {
|
||||||
|
return nil, fmt.Errorf("cannot write: no file name provided")
|
||||||
|
}
|
||||||
|
filename = buf.Filename
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Same write operation regardless of where the file came from
|
||||||
|
var bytes int
|
||||||
|
|
||||||
|
// Using a bufio.Writer because its more efficient
|
||||||
|
writer := bufio.NewWriter(file)
|
||||||
|
for _, line := range buf.Lines {
|
||||||
|
n, err := writer.WriteString(line + "\n")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes += n
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Flush(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
output := fmt.Sprintf("'%s', %dL %db written", filename, buf.LineCount(), bytes)
|
||||||
|
m.SetCommandOutput(output)
|
||||||
|
buf.SetModified(false)
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@ -12,7 +12,7 @@ import (
|
|||||||
type Command struct {
|
type Command struct {
|
||||||
Name string // Full name: "quit"
|
Name string // Full name: "quit"
|
||||||
ShortForm string // Minimum abbreviation: "q"
|
ShortForm string // Minimum abbreviation: "q"
|
||||||
Handler func(m action.Model, args []string) tea.Cmd // Handler function
|
Handler func(m action.Model, args []string, force bool) tea.Cmd // Handler function
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry: Holds all registered commands.
|
// Registry: Holds all registered commands.
|
||||||
@ -75,24 +75,34 @@ func (r *Registry) Lookup(input string) (*Command, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse: Splits a command line into command name and arguments.
|
// Parse: Splits a command line into command name and arguments.
|
||||||
func Parse(cmdLine string) (name string, args []string) {
|
func Parse(cmdLine string) (name string, args []string, force bool) {
|
||||||
parts := strings.Fields(cmdLine)
|
parts := strings.Fields(cmdLine)
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
return "", nil
|
return "", nil, false
|
||||||
}
|
}
|
||||||
return parts[0], parts[1:]
|
|
||||||
|
name = parts[0]
|
||||||
|
args = parts[1:]
|
||||||
|
|
||||||
|
// Check if command ends with ! (force flag)
|
||||||
|
if strings.HasSuffix(name, "!") {
|
||||||
|
name = strings.TrimSuffix(name, "!")
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return name, args, force
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry.Execute: Parses and executes a command line.
|
// Registry.Execute: Parses and executes a command line.
|
||||||
func (r *Registry) Execute(m action.Model, cmdLine string) (tea.Cmd, error) {
|
func (r *Registry) Execute(m action.Model, cmdLine string) (tea.Cmd, error) {
|
||||||
name, args := Parse(cmdLine)
|
name, args, force := Parse(cmdLine)
|
||||||
|
|
||||||
cmd, err := r.Lookup(name)
|
cmd, err := r.Lookup(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd.Handler(m, args), nil
|
return cmd.Handler(m, args, force), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultRegistry is the global command registry
|
// DefaultRegistry is the global command registry
|
||||||
@ -132,6 +142,12 @@ func (r *Registry) registerDefaults() {
|
|||||||
Handler: cmdWriteQuit,
|
Handler: cmdWriteQuit,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "wqall",
|
||||||
|
ShortForm: "wqa",
|
||||||
|
Handler: cmdWriteQuitAll,
|
||||||
|
})
|
||||||
|
|
||||||
// Set command
|
// Set command
|
||||||
r.Register(Command{
|
r.Register(Command{
|
||||||
Name: "set",
|
Name: "set",
|
||||||
@ -145,4 +161,11 @@ func (r *Registry) registerDefaults() {
|
|||||||
ShortForm: "reg",
|
ShortForm: "reg",
|
||||||
Handler: cmdRegisters,
|
Handler: cmdRegisters,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// File commands
|
||||||
|
r.Register(Command{
|
||||||
|
Name: "edit",
|
||||||
|
ShortForm: "e",
|
||||||
|
Handler: cmdEdit,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,6 +89,26 @@ func TestRegistryLookup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("e matches edit", func(t *testing.T) {
|
||||||
|
cmd, err := r.Lookup("e")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Lookup error: %v", err)
|
||||||
|
}
|
||||||
|
if cmd.Name != "edit" {
|
||||||
|
t.Errorf("cmd.Name = %q, want \"edit\"", cmd.Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ed matches edit", func(t *testing.T) {
|
||||||
|
cmd, err := r.Lookup("ed")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Lookup error: %v", err)
|
||||||
|
}
|
||||||
|
if cmd.Name != "edit" {
|
||||||
|
t.Errorf("cmd.Name = %q, want \"edit\"", cmd.Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("unknown command returns error", func(t *testing.T) {
|
t.Run("unknown command returns error", func(t *testing.T) {
|
||||||
_, err := r.Lookup("xyz")
|
_, err := r.Lookup("xyz")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -106,17 +126,20 @@ func TestRegistryLookup(t *testing.T) {
|
|||||||
|
|
||||||
func TestParse(t *testing.T) {
|
func TestParse(t *testing.T) {
|
||||||
t.Run("command only", func(t *testing.T) {
|
t.Run("command only", func(t *testing.T) {
|
||||||
name, args := Parse("quit")
|
name, args, force := Parse("quit")
|
||||||
if name != "quit" {
|
if name != "quit" {
|
||||||
t.Errorf("name = %q, want \"quit\"", name)
|
t.Errorf("name = %q, want \"quit\"", name)
|
||||||
}
|
}
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
t.Errorf("len(args) = %d, want 0", len(args))
|
t.Errorf("len(args) = %d, want 0", len(args))
|
||||||
}
|
}
|
||||||
|
if force {
|
||||||
|
t.Error("force should be false")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("command with one arg", func(t *testing.T) {
|
t.Run("command with one arg", func(t *testing.T) {
|
||||||
name, args := Parse("set number")
|
name, args, force := Parse("set number")
|
||||||
if name != "set" {
|
if name != "set" {
|
||||||
t.Errorf("name = %q, want \"set\"", name)
|
t.Errorf("name = %q, want \"set\"", name)
|
||||||
}
|
}
|
||||||
@ -126,10 +149,13 @@ func TestParse(t *testing.T) {
|
|||||||
if args[0] != "number" {
|
if args[0] != "number" {
|
||||||
t.Errorf("args[0] = %q, want \"number\"", args[0])
|
t.Errorf("args[0] = %q, want \"number\"", args[0])
|
||||||
}
|
}
|
||||||
|
if force {
|
||||||
|
t.Error("force should be false")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("command with multiple args", func(t *testing.T) {
|
t.Run("command with multiple args", func(t *testing.T) {
|
||||||
name, args := Parse("set number tabstop=4")
|
name, args, force := Parse("set number tabstop=4")
|
||||||
if name != "set" {
|
if name != "set" {
|
||||||
t.Errorf("name = %q, want \"set\"", name)
|
t.Errorf("name = %q, want \"set\"", name)
|
||||||
}
|
}
|
||||||
@ -142,26 +168,74 @@ func TestParse(t *testing.T) {
|
|||||||
if args[1] != "tabstop=4" {
|
if args[1] != "tabstop=4" {
|
||||||
t.Errorf("args[1] = %q, want \"tabstop=4\"", args[1])
|
t.Errorf("args[1] = %q, want \"tabstop=4\"", args[1])
|
||||||
}
|
}
|
||||||
|
if force {
|
||||||
|
t.Error("force should be false")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("empty string", func(t *testing.T) {
|
t.Run("empty string", func(t *testing.T) {
|
||||||
name, args := Parse("")
|
name, args, force := Parse("")
|
||||||
if name != "" {
|
if name != "" {
|
||||||
t.Errorf("name = %q, want \"\"", name)
|
t.Errorf("name = %q, want \"\"", name)
|
||||||
}
|
}
|
||||||
if args != nil {
|
if args != nil {
|
||||||
t.Errorf("args = %v, want nil", args)
|
t.Errorf("args = %v, want nil", args)
|
||||||
}
|
}
|
||||||
|
if force {
|
||||||
|
t.Error("force should be false")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("whitespace only", func(t *testing.T) {
|
t.Run("whitespace only", func(t *testing.T) {
|
||||||
name, args := Parse(" ")
|
name, args, force := Parse(" ")
|
||||||
if name != "" {
|
if name != "" {
|
||||||
t.Errorf("name = %q, want \"\"", name)
|
t.Errorf("name = %q, want \"\"", name)
|
||||||
}
|
}
|
||||||
if args != nil {
|
if args != nil {
|
||||||
t.Errorf("args = %v, want nil", args)
|
t.Errorf("args = %v, want nil", args)
|
||||||
}
|
}
|
||||||
|
if force {
|
||||||
|
t.Error("force should be false")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("command with force flag", func(t *testing.T) {
|
||||||
|
name, args, force := Parse("quit!")
|
||||||
|
if name != "quit" {
|
||||||
|
t.Errorf("name = %q, want \"quit\"", name)
|
||||||
|
}
|
||||||
|
if len(args) != 0 {
|
||||||
|
t.Errorf("len(args) = %d, want 0", len(args))
|
||||||
|
}
|
||||||
|
if !force {
|
||||||
|
t.Error("force should be true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("write command with force", func(t *testing.T) {
|
||||||
|
name, _, force := Parse("w!")
|
||||||
|
if name != "w" {
|
||||||
|
t.Errorf("name = %q, want \"w\"", name)
|
||||||
|
}
|
||||||
|
if !force {
|
||||||
|
t.Error("force should be true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("command with force and args", func(t *testing.T) {
|
||||||
|
name, args, force := Parse("w! file.txt")
|
||||||
|
if name != "w" {
|
||||||
|
t.Errorf("name = %q, want \"w\"", name)
|
||||||
|
}
|
||||||
|
if len(args) != 1 {
|
||||||
|
t.Errorf("len(args) = %d, want 1", len(args))
|
||||||
|
}
|
||||||
|
if args[0] != "file.txt" {
|
||||||
|
t.Errorf("args[0] = %q, want \"file.txt\"", args[0])
|
||||||
|
}
|
||||||
|
if !force {
|
||||||
|
t.Error("force should be true")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,9 +4,18 @@ type BufferOptions struct {
|
|||||||
// tabstop expandtab
|
// tabstop expandtab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BufferType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScatchBuffer BufferType = iota
|
||||||
|
FileBuffer
|
||||||
|
DirectoryBuffer
|
||||||
|
)
|
||||||
|
|
||||||
type Buffer struct {
|
type Buffer struct {
|
||||||
// Buffer data
|
// Buffer data
|
||||||
Id int
|
Id int
|
||||||
|
Type BufferType
|
||||||
|
|
||||||
// File data
|
// File data
|
||||||
Filename string
|
Filename string
|
||||||
@ -17,6 +26,7 @@ type Buffer struct {
|
|||||||
Modified bool
|
Modified bool
|
||||||
Loaded bool
|
Loaded bool
|
||||||
Listed bool
|
Listed bool
|
||||||
|
ReadOnly bool
|
||||||
|
|
||||||
// Options BufferOptions
|
// Options BufferOptions
|
||||||
// UndoTree TODO: This will be big
|
// UndoTree TODO: This will be big
|
||||||
@ -36,16 +46,17 @@ func (b *Buffer) Line(idx int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Buffer.SetLine: Set the content of the line at an index. Does nothing if the
|
// Buffer.SetLine: Set the content of the line at an index. Does nothing if the
|
||||||
// index is out of bounds.
|
// index is out of bounds. This function sets the modified flag.
|
||||||
func (b *Buffer) SetLine(idx int, content string) {
|
func (b *Buffer) SetLine(idx int, content string) {
|
||||||
if idx >= 0 && idx < len(b.Lines) {
|
if idx >= 0 && idx < len(b.Lines) {
|
||||||
b.Lines[idx] = content
|
b.Lines[idx] = content
|
||||||
}
|
}
|
||||||
|
b.Modified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer.InsertLine: Insert a line with content at an index. The index is clamped
|
// Buffer.InsertLine: Insert a line with content at an index. The index is clamped
|
||||||
// to valid bounds (0 to len(Lines)). The new line is inserted before the line at
|
// to valid bounds (0 to len(Lines)). The new line is inserted before the line at
|
||||||
// the given index.
|
// the given index. This function sets the modified flag.
|
||||||
func (b *Buffer) InsertLine(idx int, content string) {
|
func (b *Buffer) InsertLine(idx int, content string) {
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
idx = 0
|
idx = 0
|
||||||
@ -54,14 +65,16 @@ func (b *Buffer) InsertLine(idx int, content string) {
|
|||||||
idx = len(b.Lines)
|
idx = len(b.Lines)
|
||||||
}
|
}
|
||||||
b.Lines = append(b.Lines[:idx], append([]string{content}, b.Lines[idx:]...)...)
|
b.Lines = append(b.Lines[:idx], append([]string{content}, b.Lines[idx:]...)...)
|
||||||
|
b.Modified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer.DeleteLine: Delete a line at an index. Does nothing if the index is out
|
// Buffer.DeleteLine: Delete a line at an index. Does nothing if the index is out
|
||||||
// of bounds.
|
// of bounds. This function sets the modified flag.
|
||||||
func (b *Buffer) DeleteLine(idx int) {
|
func (b *Buffer) DeleteLine(idx int) {
|
||||||
if idx >= 0 && idx < len(b.Lines) {
|
if idx >= 0 && idx < len(b.Lines) {
|
||||||
b.Lines = append(b.Lines[:idx], b.Lines[idx+1:]...)
|
b.Lines = append(b.Lines[:idx], b.Lines[idx+1:]...)
|
||||||
}
|
}
|
||||||
|
b.Modified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer.LineCount: Get the number of lines in the buffer.
|
// Buffer.LineCount: Get the number of lines in the buffer.
|
||||||
@ -109,3 +122,9 @@ func (b *Buffer) SetLoaded(loaded bool) {
|
|||||||
func (b *Buffer) SetListed(listed bool) {
|
func (b *Buffer) SetListed(listed bool) {
|
||||||
b.Listed = listed
|
b.Listed = listed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buffer.SetType: Set the buffers type. This type is used to determine handling
|
||||||
|
// of I/O functions.
|
||||||
|
func (b *Buffer) SetType(t BufferType) {
|
||||||
|
b.Type = t
|
||||||
|
}
|
||||||
|
|||||||
@ -13,12 +13,14 @@ func NewBufferBuilder() *BufferBuilder {
|
|||||||
return &BufferBuilder{
|
return &BufferBuilder{
|
||||||
buffer: Buffer{
|
buffer: Buffer{
|
||||||
Id: 0, // This is set when built
|
Id: 0, // This is set when built
|
||||||
|
Type: ScatchBuffer, // Default buffer type
|
||||||
Filename: "",
|
Filename: "",
|
||||||
Filetype: "",
|
Filetype: "",
|
||||||
Lines: []string{""},
|
Lines: []string{""},
|
||||||
Modified: false,
|
Modified: false,
|
||||||
Loaded: false,
|
Loaded: false,
|
||||||
Listed: false,
|
Listed: false,
|
||||||
|
ReadOnly: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,6 +64,20 @@ func (b *BufferBuilder) Listed() *BufferBuilder {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BufferBuilder.ReadOnly: Sets the readonly flag of the buffer being built. By default,
|
||||||
|
// buffers are built with the readonly flag being false.
|
||||||
|
func (b *BufferBuilder) ReadOnly() *BufferBuilder {
|
||||||
|
b.buffer.ReadOnly = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferBuilder.Listed: Sets the type of the buffer being built. By default, buffers
|
||||||
|
// are build with the ScatchBuffer type.
|
||||||
|
func (b *BufferBuilder) WithType(t BufferType) *BufferBuilder {
|
||||||
|
b.buffer.Type = t
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
// BufferBuilder.Build: Build the final buffer and return it to the caller. Final
|
// BufferBuilder.Build: Build the final buffer and return it to the caller. Final
|
||||||
// step in the process. This is where the ID is set, so many buffers can be "in-progress"
|
// step in the process. This is where the ID is set, so many buffers can be "in-progress"
|
||||||
// but the ID will be set when they are built. Meaning, this is not thread safe.
|
// but the ID will be set when they are built. Meaning, this is not thread safe.
|
||||||
|
|||||||
@ -40,8 +40,10 @@ type Window struct {
|
|||||||
// Window.ClampCursor: Clamps the cursor in the all directions to ensure the cursor
|
// Window.ClampCursor: Clamps the cursor in the all directions to ensure the cursor
|
||||||
// does not go into an invalid position. Such as negative values or past the end of
|
// does not go into an invalid position. Such as negative values or past the end of
|
||||||
// the line. In the Y direction it validates that the cursor does not pass the end
|
// the line. In the Y direction it validates that the cursor does not pass the end
|
||||||
// of the content or attempt to be "above" the content (negative value).
|
// of the content or attempt to be "above" the content (negative value). This function
|
||||||
func (w *Window) clampCursor() {
|
// is automatically called in any time the cursor changes. It only needs to be called
|
||||||
|
// when a force clamp is needed.
|
||||||
|
func (w *Window) ClampCursor() {
|
||||||
// Clamp line to valid range [0, lineCount-1]
|
// Clamp line to valid range [0, lineCount-1]
|
||||||
maxLine := max(w.Buffer.LineCount()-1, 0)
|
maxLine := max(w.Buffer.LineCount()-1, 0)
|
||||||
if w.Cursor.Line < 0 {
|
if w.Cursor.Line < 0 {
|
||||||
@ -50,8 +52,15 @@ func (w *Window) clampCursor() {
|
|||||||
w.Cursor.Line = maxLine
|
w.Cursor.Line = maxLine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle empty buffer - no lines to clamp column against
|
||||||
|
if w.Buffer.LineCount() == 0 {
|
||||||
|
w.Cursor.Line = 0
|
||||||
|
w.Cursor.Col = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Clamp column to valid range [0, lineLen]
|
// Clamp column to valid range [0, lineLen]
|
||||||
lineLen := len(w.Buffer.Lines[w.Cursor.Line]) // Safe now - Line is valid
|
lineLen := len(w.Buffer.Lines[w.Cursor.Line])
|
||||||
if w.Cursor.Col < 0 {
|
if w.Cursor.Col < 0 {
|
||||||
w.Cursor.Col = 0
|
w.Cursor.Col = 0
|
||||||
} else if lineLen == 0 {
|
} else if lineLen == 0 {
|
||||||
@ -111,27 +120,29 @@ func (w *Window) SetNumber(number int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Window.SetBuffer: Sets the buffer that this window should display. This is used when
|
// Window.SetBuffer: Sets the buffer that this window should display. This is used when
|
||||||
// switching between buffers or opening a new file in the current window.
|
// switching between buffers or opening a new file in the current window. This function
|
||||||
|
// does clamp the cursor to the current buffer
|
||||||
func (w *Window) SetBuffer(buffer *Buffer) {
|
func (w *Window) SetBuffer(buffer *Buffer) {
|
||||||
w.Buffer = buffer
|
w.Buffer = buffer
|
||||||
|
w.ClampCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window.SetCursor: Sets the cursor position in this window to the given position.
|
// Window.SetCursor: Sets the cursor position in this window to the given position.
|
||||||
func (w *Window) SetCursor(cursor Position) {
|
func (w *Window) SetCursor(cursor Position) {
|
||||||
w.Cursor = cursor
|
w.Cursor = cursor
|
||||||
w.clampCursor()
|
w.ClampCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window.SetCursorLine: Sets the line number of the cursor position.
|
// Window.SetCursorLine: Sets the line number of the cursor position.
|
||||||
func (w *Window) SetCursorLine(line int) {
|
func (w *Window) SetCursorLine(line int) {
|
||||||
w.Cursor.Line = line
|
w.Cursor.Line = line
|
||||||
w.clampCursor()
|
w.ClampCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window.SetCursorCol: Sets the column number of the cursor position.
|
// Window.SetCursorCol: Sets the column number of the cursor position.
|
||||||
func (w *Window) SetCursorCol(col int) {
|
func (w *Window) SetCursorCol(col int) {
|
||||||
w.Cursor.Col = col
|
w.Cursor.Col = col
|
||||||
w.clampCursor()
|
w.ClampCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window.SetCursorPos: Sets both the line and column of the cursor position. This is
|
// Window.SetCursorPos: Sets both the line and column of the cursor position. This is
|
||||||
@ -139,7 +150,7 @@ func (w *Window) SetCursorCol(col int) {
|
|||||||
func (w *Window) SetCursorPos(line, col int) {
|
func (w *Window) SetCursorPos(line, col int) {
|
||||||
w.Cursor.Line = line
|
w.Cursor.Line = line
|
||||||
w.Cursor.Col = col
|
w.Cursor.Col = col
|
||||||
w.clampCursor()
|
w.ClampCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window.SetAnchor: Sets the anchor position in this window. The anchor is used for
|
// Window.SetAnchor: Sets the anchor position in this window. The anchor is used for
|
||||||
|
|||||||
@ -38,6 +38,13 @@ func sendKeys(tm *teatest.TestModel, keys ...string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendKeyString is a convenience function for sending many keys.
|
||||||
|
func sendKeyString(tm *teatest.TestModel, keyString string) {
|
||||||
|
for _, key := range keyString {
|
||||||
|
sendKeys(tm, string(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestModelOption is a functional option for configuring test models
|
// TestModelOption is a functional option for configuring test models
|
||||||
type TestModelOption func(*testModelConfig)
|
type TestModelOption func(*testModelConfig)
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,6 @@ import (
|
|||||||
|
|
||||||
// NOTE: AI Generated tests
|
// NOTE: AI Generated tests
|
||||||
|
|
||||||
// Default settings are: Number=true, RelativeNumber=true, TabSize=2, ScrollOff=8
|
|
||||||
|
|
||||||
func TestCommandSetBoolean(t *testing.T) {
|
func TestCommandSetBoolean(t *testing.T) {
|
||||||
t.Run("':set nonumber' disables line numbers", func(t *testing.T) {
|
t.Run("':set nonumber' disables line numbers", func(t *testing.T) {
|
||||||
// Default has Number=true
|
// Default has Number=true
|
||||||
@ -339,3 +337,16 @@ func TestCommandModeErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommandEdit(t *testing.T) {
|
||||||
|
t.Run(":edit with no args fails", func(t *testing.T) {
|
||||||
|
tm := newTestModel(t)
|
||||||
|
sendKeyString(tm, ":edit")
|
||||||
|
sendKeys(tm, "enter")
|
||||||
|
|
||||||
|
m := getFinalModel(t, tm)
|
||||||
|
if m.commandError == nil {
|
||||||
|
t.Error("expected commandError to be set for edit without args")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -80,6 +80,10 @@ func (m *Model) Buffers() []*core.Buffer {
|
|||||||
return m.buffers
|
return m.buffers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) SetBuffers(bufs []*core.Buffer) {
|
||||||
|
m.buffers = bufs
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) ActiveBuffer() *core.Buffer {
|
func (m *Model) ActiveBuffer() *core.Buffer {
|
||||||
win := m.ActiveWindow()
|
win := m.ActiveWindow()
|
||||||
return win.Buffer
|
return win.Buffer
|
||||||
|
|||||||
@ -199,7 +199,21 @@ func drawStatusBar(w *core.Window, mode core.Mode) string {
|
|||||||
// leftBar: Returns the left side of the status bar showing the current mode.
|
// leftBar: Returns the left side of the status bar showing the current mode.
|
||||||
func leftBar(w *core.Window, mode core.Mode) string {
|
func leftBar(w *core.Window, mode core.Mode) string {
|
||||||
buf := w.Buffer
|
buf := w.Buffer
|
||||||
return fmt.Sprintf(" %s %s", mode.ToString(), buf.Filename)
|
|
||||||
|
var flags []string
|
||||||
|
if buf.Modified {
|
||||||
|
flags = append(flags, "!")
|
||||||
|
}
|
||||||
|
if buf.ReadOnly {
|
||||||
|
flags = append(flags, "x")
|
||||||
|
}
|
||||||
|
|
||||||
|
var flagStr string
|
||||||
|
if len(flags) > 0 {
|
||||||
|
flagStr = "(" + strings.Join(flags, "") + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(" %s %s %s", mode.ToString(), buf.Filename, flagStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// rightBar: Returns the right side of the status bar showing cursor position
|
// rightBar: Returns the right side of the status bar showing cursor position
|
||||||
|
|||||||
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