// Finding a symbol by name: in the file in front, or anywhere in the project. package app import ( "context" "errors" "fmt" "path/filepath" "strings" "rickub.com/turbo-editors/turbo-core/lsp" "rickub.com/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 }