turbo-editors/turbo-corepublic Fork 0
v1.0.1
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.

edit.go · 292 lines · 9.1 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 18h ago1package buffer
2
3import "strings"
4
5// Insert puts text in at the cursor, replacing the selection if there is one.
6//
7// Line feeds inside text split the current line, so a whole paste is a single
8// call and a single undo step.
9//
10// b := buffer.NewFromString("ab")
11// b.SetCursor(buffer.Position{Line: 0, Col: 1})
12// b.Insert("X")
13// fmt.Println(b.Line(0)) // aXb
14func (b *Buffer) Insert(text string) {
15 b.DeleteSelection()
16 b.ReplaceRange(Range{Start: b.cursor, End: b.cursor}, text)
17}
18
19// InsertRune puts a single character in at the cursor, replacing the selection
20// if there is one. Consecutive InsertRune calls that type forward on one line
21// collapse into a single undo step, so undo removes a word rather than a
22// letter.
23func (b *Buffer) InsertRune(r rune) {
24 b.DeleteSelection()
25 b.ReplaceRange(Range{Start: b.cursor, End: b.cursor}, string(r))
26 // Arm the merge for the *next* keystroke; the one just recorded stays a
27 // step of its own unless more typing follows.
28 b.coalesce = true
29}
30
31// InsertNewlineAndIndent splits the line at the cursor and copies the leading
32// whitespace of the current line onto the new one, which is what you want when
33// typing a block of Go code.
34//
35// b := buffer.NewFromString("\tfoo()")
36// b.SetCursor(buffer.Position{Line: 0, Col: 6})
37// b.InsertNewlineAndIndent()
38// fmt.Printf("%q\n", b.Line(1)) // "\t"
39func (b *Buffer) InsertNewlineAndIndent() {
40 b.DeleteSelection()
41 indent := leadingWhitespace(b.lines[b.cursor.Line])
42 // Whitespace to the right of the cursor is not part of the indent: only
43 // the run that precedes it can be carried over.
44 if len(indent) > b.cursor.Col {
45 indent = indent[:b.cursor.Col]
46 }
47 b.Insert("\n" + string(indent))
48}
49
50// Backspace deletes the selection, or the character before the cursor when
51// nothing is selected. At the very start of a line it joins that line to the
52// previous one. Consecutive backspaces collapse into a single undo step.
53func (b *Buffer) Backspace() {
54 if b.DeleteSelection() {
55 return
56 }
57 if b.cursor == (Position{}) {
58 return
59 }
60
61 start := b.cursor
62 if start.Col > 0 {
63 start.Col--
64 } else {
65 start.Line--
66 start.Col = len(b.lines[start.Line])
67 }
68
69 b.DeleteRange(Range{Start: start, End: b.cursor})
70 b.coalesce = true
71}
72
73// Delete removes the selection, or the character under the cursor when nothing
74// is selected. At the end of a line it joins the next line onto this one.
75func (b *Buffer) Delete() {
76 if b.DeleteSelection() {
77 return
78 }
79
80 end := b.cursor
81 switch {
82 case end.Col < len(b.lines[end.Line]):
83 end.Col++
84 case end.Line < len(b.lines)-1:
85 end.Line++
86 end.Col = 0
87 default:
88 return // end of the buffer, nothing to delete
89 }
90
91 b.DeleteRange(Range{Start: b.cursor, End: end})
92}
93
94// DeleteRange removes the text covered by r and puts the cursor where it
95// started. Positions outside the buffer are clamped, so a caller may pass a
96// generous range without checking its bounds first.
97func (b *Buffer) DeleteRange(r Range) {
98 b.ReplaceRange(r, "")
99}
100
101// ReplaceRange swaps the text covered by r for text and returns the position
102// just after the inserted text, which is where a cursor following an edit
103// belongs.
104//
105// This is the single point through which every change to the text passes: the
106// undo history, the modified flag and the cursor are all maintained here.
107func (b *Buffer) ReplaceRange(r Range, text string) Position {
108 r = Range{Start: b.clamp(r.Start), End: b.clamp(r.End)}
109 if r.End.Before(r.Start) {
110 r.Start, r.End = r.End, r.Start
111 }
112
113 inserted := splitLines(text)
114 change := edit{
115 rng: r,
116 old: b.textIn(r),
117 new: inserted,
118 cursorBefore: b.cursor,
119 }
120
121 end := b.splice(r, inserted)
122 b.cursor = end
123 change.cursorAfter = end
124
125 b.record(change)
126 b.modified = true
127 b.revision++
128 b.ClearSelection()
129 return end
130}
131
132// splice swaps the text covered by r for lines without touching the undo
133// history, and returns the position just after the inserted text.
134//
135// r must already be clamped and correctly ordered.
136func (b *Buffer) splice(r Range, lines [][]rune) Position {
137 head := b.lines[r.Start.Line][:r.Start.Col]
138 tail := b.lines[r.End.Line][r.End.Col:]
139
140 replacement := make([][]rune, 0, len(lines))
141 first := concat(head, lines[0])
142
143 var end Position
144 if len(lines) == 1 {
145 end = Position{Line: r.Start.Line, Col: len(first)}
146 replacement = append(replacement, concat(first, tail))
147 } else {
148 replacement = append(replacement, first)
149 for _, middle := range lines[1 : len(lines)-1] {
150 replacement = append(replacement, concat(middle, nil))
151 }
152 last := lines[len(lines)-1]
153 end = Position{Line: r.Start.Line + len(lines) - 1, Col: len(last)}
154 replacement = append(replacement, concat(last, tail))
155 }
156
157 b.lines = spliceLines(b.lines, r.Start.Line, r.End.Line+1, replacement)
158 return end
159}
160
161// textIn returns a copy of the text covered by r, which must be clamped.
162func (b *Buffer) textIn(r Range) [][]rune {
163 if r.Start.Line == r.End.Line {
164 return [][]rune{concat(b.lines[r.Start.Line][r.Start.Col:r.End.Col], nil)}
165 }
166
167 out := make([][]rune, 0, r.End.Line-r.Start.Line+1)
168 out = append(out, concat(b.lines[r.Start.Line][r.Start.Col:], nil))
169 for _, line := range b.lines[r.Start.Line+1 : r.End.Line] {
170 out = append(out, concat(line, nil))
171 }
172 out = append(out, concat(b.lines[r.End.Line][:r.End.Col], nil))
173 return out
174}
175
176// spliceLines returns lines with the half-open span [from, to) replaced by
177// replacement. The input slice is left untouched.
178func spliceLines(lines [][]rune, from, to int, replacement [][]rune) [][]rune {
179 out := make([][]rune, 0, len(lines)-(to-from)+len(replacement))
180 out = append(out, lines[:from]...)
181 out = append(out, replacement...)
182 out = append(out, lines[to:]...)
183 return out
184}
185
186// concat returns a fresh slice holding a followed by b, so that the result
187// shares no memory with either input.
188func concat(a, b []rune) []rune {
189 out := make([]rune, 0, len(a)+len(b))
190 out = append(out, a...)
191 out = append(out, b...)
192 return out
193}
194
195// leadingWhitespace returns the run of spaces and tabs that starts the line.
196func leadingWhitespace(line []rune) []rune {
197 end := 0
198 for end < len(line) && (line[end] == ' ' || line[end] == '\t') {
199 end++
200 }
201 return line[:end]
202}
203
204// endOf returns the position just after lines, when they are laid down
205// starting at start.
206func endOf(start Position, lines [][]rune) Position {
207 if len(lines) == 1 {
208 return Position{Line: start.Line, Col: start.Col + len(lines[0])}
209 }
210 return Position{
211 Line: start.Line + len(lines) - 1,
212 Col: len(lines[len(lines)-1]),
213 }
214}
215
216// joinRunes renders lines as a single string separated by line feeds.
217func joinRunes(lines [][]rune) string {
218 parts := make([]string, len(lines))
219 for i, line := range lines {
220 parts[i] = string(line)
221 }
222 return strings.Join(parts, "\n")
223}
224
225// InsertLineAbove opens a blank line where the cursor is and pushes the
226// current line down, leaving the cursor on the text it was on — which is now
227// one line lower.
228//
229// This is Turbo C's Ctrl-N. It makes room *above* what you are looking at,
230// which is what you want when the thing you are about to write belongs before
231// the line in front of you.
232//
233// b := buffer.NewFromString("second\n")
234// b.InsertLineAbove()
235// b.Text() // "\nsecond\n", cursor still on "second"
236//
237// The new line is blank rather than indented like its neighbour. Turbo C left
238// it blank, and an indent nobody asked for becomes trailing whitespace the
239// moment they change their mind.
240func (b *Buffer) InsertLineAbove() {
241 cursor := b.cursor
242 start := Position{Line: cursor.Line, Col: 0}
243
244 b.ReplaceRange(Range{Start: start, End: start}, "\n")
245 b.SetCursor(Position{Line: cursor.Line + 1, Col: cursor.Col})
246}
247
248// DeleteLine removes the line the cursor is on, closing the gap.
249//
250// This is Turbo C's Ctrl-Y. The cursor stays on the same line number, which
251// now holds what used to be the line below — so holding the key deletes a run
252// of lines, which is the whole point of having it.
253//
254// b := buffer.NewFromString("one\ntwo\nthree\n")
255// b.SetCursor(buffer.Position{Line: 1})
256// b.DeleteLine()
257// b.Text() // "one\nthree\n"
258//
259// The last line of a buffer has no newline after it to take, so its own
260// leading newline goes instead. A buffer of one line keeps that line and is
261// emptied rather than left with no lines at all, because every other operation
262// here assumes there is always a line to be on.
263//
264// That one-line case is written out although ReplaceRange would reach the same
265// answer without it — the range it would build starts on line −1, which clamp
266// pulls back to 0. Leaning on that is a thing a reader has to work out from
267// two files away, and the day clamp stops being so forgiving this would fail
268// silently.
269func (b *Buffer) DeleteLine() {
270 line := b.cursor.Line
271 column := b.cursor.Col
272
273 switch {
274 case b.LineCount() == 1:
275 b.ReplaceRange(Range{
276 Start: Position{Line: 0, Col: 0},
277 End: Position{Line: 0, Col: b.LineLen(0)},
278 }, "")
279 case line == b.LineCount()-1:
280 b.ReplaceRange(Range{
281 Start: Position{Line: line - 1, Col: b.LineLen(line - 1)},
282 End: Position{Line: line, Col: b.LineLen(line)},
283 }, "")
284 default:
285 b.ReplaceRange(Range{
286 Start: Position{Line: line, Col: 0},
287 End: Position{Line: line + 1, Col: 0},
288 }, "")
289 }
290
291 b.SetCursor(Position{Line: min(line, b.LineCount()-1), Col: column})
292}