feat: lots of IO fixes! Writing and forcing seems to be working.
This commit is contained in:
parent
c963d66e3b
commit
8364d8b880
@ -11,7 +11,12 @@ import (
|
||||
// main: Entry point for the Gim text editor. Creates a buffer and window,
|
||||
// initializes the editor model, and runs the BubbleTea TUI program.
|
||||
func main() {
|
||||
// TODO: Read OS args and open file
|
||||
|
||||
// TODO: Need to implement the force bang handling!!! Opencode has this
|
||||
|
||||
buf := core.NewBufferBuilder().
|
||||
ReadOnly().
|
||||
Build()
|
||||
|
||||
win := core.NewWindowBuilder().
|
||||
@ -34,6 +39,10 @@ func main() {
|
||||
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())
|
||||
|
||||
} else {
|
||||
fmt.Printf("PRINTING ALL: %+v\n", m)
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ type Model interface {
|
||||
Windows() []*core.Window
|
||||
ActiveWindow() *core.Window
|
||||
Buffers() []*core.Buffer
|
||||
SetBuffers(bufs []*core.Buffer)
|
||||
ActiveBuffer() *core.Buffer
|
||||
|
||||
// ==================================================
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
@ -17,44 +22,199 @@ type ErrorMsg struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Quit Commands
|
||||
// --------------------------------------------------
|
||||
|
||||
// cmdQuit: Handles :quit / :q command.
|
||||
func cmdQuit(m action.Model, args []string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return tea.Quit()
|
||||
func cmdQuit(m action.Model, args []string, force bool) tea.Cmd {
|
||||
// :q! forces quit, ignoring unsaved changes
|
||||
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.
|
||||
func cmdQuitAll(m action.Model, args []string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return tea.Quit()
|
||||
}
|
||||
func cmdQuitAll(m action.Model, args []string, force bool) tea.Cmd {
|
||||
// TODO: Until splits are implemented, this is the same as cmdQuit
|
||||
return cmdQuit(m, args, force)
|
||||
}
|
||||
|
||||
// cmdWrite: Handles :write / :w command (TODO: implement file saving).
|
||||
func cmdWrite(m action.Model, args []string) tea.Cmd {
|
||||
// TODO: Implement file saving
|
||||
// If args provided, save to that filename
|
||||
// Otherwise save to current file
|
||||
// --------------------------------------------------
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
// cmdWriteQuit: Handles :wq command
|
||||
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
|
||||
}
|
||||
|
||||
// Vim's Approach:
|
||||
// " When you do :edit filename.txt
|
||||
// 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
|
||||
}
|
||||
|
||||
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(" ", 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
|
||||
}
|
||||
|
||||
// cmdWriteAll: Handles :wall / :wa command (TODO: implement saving all buffers).
|
||||
func cmdWriteAll(m action.Model, args []string) tea.Cmd {
|
||||
// TODO: Implement saving all buffers
|
||||
return nil
|
||||
}
|
||||
|
||||
// cmdWriteQuit: Handles :wq command (TODO: save then quit).
|
||||
func cmdWriteQuit(m action.Model, args []string) tea.Cmd {
|
||||
// TODO: Save then quit
|
||||
return func() tea.Msg {
|
||||
return tea.Quit()
|
||||
}
|
||||
}
|
||||
// --------------------------------------------------
|
||||
// Register Commands
|
||||
// --------------------------------------------------
|
||||
|
||||
// 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
|
||||
if len(args) < 1 {
|
||||
m.SetCommandError(fmt.Errorf("Please provide a name. Register dump not yet implemented."))
|
||||
@ -79,6 +239,10 @@ func cmdRegisters(m action.Model, args []string) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Settings Commands
|
||||
// --------------------------------------------------
|
||||
|
||||
// cmdSet: Handles :set option[=value] command for configuring editor settings.
|
||||
// Examples:
|
||||
//
|
||||
@ -87,7 +251,7 @@ func cmdRegisters(m action.Model, args []string) tea.Cmd {
|
||||
// :set number! - toggle number
|
||||
// :set tabstop=4 - set tabstop to 4
|
||||
// :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 {
|
||||
out := fmt.Sprintf("%+v", m.Settings())
|
||||
m.SetCommandOutput(out)
|
||||
|
||||
2947
internal/command/handlers_test.go
Normal file
2947
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 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 {
|
||||
Name string // Full name: "quit"
|
||||
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.
|
||||
@ -75,24 +75,34 @@ func (r *Registry) Lookup(input string) (*Command, error) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
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.
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd.Handler(m, args), nil
|
||||
return cmd.Handler(m, args, force), nil
|
||||
}
|
||||
|
||||
// DefaultRegistry is the global command registry
|
||||
@ -132,6 +142,12 @@ func (r *Registry) registerDefaults() {
|
||||
Handler: cmdWriteQuit,
|
||||
})
|
||||
|
||||
r.Register(Command{
|
||||
Name: "wqall",
|
||||
ShortForm: "wqa",
|
||||
Handler: cmdWriteQuitAll,
|
||||
})
|
||||
|
||||
// Set command
|
||||
r.Register(Command{
|
||||
Name: "set",
|
||||
@ -145,4 +161,11 @@ func (r *Registry) registerDefaults() {
|
||||
ShortForm: "reg",
|
||||
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) {
|
||||
_, err := r.Lookup("xyz")
|
||||
if err == nil {
|
||||
@ -106,17 +126,20 @@ func TestRegistryLookup(t *testing.T) {
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
t.Run("command only", func(t *testing.T) {
|
||||
name, args := Parse("quit")
|
||||
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 false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("command with one arg", func(t *testing.T) {
|
||||
name, args := Parse("set number")
|
||||
name, args, force := Parse("set number")
|
||||
if name != "set" {
|
||||
t.Errorf("name = %q, want \"set\"", name)
|
||||
}
|
||||
@ -126,10 +149,13 @@ func TestParse(t *testing.T) {
|
||||
if args[0] != "number" {
|
||||
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) {
|
||||
name, args := Parse("set number tabstop=4")
|
||||
name, args, force := Parse("set number tabstop=4")
|
||||
if name != "set" {
|
||||
t.Errorf("name = %q, want \"set\"", name)
|
||||
}
|
||||
@ -142,26 +168,74 @@ func TestParse(t *testing.T) {
|
||||
if args[1] != "tabstop=4" {
|
||||
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) {
|
||||
name, args := Parse("")
|
||||
name, args, force := Parse("")
|
||||
if name != "" {
|
||||
t.Errorf("name = %q, want \"\"", name)
|
||||
}
|
||||
if args != nil {
|
||||
t.Errorf("args = %v, want nil", args)
|
||||
}
|
||||
if force {
|
||||
t.Error("force should be false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("whitespace only", func(t *testing.T) {
|
||||
name, args := Parse(" ")
|
||||
name, args, force := Parse(" ")
|
||||
if name != "" {
|
||||
t.Errorf("name = %q, want \"\"", name)
|
||||
}
|
||||
if args != nil {
|
||||
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
|
||||
}
|
||||
|
||||
type BufferType int
|
||||
|
||||
const (
|
||||
ScatchBuffer BufferType = iota
|
||||
FileBuffer
|
||||
DirectoryBuffer
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
// Buffer data
|
||||
Id int
|
||||
Type BufferType
|
||||
|
||||
// File data
|
||||
Filename string
|
||||
@ -17,6 +26,7 @@ type Buffer struct {
|
||||
Modified bool
|
||||
Loaded bool
|
||||
Listed bool
|
||||
ReadOnly bool
|
||||
|
||||
// Options BufferOptions
|
||||
// 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
|
||||
// index is out of bounds.
|
||||
// index is out of bounds. This function sets the modified flag.
|
||||
func (b *Buffer) SetLine(idx int, content string) {
|
||||
if idx >= 0 && idx < len(b.Lines) {
|
||||
b.Lines[idx] = content
|
||||
}
|
||||
b.Modified = true
|
||||
}
|
||||
|
||||
// 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
|
||||
// the given index.
|
||||
// the given index. This function sets the modified flag.
|
||||
func (b *Buffer) InsertLine(idx int, content string) {
|
||||
if idx < 0 {
|
||||
idx = 0
|
||||
@ -54,14 +65,16 @@ func (b *Buffer) InsertLine(idx int, content string) {
|
||||
idx = len(b.Lines)
|
||||
}
|
||||
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
|
||||
// of bounds.
|
||||
// of bounds. This function sets the modified flag.
|
||||
func (b *Buffer) DeleteLine(idx int) {
|
||||
if idx >= 0 && idx < len(b.Lines) {
|
||||
b.Lines = append(b.Lines[:idx], b.Lines[idx+1:]...)
|
||||
}
|
||||
b.Modified = true
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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{
|
||||
buffer: Buffer{
|
||||
Id: 0, // This is set when built
|
||||
Type: ScatchBuffer, // Default buffer type
|
||||
Filename: "",
|
||||
Filetype: "",
|
||||
Lines: []string{""},
|
||||
Modified: false,
|
||||
Loaded: false,
|
||||
Listed: false,
|
||||
ReadOnly: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -62,6 +64,20 @@ func (b *BufferBuilder) Listed() *BufferBuilder {
|
||||
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
|
||||
// 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.
|
||||
|
||||
@ -40,8 +40,10 @@ type Window struct {
|
||||
// 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
|
||||
// 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).
|
||||
func (w *Window) clampCursor() {
|
||||
// of the content or attempt to be "above" the content (negative value). This function
|
||||
// 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]
|
||||
maxLine := max(w.Buffer.LineCount()-1, 0)
|
||||
if w.Cursor.Line < 0 {
|
||||
@ -50,8 +52,15 @@ func (w *Window) clampCursor() {
|
||||
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]
|
||||
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 {
|
||||
w.Cursor.Col = 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
|
||||
// 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) {
|
||||
w.Buffer = buffer
|
||||
w.ClampCursor()
|
||||
}
|
||||
|
||||
// Window.SetCursor: Sets the cursor position in this window to the given position.
|
||||
func (w *Window) SetCursor(cursor Position) {
|
||||
w.Cursor = cursor
|
||||
w.clampCursor()
|
||||
w.ClampCursor()
|
||||
}
|
||||
|
||||
// Window.SetCursorLine: Sets the line number of the cursor position.
|
||||
func (w *Window) SetCursorLine(line int) {
|
||||
w.Cursor.Line = line
|
||||
w.clampCursor()
|
||||
w.ClampCursor()
|
||||
}
|
||||
|
||||
// Window.SetCursorCol: Sets the column number of the cursor position.
|
||||
func (w *Window) SetCursorCol(col int) {
|
||||
w.Cursor.Col = col
|
||||
w.clampCursor()
|
||||
w.ClampCursor()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
w.Cursor.Line = line
|
||||
w.Cursor.Col = col
|
||||
w.clampCursor()
|
||||
w.ClampCursor()
|
||||
}
|
||||
|
||||
// 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
|
||||
type TestModelOption func(*testModelConfig)
|
||||
|
||||
|
||||
@ -8,8 +8,6 @@ import (
|
||||
|
||||
// NOTE: AI Generated tests
|
||||
|
||||
// Default settings are: Number=true, RelativeNumber=true, TabSize=2, ScrollOff=8
|
||||
|
||||
func TestCommandSetBoolean(t *testing.T) {
|
||||
t.Run("':set nonumber' disables line numbers", func(t *testing.T) {
|
||||
// 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
|
||||
}
|
||||
|
||||
func (m *Model) SetBuffers(bufs []*core.Buffer) {
|
||||
m.buffers = bufs
|
||||
}
|
||||
|
||||
func (m *Model) ActiveBuffer() *core.Buffer {
|
||||
win := m.ActiveWindow()
|
||||
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.
|
||||
func leftBar(w *core.Window, mode core.Mode) string {
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user