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.

🛟 Updated. 28d5985 · on main · k33g · 4h ago
code.go · 142 lines · 5.1 KBGo Blame HistoryRaw
  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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// The Code menu: everything the editor asks the language server about the
// symbol under the cursor, and what it does with the answers.

package app

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"codeberg.org/turbo-editors/turbo-core/lsp"
	"codeberg.org/turbo-editors/turbo-core/ui"
)

// GoToTypeDefinition opens the declaration of the *type* of the thing under
// the cursor, which is a different question from where the thing itself is.
func (a *App) GoToTypeDefinition() {
	a.askForLocations("Type definition", a.language.TypeDefinition)
}

// FindImplementations lists what implements the thing under the cursor: the
// types satisfying an interface, the impl blocks of a trait.
func (a *App) FindImplementations() {
	a.askForLocations("Implementations", a.language.Implementation)
}

// FindReferences lists where the thing under the cursor is used.
func (a *App) FindReferences() {
	a.askForLocations("References", a.language.References)
}

// locationQuestion is any of the four requests that answer with places in the
// code. They differ only in their name, so they are asked the same way.
type locationQuestion func(ctx context.Context, path string, line, column int, lineText string) ([]lsp.Location, error)

// askForLocations puts one question to the server and shows the answer: it
// jumps straight to a single result, offers a list for several, and says so
// for none.
//
// The title doubles as the word used in the messages, so a question that
// finds nothing says "No references found" rather than something generic.
func (a *App) askForLocations(title string, ask locationQuestion) {
	view := a.activeView()
	if view == nil {
		return
	}
	buf := view.Buffer()
	cursor := buf.Cursor()

	locations, err := ask(context.Background(), buf.Path(), cursor.Line, cursor.Col, buf.Line(cursor.Line))
	a.showLocations(title, locations, err)
}

// showLocations is what every location question does with its answer.
//
// The three outcomes are deliberately distinct. A server that has not finished
// loading says so, because "nothing found" and "I cannot answer yet" look
// identical to a user and only one of them is worth waiting out — it is the
// most confusing way completion fails, and this would have inherited it.
func (a *App) showLocations(title string, locations []lsp.Location, err error) {
	switch {
	case errors.Is(err, lsp.ErrNotReady):
		a.Message(a.language.Status())
	case err != nil:
		a.Message(strings.ToLower(title) + ": " + err.Error())
	case len(locations) == 0:
		a.Message("No " + strings.ToLower(title) + " found")
	case len(locations) == 1:
		a.jumpTo(locations[0])
	default:
		a.chooseLocation(title, locations)
	}
}

// chooseLocation offers a list of places and goes to the one chosen.
//
// This exists because a single answer is the exception rather than the rule: a
// Go interface has as many definitions as it has implementations, and until
// this was written GoToDefinition took locations[0] and threw the rest away.
func (a *App) chooseLocation(title string, locations []lsp.Location) {
	dialog, list := NewChoiceDialog(fmt.Sprintf("%s (%d)", title, len(locations)), a.locationLabels(locations), 0, a.screenRect())
	a.pushModal(dialog, func(result ui.Result) {
		if result == ui.ResultOK && list.Selected() >= 0 {
			a.jumpTo(locations[list.Selected()])
		}
	})
}

// locationLabels is how a list of places reads: the file, the line, and the
// text of that line.
//
// The line's text is what makes the list usable — twelve entries reading
// "handler.go:42" say nothing about which one anybody wants. Each file is read
// once however many places are in it, because a symbol used forty times is
// used forty times in a handful of files.
func (a *App) locationLabels(locations []lsp.Location) []string {
	lines := map[string][]string{}
	labels := make([]string, 0, len(locations))

	for _, location := range locations {
		path := lsp.URIToPath(location.URI)
		if _, read := lines[path]; !read {
			lines[path] = a.linesOf(path)
		}

		line := location.Range.Start.Line
		label := fmt.Sprintf("%s:%d", filepath.Base(path), line+1)
		if line >= 0 && line < len(lines[path]) {
			if text := strings.TrimSpace(lines[path][line]); text != "" {
				label += "  " + text
			}
		}
		labels = append(labels, label)
	}
	return labels
}

// linesOf returns a file's lines, from an open window when there is one and
// from the disk otherwise.
//
// The window comes first because it is the truth: a file edited and not yet
// saved would otherwise be listed with the text it used to have, and the line
// numbers beside it would be the server's — which follow the edits.
//
// A file that cannot be read gives no lines rather than an error. The list is
// still useful without the text, and a dialog that refuses to open because one
// of forty files moved would be worse than one with a bare line number in it.
func (a *App) linesOf(path string) []string {
	if window := a.windowFor(path); window != nil {
		if view, ok := editorViewOf(window); ok {
			return strings.Split(view.Buffer().Text(), "\n")
		}
	}
	data, err := os.ReadFile(path)
	if err != nil {
		return nil
	}
	return strings.Split(string(data), "\n")
}