turbo-editors/turbo-corepublic Fork 0
v0.9.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 v0.9.0 · k33g · 17h ago
cursor.go · 170 lines · 4.7 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 "unicode"

// Cursor returns where the insertion point currently is.
func (b *Buffer) Cursor() Position { return b.cursor }

// SetCursor moves the insertion point, clamping it to a position that exists,
// and drops the selection — that is what an unmodified arrow key does. Use
// SetCursorKeepingSelection for Shift-arrow and for a mouse drag.
//
// Any pending run of typed characters stops merging into one undo step here,
// because a deliberate move ends the "word" being typed.
//
//	b := buffer.NewFromString("hello")
//	b.SetCursor(buffer.Position{Line: 0, Col: 99})
//	fmt.Println(b.Cursor().Col) // 5
func (b *Buffer) SetCursor(p Position) {
	b.cursor = b.clamp(p)
	b.anchor = nil
	b.coalesce = false
}

// MoveLeft steps one character back, wrapping onto the end of the previous
// line. It is a no-op at the very start of the buffer.
func (b *Buffer) MoveLeft() {
	switch {
	case b.cursor.Col > 0:
		b.SetCursor(Position{Line: b.cursor.Line, Col: b.cursor.Col - 1})
	case b.cursor.Line > 0:
		prev := b.cursor.Line - 1
		b.SetCursor(Position{Line: prev, Col: len(b.lines[prev])})
	}
}

// MoveRight steps one character forward, wrapping onto the start of the next
// line. It is a no-op at the very end of the buffer.
func (b *Buffer) MoveRight() {
	switch {
	case b.cursor.Col < len(b.lines[b.cursor.Line]):
		b.SetCursor(Position{Line: b.cursor.Line, Col: b.cursor.Col + 1})
	case b.cursor.Line < len(b.lines)-1:
		b.SetCursor(Position{Line: b.cursor.Line + 1, Col: 0})
	}
}

// MoveUp steps to the previous line, keeping the column where it fits.
func (b *Buffer) MoveUp() {
	b.SetCursor(Position{Line: b.cursor.Line - 1, Col: b.cursor.Col})
}

// MoveDown steps to the next line, keeping the column where it fits.
func (b *Buffer) MoveDown() {
	b.SetCursor(Position{Line: b.cursor.Line + 1, Col: b.cursor.Col})
}

// MoveLineStart goes to column zero of the current line.
func (b *Buffer) MoveLineStart() {
	b.SetCursor(Position{Line: b.cursor.Line, Col: 0})
}

// MoveLineEnd goes just past the last character of the current line.
func (b *Buffer) MoveLineEnd() {
	b.SetCursor(Position{Line: b.cursor.Line, Col: len(b.lines[b.cursor.Line])})
}

// MoveBufferStart goes to the very first position of the text.
func (b *Buffer) MoveBufferStart() {
	b.SetCursor(Position{})
}

// MoveBufferEnd goes just past the very last character of the text.
func (b *Buffer) MoveBufferEnd() {
	b.SetCursor(b.wholeRange().End)
}

// MoveWordLeft goes to the start of the word before the cursor, skipping over
// any separators in between, as Ctrl-Left does in Turbo C.
func (b *Buffer) MoveWordLeft() {
	if b.cursor.Col == 0 {
		b.MoveLeft() // wrap onto the end of the previous line, or stay put
		return
	}

	line := b.lines[b.cursor.Line]
	col := b.cursor.Col
	for col > 0 && !IsWordRune(line[col-1]) {
		col--
	}
	for col > 0 && IsWordRune(line[col-1]) {
		col--
	}
	b.SetCursor(Position{Line: b.cursor.Line, Col: col})
}

// MoveWordRight goes to the start of the word after the cursor, as Ctrl-Right
// does in Turbo C.
func (b *Buffer) MoveWordRight() {
	line := b.lines[b.cursor.Line]
	if b.cursor.Col >= len(line) {
		b.MoveRight()
		return
	}

	col := b.cursor.Col
	for col < len(line) && IsWordRune(line[col]) {
		col++
	}
	for col < len(line) && !IsWordRune(line[col]) {
		col++
	}
	b.SetCursor(Position{Line: b.cursor.Line, Col: col})
}

// IsWordRune reports whether r belongs to an identifier.
//
// Word movement treats a run of them as one unit, and so does the completion
// popup when it works out which word is being typed — which is why this is
// exported rather than private to the cursor.
//
//	buffer.IsWordRune('_')  // true
//	buffer.IsWordRune('.')  // false
func IsWordRune(r rune) bool {
	return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
}

// DisplayColumn converts a rune column on line into the screen column it
// occupies once tabs are expanded.
//
//	b := buffer.NewFromString("\tx")
//	fmt.Println(b.DisplayColumn(0, 1)) // 8, with the default tab width
func (b *Buffer) DisplayColumn(line, col int) int {
	runes := b.lineAt(line)
	if col < 0 {
		col = 0
	}
	if col > len(runes) {
		col = len(runes)
	}

	screen := 0
	for _, r := range runes[:col] {
		if r == '\t' {
			screen += b.tabWidth - screen%b.tabWidth
		} else {
			screen++
		}
	}
	return screen
}

// RuneColumn is the inverse of DisplayColumn: it maps a screen column back to
// the rune column the cursor should sit at, which is what a mouse click needs.
func (b *Buffer) RuneColumn(line, screenCol int) int {
	runes := b.lineAt(line)

	screen := 0
	for i, r := range runes {
		width := 1
		if r == '\t' {
			width = b.tabWidth - screen%b.tabWidth
		}
		// A click in the second half of a wide tab lands on the next rune.
		if screenCol < screen+width {
			return i
		}
		screen += width
	}
	return len(runes)
}