@@ -55,6 +55,29 @@ EMPTY_QUERY = ".*"
5555
5656local log = hs .logger .new (' seal_filesearch' , ' info' )
5757
58+ ---
59+ -- Generic helper functions
60+ ---
61+
62+ local imapWithIndex = function (t , mapFn )
63+ local mappedTable = {}
64+ for i , v in ipairs (t ) do
65+ mappedTable [# mappedTable + 1 ] = mapFn (v , i )
66+ end
67+ return mappedTable
68+ end
69+
70+ local containsWholeWord = function (s , word )
71+ local pattern = " %f[%w]" .. string.lower (word ) .. " %f[%W]"
72+ return string.find (string.lower (s ), pattern )
73+ end
74+
75+ local containsCamelCaseWholeWord = function (s , word )
76+ local camelCaseWord = word :sub (1 , 1 ):upper () .. word :sub (2 ):lower ()
77+ local pattern = " %f[%u]" .. camelCaseWord .. " (%f[%u%z])"
78+ return string.find (s , pattern )
79+ end
80+
5881---
5982-- Spotlight Helper class
6083---
151174-- Private functions
152175---
153176
154- local convertSpotlightResultToQueryResult = function (item )
177+ local convertSpotlightResultToQueryResult = function (item , index )
155178 local icon = hs .image .iconForFile (item .kMDItemPath )
156179 local bundleID = item .kMDItemCFBundleIdentifier
157180 if (not icon ) and (bundleID ) then
@@ -164,13 +187,43 @@ local convertSpotlightResultToQueryResult = function(item)
164187 uuid = obj .__name .. " __" .. (bundleID or item .kMDItemDisplayName ),
165188 plugin = obj .__name ,
166189 type = " open" ,
167- image = icon
190+ image = icon ,
191+ index = index
168192 }
169193end
170194
195+ -- This function returns a sort function suitable for table.sort
196+ -- that will boost the search results that:
197+ -- * contains the whole word (not part of another word)
198+ -- * contains the word in CamelCase
199+ local buildBoostResultsSortForQuery = function (query )
200+ local queryWords = hs .fnutils .split (query , " %s+" )
201+ local scoreItem = function (item )
202+ local score = 0
203+ for _ , word in ipairs (queryWords ) do
204+ if containsWholeWord (item .text , word ) or containsCamelCaseWholeWord (item .text , word ) then
205+ score = score + 1
206+ end
207+ end
208+ return score
209+ end
210+
211+ return function (itemA , itemB )
212+ local scoreA = scoreItem (itemA )
213+ local scoreB = scoreItem (itemB )
214+ if scoreA ~= scoreB then
215+ return scoreA > scoreB
216+ else
217+ -- we just preserve the existing order otherwise
218+ return itemA .index < itemB .index
219+ end
220+ end
221+ end
222+
171223local handleFileSearchResults = function (query , searchResults )
172224 if query == obj .currentQuery then
173- obj .currentQueryResults = hs .fnutils .map (searchResults , convertSpotlightResultToQueryResult )
225+ obj .currentQueryResults = imapWithIndex (searchResults , convertSpotlightResultToQueryResult )
226+ table.sort (obj .currentQueryResults , buildBoostResultsSortForQuery (query ))
174227 obj .seal .chooser :refreshChoicesCallback ()
175228 end
176229end
0 commit comments