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

actions_edit.go · 187 lines · 5.4 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g yesterday1// Everything the Edit and Search menus do: the clipboard, undo, and moving
2// about the file.
3package app
4
5import (
6 "context"
7 "fmt"
8 "strconv"
9 "strings"
10
📦 Turbo Core f3ade8d k33g 23h ago11 "rickub.com/turbo-editors/turbo-core/buffer"
12 "rickub.com/turbo-editors/turbo-core/editor"
13 "rickub.com/turbo-editors/turbo-core/lsp"
14 "rickub.com/turbo-editors/turbo-core/ui"
🛟 Updated. 28d5985 k33g yesterday15)
16
17// Undo, Redo, Cut, Copy, Paste and SelectAll forward to the front window.
18func (a *App) Undo() { a.withView(func(v *editor.View) { v.Undo() }) }
19func (a *App) Redo() { a.withView(func(v *editor.View) { v.Redo() }) }
20func (a *App) Cut() { a.withView(func(v *editor.View) { v.Cut() }) }
21func (a *App) Copy() { a.withView(func(v *editor.View) { v.Copy() }) }
22func (a *App) SelectAll() { a.withView(func(v *editor.View) { v.SelectAll() }) }
23
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 1h ago24// Paste puts the clipboard into the front window: the file being edited, or
25// the box of an agent window, which is the one other place text is typed.
26func (a *App) Paste() {
27 if agent := a.activeAgent(); agent != nil {
28 agent.view.Paste(a.clipboard.Text())
29 return
30 }
31 a.withView(func(v *editor.View) { v.Paste() })
32}
33
🛟 Updated. 28d5985 k33g yesterday34// InsertLine and DeleteLine are Turbo C's Ctrl-N and Ctrl-Y — a blank line
35// opened above the cursor, and the cursor's own line removed.
36func (a *App) InsertLine() { a.withView(func(v *editor.View) { v.InsertLine() }) }
37func (a *App) DeleteLine() { a.withView(func(v *editor.View) { v.DeleteLine() }) }
38
39// withView runs an action on the front window's view, if there is one.
40func (a *App) withView(action func(*editor.View)) {
41 if view := a.activeView(); view != nil {
42 action(view)
43 }
44}
45
46// Find asks what to search for and jumps to the first match.
47func (a *App) Find() {
48 view := a.activeView()
49 if view == nil {
50 return
51 }
52
53 dialog := NewFindDialog(a.lastSearch, a.lastMatchCase, a.screenRect())
54 a.pushModal(dialog.Dialog(), func(result ui.Result) {
55 if result != ui.ResultOK {
56 return
57 }
58 a.lastSearch, a.lastMatchCase = dialog.Needle(), dialog.MatchCase()
59 a.FindNext()
60 })
61}
62
63// FindNext jumps to the next match of the last search.
64func (a *App) FindNext() { a.search(true) }
65
66// FindPrevious jumps to the previous match of the last search.
67func (a *App) FindPrevious() { a.search(false) }
68
69// search moves the cursor to the next or previous match and selects it.
70func (a *App) search(forwards bool) {
71 view := a.activeView()
72 if view == nil || a.lastSearch == "" {
73 return
74 }
75 buf := view.Buffer()
76
77 found, ok := a.nextMatch(buf, forwards)
78 if !ok {
79 a.Message(fmt.Sprintf("%q not found", a.lastSearch))
80 return
81 }
82
83 buf.SetCursor(found.Start)
84 buf.StartSelection()
85 buf.SetCursorKeepingSelection(found.End)
86 view.EnsureCursorVisible()
87}
88
89// nextMatch finds the next or previous occurrence of the current search.
90//
91// A forward search starts one column past the cursor, so pressing "find next"
92// on a match moves off it instead of finding it again.
93func (a *App) nextMatch(buf *buffer.Buffer, forwards bool) (buffer.Range, bool) {
94 from := buf.Cursor()
95 if forwards {
96 from.Col++
97 return buf.Find(a.lastSearch, from, a.lastMatchCase)
98 }
99 return buf.FindPrevious(a.lastSearch, from, a.lastMatchCase)
100}
101
102// GoToLine asks for a line number and jumps to it.
103func (a *App) GoToLine() {
104 view := a.activeView()
105 if view == nil {
106 return
107 }
108
109 dialog, field := NewPromptDialog("Go to line", "Line number:", "", a.screenRect())
110 a.pushModal(dialog, func(result ui.Result) {
111 if result != ui.ResultOK {
112 return
113 }
114 line, err := strconv.Atoi(strings.TrimSpace(field.Text()))
115 if err != nil || line < 1 {
116 a.Message("Not a line number")
117 return
118 }
119 view.GoToLine(line)
120 })
121}
122
123// GoToDefinition asks the language server where the symbol under the cursor is
124// declared, and opens it.
125//
126// A symbol may have more than one declaration — a Go interface method is
127// declared once per implementation — and then the list is offered rather than
128// the first one taken. It used to take locations[0] and throw the rest away,
129// which meant the editor silently answered a different question from the one
130// asked whenever the answer was interesting.
131func (a *App) GoToDefinition() {
132 a.askForLocations("Definition", a.language.Definition)
133}
134
135// jumpTo opens the file a location names and puts the cursor on it.
136func (a *App) jumpTo(location lsp.Location) {
137 path := lsp.URIToPath(location.URI)
138 a.Open(path)
139
140 view := a.activeView()
141 if view == nil {
142 return
143 }
144 line := location.Range.Start.Line
145 column := lsp.UTF16ToRune(view.Buffer().Line(line), location.Range.Start.Character)
146
147 view.Buffer().SetCursor(buffer.Position{Line: line, Col: column})
148 view.EnsureCursorVisible()
149}
150
151// DescribeSymbol shows what the language server knows about the symbol under
152// the cursor.
153func (a *App) DescribeSymbol() {
154 view := a.activeView()
155 if view == nil {
156 a.ShowKeyboardHelp()
157 return
158 }
159 buf := view.Buffer()
160 cursor := buf.Cursor()
161
162 text, err := a.language.Hover(context.Background(),
163 buf.Path(), cursor.Line, cursor.Col, buf.Line(cursor.Line))
164 if err != nil || strings.TrimSpace(text) == "" {
165 a.Message("Nothing to describe here")
166 return
167 }
168 a.ShowMessage("Symbol", trimHover(text))
169}
170
171// trimHover cuts a hover down to what fits in a small box: its first few
172// lines, with Markdown fences taken out.
173func trimHover(text string) string {
174 const maxLines = 8
175
176 var kept []string
177 for _, line := range strings.Split(text, "\n") {
178 if strings.HasPrefix(line, "```") {
179 continue
180 }
181 kept = append(kept, line)
182 if len(kept) == maxLines {
183 break
184 }
185 }
186 return strings.TrimSpace(strings.Join(kept, "\n"))
187}