-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfind.go
More file actions
181 lines (168 loc) · 5.35 KB
/
Copy pathfind.go
File metadata and controls
181 lines (168 loc) · 5.35 KB
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package repomap
import (
"sort"
"strings"
)
// SymbolMatch is a single hit from FindSymbol. Results are sorted by Score
// descending; use the File+Symbol.Line pair for a stable identifier.
type SymbolMatch struct {
File string // path relative to root
Symbol Symbol // the matching symbol
Handle string // stable symbol handle; accepted by Context
FileHandle string // stable file handle for the owning file
Score float64 // relevance score: 100=exact, 75=exact-CI, 50=prefix, 25=contains
DetailLevel int // copied from the owning RankedFile (for budget-aware callers)
}
// ParseFindQuery splits a positional query of the form
//
// [kind:][file:<path>:]<name>
//
// into (name, kind, file). Qualifier prefixes may appear in either order; the
// final token is always the name. Empty input returns all empties.
func ParseFindQuery(q string) (name, kind, file string) {
q = strings.TrimSpace(q)
if q == "" {
return "", "", ""
}
parts := strings.Split(q, ":")
// Walk left-to-right consuming qualifier prefixes. Remaining tokens rejoin
// as the name (so a name with a literal `:` still works if no qualifiers).
for len(parts) > 1 {
switch parts[0] {
case "kind":
kind = parts[1]
parts = parts[2:]
case "file":
file = parts[1]
parts = parts[2:]
default:
// No more qualifiers — everything left is the name.
name = strings.Join(parts, ":")
return name, kind, file
}
}
if len(parts) == 1 {
name = parts[0]
}
return name, kind, file
}
// FindSymbol searches the ranked symbol set for matches.
//
// name: required (empty → empty result). A name with the "symbol:" prefix is
// treated as a stable handle produced by SymbolHandle and resolved to the
// single exact match (score 100), bypassing the fuzzy ranking below.
// Otherwise the plain name is matched in priority order:
// exact (100) > case-insensitive exact (75) > prefix (50) > contains (25).
// kind: optional filter; "" matches any. Matched case-insensitively against Symbol.Kind.
// file: optional substring filter against RankedFile.Path; "" matches any.
//
// Results are sorted by Score desc, then the owning RankedFile.Score desc
// (tiebreaker), then File asc (stable tiebreaker). Safe for concurrent use.
func (m *Map) FindSymbol(name, kind, file string) []SymbolMatch {
if handleFile, handleName, handleKind, handleLine, ok := ParseSymbolHandle(name); ok {
return m.FindSymbolHandle(handleFile, handleName, handleKind, handleLine)
}
out := []SymbolMatch{}
if name == "" {
return out
}
m.mu.RLock()
// Incremental rebuilds compact m.ranked and re-rank shared FileSymbols.
// Clone the complete ranked state before releasing the lock so this search
// can safely finish against one immutable snapshot.
ranked := cloneRanked(m.ranked)
m.mu.RUnlock()
nameLower := strings.ToLower(name)
kindLower := strings.ToLower(kind)
type scored struct {
match SymbolMatch
fileScore int
}
var hits []scored
for i := range ranked {
rf := &ranked[i]
if rf.FileSymbols == nil {
continue
}
if file != "" && !strings.Contains(rf.Path, file) {
continue
}
for _, sym := range rf.Symbols {
if kindLower != "" && strings.ToLower(sym.Kind) != kindLower {
continue
}
score := scoreSymbolMatch(sym.Name, name, nameLower)
if score == 0 {
continue
}
hits = append(hits, scored{
match: makeSymbolMatch(rf, sym, score),
fileScore: rf.Score,
})
}
}
sort.SliceStable(hits, func(i, j int) bool {
if hits[i].match.Score != hits[j].match.Score {
return hits[i].match.Score > hits[j].match.Score
}
if hits[i].fileScore != hits[j].fileScore {
return hits[i].fileScore > hits[j].fileScore
}
return hits[i].match.File < hits[j].match.File
})
out = make([]SymbolMatch, len(hits))
for i, h := range hits {
out[i] = h.match
}
return out
}
// FindSymbolHandle resolves an exact symbol handle emitted by SymbolHandle.
func (m *Map) FindSymbolHandle(file, name, kind string, line int) []SymbolMatch {
out := []SymbolMatch{}
if file == "" || name == "" || kind == "" || line <= 0 {
return out
}
m.mu.RLock()
// See FindSymbol: the snapshot must not share incremental ranking state.
ranked := cloneRanked(m.ranked)
m.mu.RUnlock()
for i := range ranked {
rf := &ranked[i]
if rf.FileSymbols == nil || rf.Path != file {
continue
}
for _, sym := range rf.Symbols {
if sym.Name == name && sym.Kind == kind && sym.Line == line {
return []SymbolMatch{makeSymbolMatch(rf, sym, 100)}
}
}
}
return out
}
// makeSymbolMatch builds a SymbolMatch, deriving the stable file/symbol handles
// in one place so FindSymbol and FindSymbolHandle cannot drift on the format.
func makeSymbolMatch(rf *RankedFile, sym Symbol, score float64) SymbolMatch {
return SymbolMatch{
File: rf.Path,
Symbol: sym,
Handle: SymbolHandle(rf.Path, sym),
FileHandle: FileHandle(rf.Path),
Score: score,
DetailLevel: rf.DetailLevel,
}
}
// scoreSymbolMatch returns the relevance score, or 0 for no match.
// Caller passes nameLower pre-computed to avoid per-symbol allocation.
func scoreSymbolMatch(symName, name, nameLower string) float64 {
switch {
case symName == name:
return 100
case strings.EqualFold(symName, name):
return 75
case strings.HasPrefix(symName, name):
return 50
case strings.Contains(strings.ToLower(symName), nameLower):
return 25
}
return 0
}