turbo-editors/turbo-corepublic Fork 0
v1.0.0
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 v1.0.0 · k33g · 15h ago
edit.go · 292 lines · 9.1 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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
package buffer

import "strings"

// Insert puts text in at the cursor, replacing the selection if there is one.
//
// Line feeds inside text split the current line, so a whole paste is a single
// call and a single undo step.
//
//	b := buffer.NewFromString("ab")
//	b.SetCursor(buffer.Position{Line: 0, Col: 1})
//	b.Insert("X")
//	fmt.Println(b.Line(0)) // aXb
func (b *Buffer) Insert(text string) {
	b.DeleteSelection()
	b.ReplaceRange(Range{Start: b.cursor, End: b.cursor}, text)
}

// InsertRune puts a single character in at the cursor, replacing the selection
// if there is one. Consecutive InsertRune calls that type forward on one line
// collapse into a single undo step, so undo removes a word rather than a
// letter.
func (b *Buffer) InsertRune(r rune) {
	b.DeleteSelection()
	b.ReplaceRange(Range{Start: b.cursor, End: b.cursor}, string(r))
	// Arm the merge for the *next* keystroke; the one just recorded stays a
	// step of its own unless more typing follows.
	b.coalesce = true
}

// InsertNewlineAndIndent splits the line at the cursor and copies the leading
// whitespace of the current line onto the new one, which is what you want when
// typing a block of Go code.
//
//	b := buffer.NewFromString("\tfoo()")
//	b.SetCursor(buffer.Position{Line: 0, Col: 6})
//	b.InsertNewlineAndIndent()
//	fmt.Printf("%q\n", b.Line(1)) // "\t"
func (b *Buffer) InsertNewlineAndIndent() {
	b.DeleteSelection()
	indent := leadingWhitespace(b.lines[b.cursor.Line])
	// Whitespace to the right of the cursor is not part of the indent: only
	// the run that precedes it can be carried over.
	if len(indent) > b.cursor.Col {
		indent = indent[:b.cursor.Col]
	}
	b.Insert("\n" + string(indent))
}

// Backspace deletes the selection, or the character before the cursor when
// nothing is selected. At the very start of a line it joins that line to the
// previous one. Consecutive backspaces collapse into a single undo step.
func (b *Buffer) Backspace() {
	if b.DeleteSelection() {
		return
	}
	if b.cursor == (Position{}) {
		return
	}

	start := b.cursor
	if start.Col > 0 {
		start.Col--
	} else {
		start.Line--
		start.Col = len(b.lines[start.Line])
	}

	b.DeleteRange(Range{Start: start, End: b.cursor})
	b.coalesce = true
}

// Delete removes the selection, or the character under the cursor when nothing
// is selected. At the end of a line it joins the next line onto this one.
func (b *Buffer) Delete() {
	if b.DeleteSelection() {
		return
	}

	end := b.cursor
	switch {
	case end.Col < len(b.lines[end.Line]):
		end.Col++
	case end.Line < len(b.lines)-1:
		end.Line++
		end.Col = 0
	default:
		return // end of the buffer, nothing to delete
	}

	b.DeleteRange(Range{Start: b.cursor, End: end})
}

// DeleteRange removes the text covered by r and puts the cursor where it
// started. Positions outside the buffer are clamped, so a caller may pass a
// generous range without checking its bounds first.
func (b *Buffer) DeleteRange(r Range) {
	b.ReplaceRange(r, "")
}

// ReplaceRange swaps the text covered by r for text and returns the position
// just after the inserted text, which is where a cursor following an edit
// belongs.
//
// This is the single point through which every change to the text passes: the
// undo history, the modified flag and the cursor are all maintained here.
func (b *Buffer) ReplaceRange(r Range, text string) Position {
	r = Range{Start: b.clamp(r.Start), End: b.clamp(r.End)}
	if r.End.Before(r.Start) {
		r.Start, r.End = r.End, r.Start
	}

	inserted := splitLines(text)
	change := edit{
		rng:          r,
		old:          b.textIn(r),
		new:          inserted,
		cursorBefore: b.cursor,
	}

	end := b.splice(r, inserted)
	b.cursor = end
	change.cursorAfter = end

	b.record(change)
	b.modified = true
	b.revision++
	b.ClearSelection()
	return end
}

// splice swaps the text covered by r for lines without touching the undo
// history, and returns the position just after the inserted text.
//
// r must already be clamped and correctly ordered.
func (b *Buffer) splice(r Range, lines [][]rune) Position {
	head := b.lines[r.Start.Line][:r.Start.Col]
	tail := b.lines[r.End.Line][r.End.Col:]

	replacement := make([][]rune, 0, len(lines))
	first := concat(head, lines[0])

	var end Position
	if len(lines) == 1 {
		end = Position{Line: r.Start.Line, Col: len(first)}
		replacement = append(replacement, concat(first, tail))
	} else {
		replacement = append(replacement, first)
		for _, middle := range lines[1 : len(lines)-1] {
			replacement = append(replacement, concat(middle, nil))
		}
		last := lines[len(lines)-1]
		end = Position{Line: r.Start.Line + len(lines) - 1, Col: len(last)}
		replacement = append(replacement, concat(last, tail))
	}

	b.lines = spliceLines(b.lines, r.Start.Line, r.End.Line+1, replacement)
	return end
}

