package buffer import ( "bytes" "fmt" "log/slog" "os" "time" "code.chimeric.al/dwrz/wen/internal/editor/buffer/line" ) type Buffer struct { cursor *Cursor lines []*line.Line log *slog.Logger name string offset *offset saved time.Time } // offset is the 0-indexed first visible display column (glyph) and row (line) // of the buffer viewport. type offset struct { glyph int line int } func (b *Buffer) CursorLine() *line.Line { return b.lines[b.cursor.line] } func (b *Buffer) Name() string { return b.name } type NewBufferParams struct { Name string Log *slog.Logger } // Create creates or truncates the named file on disk. // It returns a Buffer initialized with a single empty line, // so CursorLine is always valid. func Create(p NewBufferParams) (*Buffer, error) { var b = &Buffer{ cursor: &Cursor{}, lines: []*line.Line{{}}, log: p.Log, name: p.Name, offset: &offset{}, } // Create the file. f, err := os.Create(b.name) if err != nil { return nil, fmt.Errorf("failed to create file: %w", err) } if err := f.Close(); err != nil { return nil, fmt.Errorf("failed to close file: %w", err) } return b, nil } // Open reads the named file into a Buffer. If the file is empty, the Buffer // is initialized with a single empty line so CursorLine is always valid. func Open(p NewBufferParams) (*Buffer, error) { var b = &Buffer{ cursor: &Cursor{}, lines: []*line.Line{}, log: p.Log, name: p.Name, offset: &offset{}, } data, err := os.ReadFile(b.name) if err != nil { return nil, fmt.Errorf("failed to open file: %w", err) } // Trim a single trailing newline so saving doesn't add an extra blank line. data = bytes.TrimSuffix(data, []byte("\n")) data = bytes.TrimSuffix(data, []byte("\r")) if len(data) > 0 { for _, raw := range bytes.Split(data, []byte("\n")) { raw = bytes.TrimSuffix(raw, []byte("\r")) b.lines = append(b.lines, line.New(string(raw))) } } // Add a line to write in, if the file is empty. if len(b.lines) == 0 { b.lines = append(b.lines, &line.Line{}) } return b, nil }