package buffer import ( "errors" "fmt" "os" "path/filepath" ) // ErrNoPath is returned by Save when the buffer has never been given a file // name. Callers should prompt for one and use SaveAs instead. var ErrNoPath = errors.New("buffer: no file name; use SaveAs") // ErrModified is returned by Reload when the buffer has unsaved changes, which // reloading would throw away. var ErrModified = errors.New("buffer: unsaved changes; will not reload") // Open reads path into a new buffer. // // The file's line endings and its trailing newline, if any, are remembered so // that saving an unmodified buffer reproduces the file byte for byte. A // missing file is not an error: it yields an empty buffer bound to that path, // which is how "open a file that does not exist yet" works. // // b, err := buffer.Open("main.go") // if err != nil { // return err // } // fmt.Println(b.LineCount()) func Open(path string) (*Buffer, error) { b := New() b.path = path data, err := os.ReadFile(path) if errors.Is(err, os.ErrNotExist) { b.clearHistory() return b, nil } if err != nil { return nil, fmt.Errorf("buffer: open %s: %w", path, err) } b.SetText(string(data)) b.modified = false b.clearHistory() return b, nil } // Reload reads the buffer's file again, discarding what is in it. // // It reports whether the text actually changed, so a caller can tell a file // something rewrote from one it did not touch. // // The buffer must not be modified: reloading over unsaved work would throw it // away, and this returns ErrModified rather than doing that. That restriction // is the whole safety of the operation — a formatter rewriting a file under an // unmodified buffer costs nothing, and under a modified one costs the user's // work. // // The cursor is kept where it was, clamped into whatever the file now holds: // a formatter moves lines around, and putting the cursor back at the top // would lose the reader's place for no reason. // // changed, err := buf.Reload() // if errors.Is(err, buffer.ErrModified) { // // leave it alone and tell the user // } func (b *Buffer) Reload() (bool, error) { if b.path == "" { return false, ErrNoPath } if b.modified { return false, ErrModified } data, err := os.ReadFile(b.path) if err != nil { return false, fmt.Errorf("buffer: reload %s: %w", b.path, err) } if string(data) == b.Text() { return false, nil } cursor := b.cursor b.SetText(string(data)) b.modified = false b.clearHistory() b.SetCursor(cursor) return true, nil } // Save writes the buffer back to the file it came from and clears the modified // flag. It returns ErrNoPath if the buffer has no file name yet. func (b *Buffer) Save() error { if b.path == "" { return ErrNoPath } return b.SaveAs(b.path) } // SaveAs writes the buffer to path, adopts that path as its own, and clears // the modified flag. // // The write goes to a temporary file in the same directory which is then // renamed over the target, so an interrupted save cannot leave a half-written // source file behind. func (b *Buffer) SaveAs(path string) error { if err := writeFileAtomically(path, []byte(b.Text())); err != nil { return err } b.path = path b.modified = false return nil } // writeFileAtomically writes data to path via a temporary file and a rename, // so that readers of path only ever see the old content or the new one. func writeFileAtomically(path string, data []byte) error { dir := filepath.Dir(path) tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*") if err != nil { return fmt.Errorf("buffer: save %s: %w", path, err) } tmpName := tmp.Name() // CreateTemp makes the file readable by its owner only; the rename would // otherwise silently tighten the permissions of an existing source file. if err := os.Chmod(tmpName, modeOf(path)); err != nil { tmp.Close() os.Remove(tmpName) return fmt.Errorf("buffer: save %s: %w", path, err) } // From here on every failure must remove the temporary file, or a failed // save would litter the user's source directory. if err := writeAndClose(tmp, data); err != nil { os.Remove(tmpName) return fmt.Errorf("buffer: save %s: %w", path, err) } if err := os.Rename(tmpName, path); err != nil { os.Remove(tmpName) return fmt.Errorf("buffer: save %s: %w", path, err) } return nil } // writeAndClose writes data to f and closes it, reporting the first failure. func writeAndClose(f *os.File, data []byte) error { if _, err := f.Write(data); err != nil { f.Close() return err } return f.Close() } // defaultFileMode is what a source file created by the editor gets, before the // process umask is applied. const defaultFileMode = 0o644 // modeOf returns the permissions of an existing file, or defaultFileMode when // the file is new or cannot be inspected. func modeOf(path string) os.FileMode { info, err := os.Stat(path) if err != nil { return defaultFileMode } return info.Mode().Perm() }