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