@@ -10,14 +10,19 @@ import Control.Monad.State (evalStateT)
10
10
import Data.Foldable qualified as Foldable
11
11
import Data.List (partition )
12
12
import Data.List qualified as List
13
+ import Data.List.NonEmpty qualified as NonEmpty
13
14
import Data.Map qualified as Map
15
+ import Data.Ord (clamp )
14
16
import Data.Sequence qualified as Seq
15
17
import Data.Set qualified as Set
18
+ import Data.Text qualified as Text
16
19
import Unison.ABT qualified as ABT
17
20
import Unison.Blank qualified as Blank
18
21
import Unison.Builtin qualified as Builtin
19
22
import Unison.ConstructorReference qualified as ConstructorReference
20
23
import Unison.Name (Name )
24
+ import Unison.Name qualified as Name
25
+ import Unison.NameSegment qualified as NameSegment
21
26
import Unison.Names qualified as Names
22
27
import Unison.Names.ResolvesTo (ResolvesTo (.. ))
23
28
import Unison.Parser.Ann (Ann )
@@ -28,7 +33,7 @@ import Unison.Referent (Referent)
28
33
import Unison.Referent qualified as Referent
29
34
import Unison.Result (CompilerBug (.. ), Note (.. ), ResultT , pattern Result )
30
35
import Unison.Result qualified as Result
31
- import Unison.Syntax.Name qualified as Name (unsafeParseVar )
36
+ import Unison.Syntax.Name qualified as Name (toText , unsafeParseText , unsafeParseVar )
32
37
import Unison.Syntax.Parser qualified as Parser
33
38
import Unison.Term qualified as Term
34
39
import Unison.Type qualified as Type
@@ -94,21 +99,50 @@ computeTypecheckingEnvironment shouldUseTndr ambientAbilities typeLookupf uf =
94
99
{ ambientAbilities = ambientAbilities,
95
100
typeLookup = tl,
96
101
termsByShortname = Map. empty,
102
+ freeNameToFuzzyTermsByShortName = Map. empty,
97
103
topLevelComponents = Map. empty
98
104
}
99
105
ShouldUseTndr'Yes parsingEnv -> do
100
- let tm = UF. typecheckingTerm uf
101
- resolveName :: Name -> Relation Name (ResolvesTo Referent )
106
+ let resolveName :: Name -> Relation Name (ResolvesTo Referent )
102
107
resolveName =
103
108
Names. resolveNameIncludingNames
104
109
(Names. shadowing1 (Names. terms (UF. toNames uf)) (Names. terms (Parser. names parsingEnv)))
105
- (Set. map Name. unsafeParseVar (UF. toTermAndWatchNames uf))
106
- possibleDeps = do
107
- v <- Set. toList (Term. freeVars tm)
108
- let shortname = Name. unsafeParseVar v
109
- (name, ref) <- Rel. toList (resolveName shortname)
110
- [(name, shortname, ref)]
111
- possibleRefs =
110
+ localNames
111
+
112
+ localNames = Set. map Name. unsafeParseVar (UF. toTermAndWatchNames uf)
113
+ globalNamesShadowed = Names. shadowing (UF. toNames uf) (Parser. names parsingEnv)
114
+
115
+ freeNames :: [Name ]
116
+ freeNames =
117
+ Name. unsafeParseVar <$> Set. toList (Term. freeVars $ UF. typecheckingTerm uf)
118
+
119
+ possibleDepsExact :: [(Name , Name , ResolvesTo Referent )]
120
+ possibleDepsExact = do
121
+ freeName <- freeNames
122
+ (name, ref) <- Rel. toList (resolveName freeName)
123
+ [(name, freeName, ref)]
124
+
125
+ getFreeNameDepsFuzzy :: Name -> [(Name , Name , ResolvesTo Referent )]
126
+ getFreeNameDepsFuzzy freeName = do
127
+ let wantedTopNFuzzyMatches = 3
128
+ -- We use fuzzy matching by edit distance here because it is usually more appropriate
129
+ -- than FZF-style fuzzy finding for offering suggestions for typos or other user errors.
130
+ let fuzzyMatches =
131
+ take wantedTopNFuzzyMatches $
132
+ fuzzyFindByEditDistanceRanked globalNamesShadowed localNames freeName
133
+
134
+ let names = fuzzyMatches ^.. each . _2
135
+ let resolvedNames = Rel. toList . resolveName =<< names
136
+ let getShortName longname = Name. unsafeParseText (NameSegment. toUnescapedText $ Name. lastSegment longname)
137
+
138
+ map (\ (longname, ref) -> (longname, getShortName longname, ref)) resolvedNames
139
+
140
+ freeNameDepsFuzzy :: Map Name [(Name , Name , ResolvesTo Referent )]
141
+ freeNameDepsFuzzy =
142
+ Map. fromList [(freeName, getFreeNameDepsFuzzy freeName) | freeName <- freeNames]
143
+
144
+ getPossibleRefs :: [(Name , Name , ResolvesTo Referent )] -> Defns (Set TermReference ) (Set TypeReference )
145
+ getPossibleRefs =
112
146
List. foldl'
113
147
( \ acc -> \ case
114
148
(_, _, ResolvesToNamespace ref0) ->
@@ -118,30 +152,106 @@ computeTypecheckingEnvironment shouldUseTndr ambientAbilities typeLookupf uf =
118
152
(_, _, ResolvesToLocal _) -> acc
119
153
)
120
154
(Defns Set. empty Set. empty)
121
- possibleDeps
122
- tl <- fmap (UF. declsToTypeLookup uf <> ) (typeLookupf (UF. dependencies uf <> possibleRefs))
123
- let termsByShortname :: Map Name [Either Name (Typechecker. NamedReference v Ann )]
124
- termsByShortname =
155
+
156
+ typeLookup <-
157
+ fmap
158
+ (UF. declsToTypeLookup uf <> )
159
+ ( typeLookupf
160
+ ( UF. dependencies uf
161
+ <> getPossibleRefs possibleDepsExact
162
+ <> getPossibleRefs (join $ Map. elems freeNameDepsFuzzy)
163
+ )
164
+ )
165
+
166
+ let getTermsByShortname :: [(Name , Name , ResolvesTo Referent )] -> Map Name [Either Name (Typechecker. NamedReference v Ann )]
167
+ getTermsByShortname =
125
168
List. foldl'
126
169
( \ acc -> \ case
127
170
(name, shortname, ResolvesToLocal _) -> let v = Left name in Map. upsert (maybe [v] (v : )) shortname acc
128
171
(name, shortname, ResolvesToNamespace ref) ->
129
- case TL. typeOfReferent tl ref of
172
+ case TL. typeOfReferent typeLookup ref of
130
173
Just ty ->
131
174
let v = Right (Typechecker. NamedReference name ty (Context. ReplacementRef ref))
132
175
in Map. upsert (maybe [v] (v : )) shortname acc
133
176
Nothing -> acc
134
177
)
135
178
Map. empty
136
- possibleDeps
179
+
180
+ let termsByShortname = getTermsByShortname possibleDepsExact
181
+ let freeNameToFuzzyTermsByShortName = Map. mapWithKey (\ _ v -> getTermsByShortname v) freeNameDepsFuzzy
182
+
137
183
pure
138
184
Typechecker. Env
139
185
{ ambientAbilities,
140
- typeLookup = tl ,
186
+ typeLookup = typeLookup ,
141
187
termsByShortname,
188
+ freeNameToFuzzyTermsByShortName,
142
189
topLevelComponents = Map. empty
143
190
}
144
191
192
+ -- | 'fuzzyFindByEditDistanceRanked' finds matches for the given 'name' within 'names' by edit distance.
193
+ --
194
+ -- Returns a list of 3-tuples composed of an edit-distance Score, a Name, and a List of term and type references).
195
+ --
196
+ -- Adapted from Unison.Server.Backend.fuzzyFind
197
+ --
198
+ -- TODO: Consider moving to Unison.Names
199
+ --
200
+ -- TODO: Take type similarity into account when ranking matches
201
+ fuzzyFindByEditDistanceRanked ::
202
+ Names. Names ->
203
+ Set Name ->
204
+ Name ->
205
+ [(Int , Name )]
206
+ fuzzyFindByEditDistanceRanked globalNames localNames name =
207
+ let query =
208
+ (Text. unpack . nameToText) name
209
+
210
+ -- Use 'nameToTextFromLastNSegments' so edit distance is not biased towards shorter fully-qualified names
211
+ -- and the name being queried is only partially qualified.
212
+ fzfGlobalNames =
213
+ Names. queryEditDistances nameToTextFromLastNSegments query globalNames
214
+ fzfLocalNames =
215
+ Names. queryEditDistances' nameToTextFromLastNSegments query localNames
216
+ fzfNames = fzfGlobalNames ++ fzfLocalNames
217
+
218
+ -- Keep only matches with a sufficiently low edit-distance score
219
+ filterByScore = filter (\ (score, _, _) -> score < maxScore)
220
+
221
+ -- Prefer lower edit distances and then prefer shorter names by segment count
222
+ rank (score, name, _) = (score, length $ Name. segments name)
223
+
224
+ -- Remove dupes based on refs
225
+ dedupe =
226
+ List. nubOrdOn (\ (_, _, refs) -> refs)
227
+
228
+ dropRef = map (\ (x, y, _) -> (x, y))
229
+
230
+ refine =
231
+ dropRef . dedupe . sortOn rank . filterByScore
232
+ in refine fzfNames
233
+ where
234
+ nNameSegments = max 1 $ NonEmpty. length $ Name. segments name
235
+
236
+ takeLast :: Int -> NonEmpty. NonEmpty a -> [a ]
237
+ takeLast n xs = NonEmpty. drop (NonEmpty. length xs - n) xs
238
+ nameFromLastNSegments =
239
+ Name. fromSegments
240
+ . NonEmpty. fromList
241
+ . takeLast nNameSegments
242
+ . Name. segments
243
+
244
+ -- Convert to lowercase for case-insensitive fuzzy matching
245
+ nameToText = Text. toLower . Name. toText
246
+ nameToTextFromLastNSegments = nameToText . nameFromLastNSegments
247
+
248
+ ceilingDiv :: Int -> Int -> Int
249
+ ceilingDiv x y = (x + 1 ) `div` y
250
+ -- Expect edit distances (number of typos) to be about half the length of the name being queried
251
+ -- But clamp max edit distance to work well with very short names
252
+ -- and keep ranking reasonably fast when a verbose name is queried
253
+ maxScore = clamp (3 , 16 ) $ Text. length (nameToText name) `ceilingDiv` 2
254
+
145
255
synthesizeFile ::
146
256
forall m v .
147
257
(Monad m , Var v ) =>
0 commit comments