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
buffer.go · 214 lines · 5.9 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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package buffer

import "strings"

// DefaultTabWidth is how many columns a tab character spans on screen unless
// the caller says otherwise. Eight matches gofmt's own assumption.
const DefaultTabWidth = 8

// Buffer is the text of one file, held as a slice of lines.
//
// Every buffer has at least one line, so an empty buffer is a single empty
// line rather than no line at all. That invariant removes a special case from
// every method below.
//
// A Buffer is not safe for concurrent use.
type Buffer struct {
	lines [][]rune

	path         string
	modified     bool
	crlf         bool // the file used \r\n line endings when it was read
	finalNewline bool // the file ended with a line terminator

	cursor Position
	anchor *Position // start of the selection, nil when nothing is selected

	tabWidth int

	undoStack []edit
	redoStack []edit
	coalesce  bool // the next edit may merge into the previous undo entry

	revision int // bumped on every change, so caches know when to rebuild
}

// New returns an empty buffer holding a single empty line.
//
// Its text is the empty string, not a lone line feed: the buffer holds exactly
// what was put into it, and nothing is put into a new one. Pressing Enter at
// the end of the last line is what gives a file its trailing newline, here as
// in any other editor.
//
//	b := buffer.New()
//	b.Insert("package main\n")
//	fmt.Println(b.LineCount()) // 2
func New() *Buffer {
	return &Buffer{
		lines:    [][]rune{{}},
		tabWidth: DefaultTabWidth,
	}
}

// NewFromString returns a buffer holding text, split on line feeds.
//
// A trailing line feed is remembered rather than turned into an extra empty
// line, so that saving the buffer reproduces the input byte for byte.
//
//	b := buffer.NewFromString("package main\n\nfunc main() {}\n")
//	fmt.Println(b.LineCount()) // 3
func NewFromString(text string) *Buffer {
	b := New()
	b.SetText(text)
	b.modified = false
	b.clearHistory()
	return b
}

// SetText replaces the whole content of the buffer, resets the cursor to the
// start and drops the selection. The undo history is kept, so loading a
// template over a buffer stays undoable.
func (b *Buffer) SetText(text string) {
	b.crlf = strings.Contains(text, "\r\n")
	if b.crlf {
		text = strings.ReplaceAll(text, "\r\n", "\n")
	}

	b.finalNewline = strings.HasSuffix(text, "\n")
	if b.finalNewline {
		text = strings.TrimSuffix(text, "\n")
	}

	b.ReplaceRange(b.wholeRange(), text)
	b.SetCursor(Position{})
	b.ClearSelection()
}

// Text returns the whole content of the buffer, with the line endings the file
// was read with and the trailing newline it had, if any.
func (b *Buffer) Text() string {
	sep := "\n"
	if b.crlf {
		sep = "\r\n"
	}

	parts := make([]string, len(b.lines))
	for i, line := range b.lines {
		parts[i] = string(line)
	}

	text := strings.Join(parts, sep)
	if b.finalNewline {
		text += sep
	}
	return text
}

// LineCount returns the number of lines, always at least one.
func (b *Buffer) LineCount() int {
	return len(b.lines)
}

// Line returns line i as a string, or the empty string if i is out of range.
func (b *Buffer) Line(i int) string {
	if i < 0 || i >= len(b.lines) {
		return ""
	}
	return string(b.lines[i])
}

// LineRunes returns a copy of line i. Callers may modify the result freely; it
// is disconnected from the buffer.
func (b *Buffer) LineRunes(i int) []rune {
	if i < 0 || i >= len(b.lines) {
		return nil
	}
	out := make([]rune, len(b.lines[i]))
	copy(out, b.lines[i])
	return out
}

// LineLen returns the number of runes on line i, or zero if i is out of range.
func (b *Buffer) LineLen(i int) int {
	if i < 0 || i >= len(b.lines) {
		return 0
	}
	return len(b.lines[i])
}

// Path returns the file this buffer was read from, or the empty string for a
// buffer that has never been associated with a file.
func (b *Buffer) Path() string { return b.path }

// SetPath records the file this buffer belongs to, as Save As does.
func (b *Buffer) SetPath(path string) { b.path = path }

// Modified reports whether the buffer has unsaved changes. Undoing back to the
// state that was last saved still counts as modified: the flag errs towards
// offering to save rather than towards losing work.
func (b *Buffer) Modified() bool { return b.modified }

// Revision is a counter bumped on every change to the text. Two reads that
// return the same number are guaranteed to have seen the same text, which is
// what lets the syntax highlighter skip work between redraws.
func (b *Buffer) Revision() int { return b.revision }

// TabWidth returns how many columns a tab character spans on screen.
func (b *Buffer) TabWidth() int { return b.tabWidth }

// SetTabWidth sets how many columns a tab character spans on screen. Values
// below one are ignored, since a zero-width tab would make columns ambiguous.
func (b *Buffer) SetTabWidth(width int) {
	if width > 0 {
		b.tabWidth = width
	}
}

// wholeRange returns the range covering every character in the buffer.
func (b *Buffer) wholeRange() Range {
	last := len(b.lines) - 1
	return Range{
		Start: Position{},
		End:   Position{Line: last, Col: len(b.lines[last])},
	}
}

// lineAt returns line i, clamped into range. A buffer always holds at least
// one line, so the result is never nil.
func (b *Buffer) lineAt(i int) []rune {
	if i < 0 {
		return b.lines[0]
	}
	if i >= len(b.lines) {
		return b.lines[len(b.lines)-1]
	}
	return b.lines[i]
}

// clamp moves p to the nearest position that actually exists in the buffer.
func (b *Buffer) clamp(p Position) Position {
	if p.Line < 0 {
		return Position{}
	}
	if p.Line >= len(b.lines) {
		last := len(b.lines) - 1
		return Position{Line: last, Col: len(b.lines[last])}
	}
	if p.Col < 0 {
		p.Col = 0
	}
	if p.Col > len(b.lines[p.Line]) {
		p.Col = len(b.lines[p.Line])
	}
	return p
}

// splitLines turns text into lines of runes, always returning at least one.
func splitLines(text string) [][]rune {
	parts := strings.Split(text, "\n")
	lines := make([][]rune, len(parts))
	for i, part := range parts {
		lines[i] = []rune(part)
	}
	return lines
}