turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
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.

🛟 Updated. 28d5985 · on 28d59854361aeda8541d853093e732126f3d7bff · k33g · 18h ago
file.go · 170 lines · 4.8 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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()
}