turbo-editors/turbo-corepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

file.go · 170 lines · 4.8 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 4h ago1package buffer
2
3import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8)
9
10// ErrNoPath is returned by Save when the buffer has never been given a file
11// name. Callers should prompt for one and use SaveAs instead.
12var ErrNoPath = errors.New("buffer: no file name; use SaveAs")
13
14// ErrModified is returned by Reload when the buffer has unsaved changes, which
15// reloading would throw away.
16var ErrModified = errors.New("buffer: unsaved changes; will not reload")
17
18// Open reads path into a new buffer.
19//
20// The file's line endings and its trailing newline, if any, are remembered so
21// that saving an unmodified buffer reproduces the file byte for byte. A
22// missing file is not an error: it yields an empty buffer bound to that path,
23// which is how "open a file that does not exist yet" works.
24//
25// b, err := buffer.Open("main.go")
26// if err != nil {
27// return err
28// }
29// fmt.Println(b.LineCount())
30func Open(path string) (*Buffer, error) {
31 b := New()
32 b.path = path
33
34 data, err := os.ReadFile(path)
35 if errors.Is(err, os.ErrNotExist) {
36 b.clearHistory()
37 return b, nil
38 }
39 if err != nil {
40 return nil, fmt.Errorf("buffer: open %s: %w", path, err)
41 }
42
43 b.SetText(string(data))
44 b.modified = false
45 b.clearHistory()
46 return b, nil
47}
48
49// Reload reads the buffer's file again, discarding what is in it.
50//
51// It reports whether the text actually changed, so a caller can tell a file
52// something rewrote from one it did not touch.
53//
54// The buffer must not be modified: reloading over unsaved work would throw it
55// away, and this returns ErrModified rather than doing that. That restriction
56// is the whole safety of the operation — a formatter rewriting a file under an
57// unmodified buffer costs nothing, and under a modified one costs the user's
58// work.
59//
60// The cursor is kept where it was, clamped into whatever the file now holds:
61// a formatter moves lines around, and putting the cursor back at the top
62// would lose the reader's place for no reason.
63//
64// changed, err := buf.Reload()
65// if errors.Is(err, buffer.ErrModified) {
66// // leave it alone and tell the user
67// }
68func (b *Buffer) Reload() (bool, error) {
69 if b.path == "" {
70 return false, ErrNoPath
71 }
72 if b.modified {
73 return false, ErrModified
74 }
75
76 data, err := os.ReadFile(b.path)
77 if err != nil {
78 return false, fmt.Errorf("buffer: reload %s: %w", b.path, err)
79 }
80 if string(data) == b.Text() {
81 return false, nil
82 }
83
84 cursor := b.cursor
85 b.SetText(string(data))
86 b.modified = false
87 b.clearHistory()
88 b.SetCursor(cursor)
89 return true, nil
90}
91
92// Save writes the buffer back to the file it came from and clears the modified
93// flag. It returns ErrNoPath if the buffer has no file name yet.
94func (b *Buffer) Save() error {
95 if b.path == "" {
96 return ErrNoPath
97 }
98 return b.SaveAs(b.path)
99}
100
101// SaveAs writes the buffer to path, adopts that path as its own, and clears
102// the modified flag.
103//
104// The write goes to a temporary file in the same directory which is then
105// renamed over the target, so an interrupted save cannot leave a half-written
106// source file behind.
107func (b *Buffer) SaveAs(path string) error {
108 if err := writeFileAtomically(path, []byte(b.Text())); err != nil {
109 return err
110 }
111 b.path = path
112 b.modified = false
113 return nil
114}
115
116// writeFileAtomically writes data to path via a temporary file and a rename,
117// so that readers of path only ever see the old content or the new one.
118func writeFileAtomically(path string, data []byte) error {
119 dir := filepath.Dir(path)
120
121 tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*")
122 if err != nil {
123 return fmt.Errorf("buffer: save %s: %w", path, err)
124 }
125 tmpName := tmp.Name()
126
127 // CreateTemp makes the file readable by its owner only; the rename would
128 // otherwise silently tighten the permissions of an existing source file.
129 if err := os.Chmod(tmpName, modeOf(path)); err != nil {
130 tmp.Close()
131 os.Remove(tmpName)
132 return fmt.Errorf("buffer: save %s: %w", path, err)
133 }
134
135 // From here on every failure must remove the temporary file, or a failed
136 // save would litter the user's source directory.
137 if err := writeAndClose(tmp, data); err != nil {
138 os.Remove(tmpName)
139 return fmt.Errorf("buffer: save %s: %w", path, err)
140 }
141
142 if err := os.Rename(tmpName, path); err != nil {
143 os.Remove(tmpName)
144 return fmt.Errorf("buffer: save %s: %w", path, err)
145 }
146 return nil
147}
148
149// writeAndClose writes data to f and closes it, reporting the first failure.
150func writeAndClose(f *os.File, data []byte) error {
151 if _, err := f.Write(data); err != nil {
152 f.Close()
153 return err
154 }
155 return f.Close()
156}
157
158// defaultFileMode is what a source file created by the editor gets, before the
159// process umask is applied.
160const defaultFileMode = 0o644
161
162// modeOf returns the permissions of an existing file, or defaultFileMode when
163// the file is new or cannot be inspected.
164func modeOf(path string) os.FileMode {
165 info, err := os.Stat(path)
166 if err != nil {
167 return defaultFileMode
168 }
169 return info.Mode().Perm()
170}