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

code.go · 142 lines · 5.1 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 5h ago1// The Code menu: everything the editor asks the language server about the
2// symbol under the cursor, and what it does with the answers.
3
4package app
5
6import (
7 "context"
8 "errors"
9 "fmt"
10 "os"
11 "path/filepath"
12 "strings"
13
14 "codeberg.org/turbo-editors/turbo-core/lsp"
15 "codeberg.org/turbo-editors/turbo-core/ui"
16)
17
18// GoToTypeDefinition opens the declaration of the *type* of the thing under
19// the cursor, which is a different question from where the thing itself is.
20func (a *App) GoToTypeDefinition() {
21 a.askForLocations("Type definition", a.language.TypeDefinition)
22}
23
24// FindImplementations lists what implements the thing under the cursor: the
25// types satisfying an interface, the impl blocks of a trait.
26func (a *App) FindImplementations() {
27 a.askForLocations("Implementations", a.language.Implementation)
28}
29
30// FindReferences lists where the thing under the cursor is used.
31func (a *App) FindReferences() {
32 a.askForLocations("References", a.language.References)
33}
34
35// locationQuestion is any of the four requests that answer with places in the
36// code. They differ only in their name, so they are asked the same way.
37type locationQuestion func(ctx context.Context, path string, line, column int, lineText string) ([]lsp.Location, error)
38
39// askForLocations puts one question to the server and shows the answer: it
40// jumps straight to a single result, offers a list for several, and says so
41// for none.
42//
43// The title doubles as the word used in the messages, so a question that
44// finds nothing says "No references found" rather than something generic.
45func (a *App) askForLocations(title string, ask locationQuestion) {
46 view := a.activeView()
47 if view == nil {
48 return
49 }
50 buf := view.Buffer()
51 cursor := buf.Cursor()
52
53 locations, err := ask(context.Background(), buf.Path(), cursor.Line, cursor.Col, buf.Line(cursor.Line))
54 a.showLocations(title, locations, err)
55}
56
57// showLocations is what every location question does with its answer.
58//
59// The three outcomes are deliberately distinct. A server that has not finished
60// loading says so, because "nothing found" and "I cannot answer yet" look
61// identical to a user and only one of them is worth waiting out — it is the
62// most confusing way completion fails, and this would have inherited it.
63func (a *App) showLocations(title string, locations []lsp.Location, err error) {
64 switch {
65 case errors.Is(err, lsp.ErrNotReady):
66 a.Message(a.language.Status())
67 case err != nil:
68 a.Message(strings.ToLower(title) + ": " + err.Error())
69 case len(locations) == 0:
70 a.Message("No " + strings.ToLower(title) + " found")
71 case len(locations) == 1:
72 a.jumpTo(locations[0])
73 default:
74 a.chooseLocation(title, locations)
75 }
76}
77
78// chooseLocation offers a list of places and goes to the one chosen.
79//
80// This exists because a single answer is the exception rather than the rule: a
81// Go interface has as many definitions as it has implementations, and until
82// this was written GoToDefinition took locations[0] and threw the rest away.
83func (a *App) chooseLocation(title string, locations []lsp.Location) {
84 dialog, list := NewChoiceDialog(fmt.Sprintf("%s (%d)", title, len(locations)), a.locationLabels(locations), 0, a.screenRect())
85 a.pushModal(dialog, func(result ui.Result) {
86 if result == ui.ResultOK && list.Selected() >= 0 {
87 a.jumpTo(locations[list.Selected()])
88 }
89 })
90}
91
92// locationLabels is how a list of places reads: the file, the line, and the
93// text of that line.
94//
95// The line's text is what makes the list usable — twelve entries reading
96// "handler.go:42" say nothing about which one anybody wants. Each file is read
97// once however many places are in it, because a symbol used forty times is
98// used forty times in a handful of files.
99func (a *App) locationLabels(locations []lsp.Location) []string {
100 lines := map[string][]string{}
101 labels := make([]string, 0, len(locations))
102
103 for _, location := range locations {
104 path := lsp.URIToPath(location.URI)
105 if _, read := lines[path]; !read {
106 lines[path] = a.linesOf(path)
107 }
108
109 line := location.Range.Start.Line
110 label := fmt.Sprintf("%s:%d", filepath.Base(path), line+1)
111 if line >= 0 && line < len(lines[path]) {
112 if text := strings.TrimSpace(lines[path][line]); text != "" {
113 label += " " + text
114 }
115 }
116 labels = append(labels, label)
117 }
118 return labels
119}
120
121// linesOf returns a file's lines, from an open window when there is one and
122// from the disk otherwise.
123//
124// The window comes first because it is the truth: a file edited and not yet
125// saved would otherwise be listed with the text it used to have, and the line
126// numbers beside it would be the server's — which follow the edits.
127//
128// A file that cannot be read gives no lines rather than an error. The list is
129// still useful without the text, and a dialog that refuses to open because one
130// of forty files moved would be worse than one with a bare line number in it.
131func (a *App) linesOf(path string) []string {
132 if window := a.windowFor(path); window != nil {
133 if view, ok := editorViewOf(window); ok {
134 return strings.Split(view.Buffer().Text(), "\n")
135 }
136 }
137 data, err := os.ReadFile(path)
138 if err != nil {
139 return nil
140 }
141 return strings.Split(string(data), "\n")
142}