| 🛟 Updated. 28d5985 k33g 5h ago | 1 | // 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 | |
| 4 | package app |
| 5 | |
| 6 | import ( |
| 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. |
| 20 | func (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. |
| 26 | func (a *App) FindImplementations() { |
| 27 | a.askForLocations("Implementations", a.language.Implementation) |
| 28 | } |
| 29 | |
| 30 | // FindReferences lists where the thing under the cursor is used. |
| 31 | func (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. |
| 37 | type 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. |
| 45 | func (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. |
| 63 | func (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. |
| 83 | func (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. |
| 99 | func (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. |
| 131 | func (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 | } |