v2 / vlib / v / util / suggestions.v
142 lines · 129 sloc · 4.46 KB · 97de6cda7667c1aba8a5574bd2d4aee5e604918d
Raw
1module util
2
3import term
4import strings
5
6// Possibility is a simple pair of a string, with a similarity coefficient
7// determined by the editing distance to a wanted value.
8struct Possibility {
9 value string
10 svalue string
11mut:
12 similarity f32 // 0.0 .. 1.0; 0.0 means the strings have nothing in common, and 1.0 means exactly equal strings
13}
14
15// CalculateSuggestionSimilarityFN is the type of the similarity comparison function, that will be used to determine what suggestions are best
16pub type CalculateSuggestionSimilarityFN = fn (s1 string, s2 string) f32
17
18// Suggestion is set of known possibilities and a wanted string.
19// It has helper methods for making educated guesses based on the possibilities,
20// on which of them match best the wanted string.
21struct Suggestion {
22mut:
23 known []Possibility
24 wanted string
25 swanted string
26 similarity_threshold f32
27 similarity_fn CalculateSuggestionSimilarityFN = strings.dice_coefficient
28}
29
30// SuggestionParams contains the defaults for the optional parameters of new_suggestion.
31@[params]
32pub struct SuggestionParams {
33pub mut:
34 similarity_threshold f32 = 0.5 // only items for which the similarity is above similarity_threshold, will be shown
35 similarity_fn CalculateSuggestionSimilarityFN = strings.dice_coefficient // see also strings.hamming_similarity
36}
37
38// new_suggestion creates a new Suggestion, given a wanted value and a list of possibilities.
39pub fn new_suggestion(wanted string, possibilities []string, params SuggestionParams) Suggestion {
40 mut s := Suggestion{
41 known: []Possibility{cap: int(max_suggestions_limit)}
42 wanted: wanted
43 swanted: short_module_name(wanted)
44 similarity_threshold: params.similarity_threshold
45 similarity_fn: params.similarity_fn
46 }
47 s.add_many(possibilities)
48 s.sort()
49 return s
50}
51
52const max_suggestions_limit = $d('max_suggestions_limit', 200)
53
54// add adds the `val` to the list of known possibilities of the suggestion.
55// It calculates the similarity metric towards the wanted value.
56pub fn (mut s Suggestion) add(val string) {
57 if s.known.len >= max_suggestions_limit {
58 return
59 }
60 if val in [s.wanted, s.swanted] {
61 return
62 }
63 sval := short_module_name(val)
64 if sval in [s.wanted, s.swanted] {
65 return
66 }
67 // round to 3 decimal places to avoid float comparison issues
68 similarity := f32(int(s.similarity_fn(s.swanted, sval) * 1000)) / 1000
69 s.known << Possibility{
70 value: val
71 svalue: sval
72 similarity: similarity
73 }
74}
75
76// add adds all of the `many` to the list of known possibilities of the suggestion
77pub fn (mut s Suggestion) add_many(many []string) {
78 for x in many {
79 if s.known.len >= max_suggestions_limit {
80 break
81 }
82 s.add(x)
83 }
84}
85
86// sort sorts the list of known possibilities, based on their similarity metric.
87// Equal strings will be first, followed by less similar ones, very distinct ones will be last.
88pub fn (mut s Suggestion) sort() {
89 s.known.sort(a.similarity < b.similarity)
90}
91
92// say produces a final suggestion message, based on the preset `wanted` and
93// `possibilities` fields, accumulated in the Suggestion.
94pub fn (s Suggestion) say(msg string) string {
95 mut res := msg
96 mut found := false
97 if s.known.len > 0 {
98 top_possibility := s.known.last()
99 if top_possibility.similarity > s.similarity_threshold {
100 val := top_possibility.value
101 if !val.starts_with('[]') {
102 res += '.\nDid you mean `${highlight_suggestion(val)}`?'
103 found = true
104 }
105 }
106 }
107 if !found {
108 if s.known.len > 0 {
109 mut values := s.known.map('`${highlight_suggestion(it.svalue)}`')
110 values.sort()
111 if values.len == 1 {
112 res += '.\n1 possibility: ${values[0]}.'
113 } else if values.len < 25 {
114 // it is hard to read/use too many suggestions
115 res += '.\n${values.len} possibilities: ' + values.join(', ') + '.'
116 }
117 }
118 }
119 return res
120}
121
122// short_module_name returns a shortened version of the fully qualified `name`,
123// i.e. `xyz.def.abc.symname` -> `abc.symname`
124pub fn short_module_name(name string) string {
125 if !name.contains('.') {
126 return name
127 }
128 vals := name.split('.')
129 if vals.len < 2 {
130 return name
131 }
132 mname := vals[vals.len - 2]
133 symname := vals.last()
134 return '${mname}.${symname}'
135}
136
137// highlight_suggestion returns a colorfull/highlighted version of `message`,
138// but only if the standard error output allows for color messages, otherwise
139// the plain message will be returned.
140pub fn highlight_suggestion(message string) string {
141 return term.ecolorize(term.bright_blue, message)
142}
143