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
|
// Finding a symbol by name: in the file in front, or anywhere in the project.
package app
import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"codeberg.org/turbo-editors/turbo-core/lsp"
"codeberg.org/turbo-editors/turbo-core/ui"
)
// SymbolInFile lists what the file in front declares, and goes to the one
// chosen. It is the outline of a file, offered as a way of moving around it.
func (a *App) SymbolInFile() {
view := a.activeView()
if view == nil {
return
}
symbols, err := a.language.DocumentSymbols(context.Background(), view.Buffer().Path())
a.showSymbols("Symbols in file", symbols, err, outlineLabel)
}
// SymbolInProject asks for a name and lists the symbols matching it anywhere
// in the project.
//
// The query goes to the server, which decides what "matching" means — gopls
// matches fuzzily, others by prefix. Deciding that here would mean filtering
// an answer the server has already filtered, by a different rule.
func (a *App) SymbolInProject() {
dialog, field := NewPromptDialog("Symbol in project", "Name:", "", a.screenRect())
a.pushModal(dialog, func(result ui.Result) {
if result != ui.ResultOK {
return
}
query := strings.TrimSpace(field.Text())
if query == "" {
// An empty query is answered by some servers with the whole
// project and by others with nothing. Neither is what was meant.
a.Message("Nothing to look for")
return
}
symbols, err := a.language.WorkspaceSymbols(context.Background(), query)
a.showSymbols("Symbols matching "+query, symbols, err, projectLabel)
})
}
// showSymbols offers a list of symbols and goes to the one chosen.
//
// Unlike a list of locations, a single symbol still gets the list. Finding one
// match for a name is an answer worth reading — it says *which* thing has that
// name and where — where a single definition is somewhere you simply wanted to
// be taken.
func (a *App) showSymbols(title string, symbols []lsp.Symbol, err error, label func(lsp.Symbol) string) {
switch {
case errors.Is(err, lsp.ErrNotReady):
a.Message(a.language.Status())
return
case err != nil:
a.Message("Symbols: " + err.Error())
return
case len(symbols) == 0:
a.Message("No symbols found")
return
}
labels := make([]string, 0, len(symbols))
for _, symbol := range symbols {
labels = append(labels, label(symbol))
}
dialog, list := NewChoiceDialog(fmt.Sprintf("%s (%d)", title, len(symbols)), labels, 0, a.screenRect())
a.pushModal(dialog, func(result ui.Result) {
if result == ui.ResultOK && list.Selected() >= 0 {
a.jumpTo(symbols[list.Selected()].Location)
}
})
}
// outlineLabel is how a symbol reads in a file's own outline: indented by how
// deeply it is nested, so a type's methods sit under it, with the kind after
// the name rather than before it so the names line up and can be read down.
func outlineLabel(symbol lsp.Symbol) string {
label := strings.Repeat(" ", symbol.Depth) + symbol.Name
if kind := symbol.Kind.String(); kind != "" {
label += " " + kind
}
return label
}
// projectLabel is how a symbol reads in a search across the project: the name,
// what contains it, and which file — because the file is what tells two
// identically-named symbols apart, and there are usually several.
func projectLabel(symbol lsp.Symbol) string {
label := symbol.Name
if kind := symbol.Kind.String(); kind != "" {
label += " " + kind
}
if symbol.Container != "" {
label += " in " + symbol.Container
}
if path := lsp.URIToPath(symbol.Location.URI); path != "" {
label += " (" + filepath.Base(path) + ")"
}
return label
}
|