| 1 | module util |
| 2 | |
| 3 | import term |
| 4 | import 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. |
| 8 | struct Possibility { |
| 9 | value string |
| 10 | svalue string |
| 11 | mut: |
| 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 |
| 16 | pub 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. |
| 21 | struct Suggestion { |
| 22 | mut: |
| 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] |
| 32 | pub struct SuggestionParams { |
| 33 | pub 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. |
| 39 | pub 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 | |
| 52 | const 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. |
| 56 | pub 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 |
| 77 | pub 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. |
| 88 | pub 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. |
| 94 | pub 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` |
| 124 | pub 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. |
| 140 | pub fn highlight_suggestion(message string) string { |
| 141 | return term.ecolorize(term.bright_blue, message) |
| 142 | } |
| 143 | |