clerk/internal/lsp/workspace_symbol.go (view raw)
Oleksandr Smirnov
Oleksandr Smirnov
olexsmir@gmail.com lsp: workspace/symbol and textdocument/references, 1 month ago
olexsmir@gmail.com lsp: workspace/symbol and textdocument/references, 1 month ago
| 1 | package lsp |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "sort" |
| 6 | |
| 7 | "go.lsp.dev/protocol" |
| 8 | "go.lsp.dev/uri" |
| 9 | |
| 10 | "olexsmir.xyz/clerk/internal/analyzer" |
| 11 | "olexsmir.xyz/clerk/internal/lsp/fuzzy" |
| 12 | ) |
| 13 | |
| 14 | func (s *server) Symbols(_ context.Context, params *protocol.WorkspaceSymbolParams) (protocol.WorkspaceSymbolResult, error) { |
| 15 | if params.Query == "" { |
| 16 | return nil, nil |
| 17 | } |
| 18 | |
| 19 | s.mu.Lock() |
| 20 | var u uri.URI |
| 21 | for u = range s.openDocs { |
| 22 | break |
| 23 | } |
| 24 | s.mu.Unlock() |
| 25 | an := s.analysisFor(u) |
| 26 | if an == nil { |
| 27 | return nil, nil |
| 28 | } |
| 29 | |
| 30 | symbols := searchSymbols(an, params.Query) |
| 31 | if len(symbols) == 0 { |
| 32 | return nil, nil |
| 33 | } |
| 34 | return protocol.WorkspaceSymbolSlice(symbols), nil |
| 35 | } |
| 36 | |
| 37 | const maxSymbolResults = 100 |
| 38 | |
| 39 | type scoredSymbol struct { |
| 40 | kind symbolKind |
| 41 | name string |
| 42 | score float64 |
| 43 | } |
| 44 | |
| 45 | func searchSymbols(an *analyzer.Analysis, query string) []protocol.WorkspaceSymbol { |
| 46 | matcher := fuzzy.Compile(query) |
| 47 | |
| 48 | scored := make([]scoredSymbol, 0, len(an.AccountNames)+len(an.Commodities)+len(an.PayeeNames)+len(an.TagNames)) |
| 49 | add := func(kind symbolKind, name string) { |
| 50 | if score := matcher.Score(name); score > 0 { |
| 51 | scored = append(scored, scoredSymbol{kind, name, score}) |
| 52 | } |
| 53 | } |
| 54 | for _, name := range an.AccountNames { |
| 55 | add(symbolAccount, name) |
| 56 | } |
| 57 | for name := range an.Commodities { |
| 58 | add(symbolCommodity, name) |
| 59 | } |
| 60 | for _, name := range an.PayeeNames { |
| 61 | add(symbolPayee, name) |
| 62 | } |
| 63 | for _, name := range an.TagNames { |
| 64 | add(symbolTag, name) |
| 65 | } |
| 66 | |
| 67 | sortScoredSymbols(scored) |
| 68 | if len(scored) > maxSymbolResults { |
| 69 | scored = scored[:maxSymbolResults] |
| 70 | } |
| 71 | |
| 72 | symbols := make([]protocol.WorkspaceSymbol, 0, len(scored)) |
| 73 | for _, s := range scored { |
| 74 | loc := definitionLocation(an, s.kind, s.name) |
| 75 | if loc == nil { |
| 76 | continue |
| 77 | } |
| 78 | symbols = append(symbols, protocol.WorkspaceSymbol{ |
| 79 | BaseSymbolInformation: protocol.BaseSymbolInformation{ |
| 80 | Name: s.name, |
| 81 | Kind: s.kind.ToProtocol(), |
| 82 | }, |
| 83 | Location: loc, |
| 84 | }) |
| 85 | } |
| 86 | return symbols |
| 87 | } |
| 88 | |
| 89 | func definitionLocation(an *analyzer.Analysis, kind symbolKind, name string) *protocol.Location { |
| 90 | switch kind { |
| 91 | case symbolAccount: |
| 92 | return findAccountDefinition(an, name) |
| 93 | case symbolCommodity: |
| 94 | return findCommodityDefinition(an, name) |
| 95 | case symbolPayee: |
| 96 | return findPayeeDefinition(an, name) |
| 97 | case symbolTag: |
| 98 | return findTagDefinition(an, name) |
| 99 | } |
| 100 | return nil |
| 101 | } |
| 102 | |
| 103 | func sortScoredSymbols(scored []scoredSymbol) { |
| 104 | sort.Slice(scored, func(i, j int) bool { |
| 105 | if scored[i].score != scored[j].score { |
| 106 | return scored[i].score > scored[j].score |
| 107 | } |
| 108 | return scored[i].name < scored[j].name |
| 109 | }) |
| 110 | } |