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.

cursor.go · 170 lines · 4.7 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 21h ago1package buffer
2
3import "unicode"
4
5// Cursor returns where the insertion point currently is.
6func (b *Buffer) Cursor() Position { return b.cursor }
7
8// SetCursor moves the insertion point, clamping it to a position that exists,
9// and drops the selection — that is what an unmodified arrow key does. Use
10// SetCursorKeepingSelection for Shift-arrow and for a mouse drag.
11//
12// Any pending run of typed characters stops merging into one undo step here,
13// because a deliberate move ends the "word" being typed.
14//
15// b := buffer.NewFromString("hello")
16// b.SetCursor(buffer.Position{Line: 0, Col: 99})
17// fmt.Println(b.Cursor().Col) // 5
18func (b *Buffer) SetCursor(p Position) {
19 b.cursor = b.clamp(p)
20 b.anchor = nil
21 b.coalesce = false
22}
23
24// MoveLeft steps one character back, wrapping onto the end of the previous
25// line. It is a no-op at the very start of the buffer.
26func (b *Buffer) MoveLeft() {
27 switch {
28 case b.cursor.Col > 0:
29 b.SetCursor(Position{Line: b.cursor.Line, Col: b.cursor.Col - 1})
30 case b.cursor.Line > 0:
31 prev := b.cursor.Line - 1
32 b.SetCursor(Position{Line: prev, Col: len(b.lines[prev])})
33 }
34}
35
36// MoveRight steps one character forward, wrapping onto the start of the next
37// line. It is a no-op at the very end of the buffer.
38func (b *Buffer) MoveRight() {
39 switch {
40 case b.cursor.Col < len(b.lines[b.cursor.Line]):
41 b.SetCursor(Position{Line: b.cursor.Line, Col: b.cursor.Col + 1})
42 case b.cursor.Line < len(b.lines)-1:
43 b.SetCursor(Position{Line: b.cursor.Line + 1, Col: 0})
44 }
45}
46
47// MoveUp steps to the previous line, keeping the column where it fits.
48func (b *Buffer) MoveUp() {
49 b.SetCursor(Position{Line: b.cursor.Line - 1, Col: b.cursor.Col})
50}
51
52// MoveDown steps to the next line, keeping the column where it fits.
53func (b *Buffer) MoveDown() {
54 b.SetCursor(Position{Line: b.cursor.Line + 1, Col: b.cursor.Col})
55}
56
57// MoveLineStart goes to column zero of the current line.
58func (b *Buffer) MoveLineStart() {
59 b.SetCursor(Position{Line: b.cursor.Line, Col: 0})
60}
61
62// MoveLineEnd goes just past the last character of the current line.
63func (b *Buffer) MoveLineEnd() {
64 b.SetCursor(Position{Line: b.cursor.Line, Col: len(b.lines[b.cursor.Line])})
65}
66
67// MoveBufferStart goes to the very first position of the text.
68func (b *Buffer) MoveBufferStart() {
69 b.SetCursor(Position{})
70}
71
72// MoveBufferEnd goes just past the very last character of the text.
73func (b *Buffer) MoveBufferEnd() {
74 b.SetCursor(b.wholeRange().End)
75}
76
77// MoveWordLeft goes to the start of the word before the cursor, skipping over
78// any separators in between, as Ctrl-Left does in Turbo C.
79func (b *Buffer) MoveWordLeft() {
80 if b.cursor.Col == 0 {
81 b.MoveLeft() // wrap onto the end of the previous line, or stay put
82 return
83 }
84
85 line := b.lines[b.cursor.Line]
86 col := b.cursor.Col
87 for col > 0 && !IsWordRune(line[col-1]) {
88 col--
89 }
90 for col > 0 && IsWordRune(line[col-1]) {
91 col--
92 }
93 b.SetCursor(Position{Line: b.cursor.Line, Col: col})
94}
95
96// MoveWordRight goes to the start of the word after the cursor, as Ctrl-Right
97// does in Turbo C.
98func (b *Buffer) MoveWordRight() {
99 line := b.lines[b.cursor.Line]
100 if b.cursor.Col >= len(line) {
101 b.MoveRight()
102 return
103 }
104
105 col := b.cursor.Col
106 for col < len(line) && IsWordRune(line[col]) {
107 col++
108 }
109 for col < len(line) && !IsWordRune(line[col]) {
110 col++
111 }
112 b.SetCursor(Position{Line: b.cursor.Line, Col: col})
113}
114
115// IsWordRune reports whether r belongs to an identifier.
116//
117// Word movement treats a run of them as one unit, and so does the completion
118// popup when it works out which word is being typed — which is why this is
119// exported rather than private to the cursor.
120//
121// buffer.IsWordRune('_') // true
122// buffer.IsWordRune('.') // false
123func IsWordRune(r rune) bool {
124 return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
125}
126
127// DisplayColumn converts a rune column on line into the screen column it
128// occupies once tabs are expanded.
129//
130// b := buffer.NewFromString("\tx")
131// fmt.Println(b.DisplayColumn(0, 1)) // 8, with the default tab width
132func (b *Buffer) DisplayColumn(line, col int) int {
133 runes := b.lineAt(line)
134 if col < 0 {
135 col = 0
136 }
137 if col > len(runes) {
138 col = len(runes)
139 }
140
141 screen := 0
142 for _, r := range runes[:col] {
143 if r == '\t' {
144 screen += b.tabWidth - screen%b.tabWidth
145 } else {
146 screen++
147 }
148 }
149 return screen
150}
151
152// RuneColumn is the inverse of DisplayColumn: it maps a screen column back to
153// the rune column the cursor should sit at, which is what a mouse click needs.
154func (b *Buffer) RuneColumn(line, screenCol int) int {
155 runes := b.lineAt(line)
156
157 screen := 0
158 for i, r := range runes {
159 width := 1
160 if r == '\t' {
161 width = b.tabWidth - screen%b.tabWidth
162 }
163 // A click in the second half of a wide tab lands on the next rune.
164 if screenCol < screen+width {
165 return i
166 }
167 screen += width
168 }
169 return len(runes)
170}