// textIn returns a copy of the text covered by r, which must be clamped.
func (b *Buffer) textIn(r Range) [][]rune {
	if r.Start.Line == r.End.Line {
		return [][]rune{concat(b.lines[r.Start.Line][r.Start.Col:r.End.Col], nil)}
	}

	out := make([][]rune, 0, r.End.Line-r.Start.Line+1)
	out = append(out, concat(b.lines[r.Start.Line][r.Start.Col:], nil))
	for _, line := range b.lines[r.Start.Line+1 : r.End.Line] {
		out = append(out, concat(line, nil))
	}
	out = append(out, concat(b.lines[r.End.Line][:r.End.Col], nil))
	return out
}

// spliceLines returns lines with the half-open span [from, to) replaced by
// replacement. The input slice is left untouched.
func spliceLines(lines [][]rune, from, to int, replacement [][]rune) [][]rune {
	out := make([][]rune, 0, len(lines)-(to-from)+len(replacement))
	out = append(out, lines[:from]...)
	out = append(out, replacement...)
	out = append(out, lines[to:]...)
	return out
}

// concat returns a fresh slice holding a followed by b, so that the result
// shares no memory with either input.
func concat(a, b []rune) []rune {
	out := make([]rune, 0, len(a)+len(b))
	out = append(out, a...)
	out = append(out, b...)
	return out
}

// leadingWhitespace returns the run of spaces and tabs that starts the line.
func leadingWhitespace(line []rune) []rune {
	end := 0
	for end < len(line) && (line[end] == ' ' || line[end] == '\t') {
		end++
	}
	return line[:end]
}

// endOf returns the position just after lines, when they are laid down
// starting at start.
func endOf(start Position, lines [][]rune) Position {
	if len(lines) == 1 {
		return Position{Line: start.Line, Col: start.Col + len(lines[0])}
	}
	return Position{
		Line: start.Line + len(lines) - 1,
		Col:  len(lines[len(lines)-1]),
	}
}

// joinRunes renders lines as a single string separated by line feeds.
func joinRunes(lines [][]rune) string {
	parts := make([]string, len(lines))
	for i, line := range lines {
		parts[i] = string(line)
	}
	return strings.Join(parts, "\n")
}

// InsertLineAbove opens a blank line where the cursor is and pushes the
// current line down, leaving the cursor on the text it was on — which is now
// one line lower.
//
// This is Turbo C's Ctrl-N. It makes room *above* what you are looking at,
// which is what you want when the thing you are about to write belongs before
// the line in front of you.
//
//	b := buffer.NewFromString("second\n")
//	b.InsertLineAbove()
//	b.Text() // "\nsecond\n", cursor still on "second"
//
// The new line is blank rather than indented like its neighbour. Turbo C left
// it blank, and an indent nobody asked for becomes trailing whitespace the
// moment they change their mind.
func (b *Buffer) InsertLineAbove() {
	cursor := b.cursor
	start := Position{Line: cursor.Line, Col: 0}

	b.ReplaceRange(Range{Start: start, End: start}, "\n")
	b.SetCursor(Position{Line: cursor.Line + 1, Col: cursor.Col})
}

// DeleteLine removes the line the cursor is on, closing the gap.
//
// This is Turbo C's Ctrl-Y. The cursor stays on the same line number, which
// now holds what used to be the line below — so holding the key deletes a run
// of lines, which is the whole point of having it.
//
//	b := buffer.NewFromString("one\ntwo\nthree\n")
//	b.SetCursor(buffer.Position{Line: 1})
//	b.DeleteLine()
//	b.Text() // "one\nthree\n"
//
// The last line of a buffer has no newline after it to take, so its own
// leading newline goes instead. A buffer of one line keeps that line and is
// emptied rather than left with no lines at all, because every other operation
// here assumes there is always a line to be on.
//
// That one-line case is written out although ReplaceRange would reach the same
// answer without it — the range it would build starts on line −1, which clamp
// pulls back to 0. Leaning on that is a thing a reader has to work out from
// two files away, and the day clamp stops being so forgiving this would fail
// silently.
func (b *Buffer) DeleteLine() {
	line := b.cursor.Line
	column := b.cursor.Col

	switch {
	case b.LineCount() == 1:
		b.ReplaceRange(Range{
			Start: Position{Line: 0, Col: 0},
			End:   Position{Line: 0, Col: b.LineLen(0)},
		}, "")
	case line == b.LineCount()-1:
		b.ReplaceRange(Range{
			Start: Position{Line: line - 1, Col: b.LineLen(line - 1)},
			End:   Position{Line: line, Col: b.LineLen(line)},
		}, "")
	default:
		b.ReplaceRange(Range{
			Start: Position{Line: line, Col: 0},
			End:   Position{Line: line + 1, Col: 0},
		}, "")
	}

	b.SetCursor(Position{Line: min(line, b.LineCount()-1), Col: column})
}