@@ -235,14 +235,7 @@ defmodule NextLS do
235
235
end
236
236
237
237
def handle_request ( % WorkspaceSymbol { params: % { query: query } } , lsp ) do
238
- filter = fn sym ->
239
- if query == "" do
240
- true
241
- else
242
- # TODO: sqlite has a regexp feature, this can be done in sql most likely
243
- to_string ( sym ) =~ query
244
- end
245
- end
238
+ case_sensitive? = String . downcase ( query ) != query
246
239
247
240
symbols = fn pid ->
248
241
rows =
@@ -270,32 +263,35 @@ defmodule NextLS do
270
263
271
264
symbols =
272
265
dispatch ( lsp . assigns . registry , :databases , fn entries ->
273
- for { pid , _ } <- entries , symbol <- symbols . ( pid ) , filter . ( symbol . name ) do
274
- name =
275
- if symbol . type != "defstruct" do
276
- "#{ symbol . type } #{ symbol . name } "
277
- else
278
- "#{ symbol . name } "
279
- end
266
+ filtered_symbols =
267
+ for { pid , _ } <- entries , symbol <- symbols . ( pid ) , score = fuzzy_match ( symbol . name , query , case_sensitive? ) do
268
+ name =
269
+ if symbol . type != "defstruct" do
270
+ "#{ symbol . type } #{ symbol . name } "
271
+ else
272
+ "#{ symbol . name } "
273
+ end
274
+
275
+ { % SymbolInformation {
276
+ name: name ,
277
+ kind: elixir_kind_to_lsp_kind ( symbol . type ) ,
278
+ location: % Location {
279
+ uri: "file://#{ symbol . file } " ,
280
+ range: % Range {
281
+ start: % Position {
282
+ line: symbol . line - 1 ,
283
+ character: symbol . column - 1
284
+ } ,
285
+ end: % Position {
286
+ line: symbol . line - 1 ,
287
+ character: symbol . column - 1
288
+ }
289
+ }
290
+ }
291
+ } , score }
292
+ end
280
293
281
- % SymbolInformation {
282
- name: name ,
283
- kind: elixir_kind_to_lsp_kind ( symbol . type ) ,
284
- location: % Location {
285
- uri: "file://#{ symbol . file } " ,
286
- range: % Range {
287
- start: % Position {
288
- line: symbol . line - 1 ,
289
- character: symbol . column - 1
290
- } ,
291
- end: % Position {
292
- line: symbol . line - 1 ,
293
- character: symbol . column - 1
294
- }
295
- }
296
- }
297
- }
298
- end
294
+ filtered_symbols |> List . keysort ( 1 , :desc ) |> Enum . map ( & elem ( & 1 , 0 ) )
299
295
end )
300
296
301
297
{ :reply , symbols , lsp }
@@ -706,15 +702,14 @@ defmodule NextLS do
706
702
end
707
703
708
704
defp symbol_info ( file , line , col , database ) do
709
- definition_query =
710
- ~Q"""
711
- SELECT module, type, name
712
- FROM "symbols" sym
713
- WHERE sym.file = ?
714
- AND sym.line = ?
715
- ORDER BY sym.id ASC
716
- LIMIT 1
717
- """
705
+ definition_query = ~Q"""
706
+ SELECT module, type, name
707
+ FROM "symbols" sym
708
+ WHERE sym.file = ?
709
+ AND sym.line = ?
710
+ ORDER BY sym.id ASC
711
+ LIMIT 1
712
+ """
718
713
719
714
reference_query = ~Q"""
720
715
SELECT identifier, type, module
@@ -757,4 +752,93 @@ defmodule NextLS do
757
752
end
758
753
759
754
defp clamp ( line ) , do: max ( line , 0 )
755
+
756
+ # This is an implementation of a sequential fuzzy string matching algorithm,
757
+ # similar to those used in code editors like Sublime Text.
758
+ # It is based on Forrest Smith's work on https://github.com/forrestthewoods/lib_fts/)
759
+ # and his blog post https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/.
760
+ #
761
+ # Function checks if letters from the query present in the source in correct order.
762
+ # It calculates match score only for matching sources.
763
+
764
+ defp fuzzy_match ( _source , "" , _case_sensitive ) , do: 1
765
+
766
+ defp fuzzy_match ( source , query , case_sensitive ) do
767
+ source_converted = if case_sensitive , do: source , else: String . downcase ( source )
768
+ source_letters = String . codepoints ( source_converted )
769
+ query_letters = String . codepoints ( query )
770
+
771
+ if do_fuzzy_match? ( source_letters , query_letters ) do
772
+ source_anycase = String . codepoints ( source )
773
+ source_downcase = query |> String . downcase ( ) |> String . codepoints ( )
774
+
775
+ calc_match_score ( source_anycase , source_downcase , % { leading: true , separator: true } , 0 )
776
+ else
777
+ false
778
+ end
779
+ end
780
+
781
+ defp do_fuzzy_match? ( _source_letters , [ ] ) , do: true
782
+
783
+ defp do_fuzzy_match? ( source_letters , [ query_head | query_rest ] ) do
784
+ case match_letter ( source_letters , query_head ) do
785
+ :no_match -> false
786
+ rest_source_letters -> do_fuzzy_match? ( rest_source_letters , query_rest )
787
+ end
788
+ end
789
+
790
+ defp match_letter ( [ ] , _query_letter ) , do: :no_match
791
+
792
+ defp match_letter ( [ source_letter | source_rest ] , query_letter ) when query_letter == source_letter , do: source_rest
793
+
794
+ defp match_letter ( [ _ | source_rest ] , query_letter ) , do: match_letter ( source_rest , query_letter )
795
+
796
+ defp calc_match_score ( _source_letters , [ ] , _traits , score ) , do: score
797
+
798
+ defp calc_match_score ( source_letters , [ query_letter | query_rest ] , traits , score ) do
799
+ { rest_source_letters , new_traits , new_score } = calc_letter_score ( source_letters , query_letter , traits , score )
800
+
801
+ calc_match_score ( rest_source_letters , query_rest , new_traits , new_score )
802
+ end
803
+
804
+ defp calc_letter_score ( [ source_letter | source_rest ] , query_letter , traits , score ) do
805
+ separator? = source_letter in [ "_" , "." , "-" , "/" , " " ]
806
+ source_letter_downcase = String . downcase ( source_letter )
807
+ upper? = source_letter_downcase != source_letter
808
+
809
+ if query_letter == source_letter_downcase do
810
+ new_traits = % { matched: true , leading: false , separator: separator? , upper: upper? }
811
+ new_score = calc_matched_bonus ( score , traits , new_traits )
812
+
813
+ { source_rest , new_traits , new_score }
814
+ else
815
+ new_traits = % {
816
+ matched: false ,
817
+ separator: separator? ,
818
+ upper: upper? ,
819
+ leading: traits . leading
820
+ }
821
+
822
+ new_score = calc_unmatched_penalty ( score , traits )
823
+
824
+ calc_letter_score ( source_rest , query_letter , new_traits , new_score )
825
+ end
826
+ end
827
+
828
+ # bonus if match occurs after a separator or on the first letter
829
+ defp calc_matched_bonus ( score , % { separator: true } , _new_traits ) , do: score + 30
830
+
831
+ # bonus if match is uppercase and previous is lowercase
832
+ defp calc_matched_bonus ( score , % { upper: false } , % { upper: true } ) , do: score + 30
833
+
834
+ # bonus for adjacent matches
835
+ defp calc_matched_bonus ( score , % { matched: true } , _new_traits ) , do: score + 15
836
+
837
+ defp calc_matched_bonus ( score , _traits , _new_traits ) , do: score
838
+
839
+ # penalty applied for every letter in str before the first match
840
+ defp calc_unmatched_penalty ( score , % { leading: true } ) when score > - 15 , do: score - 5
841
+
842
+ # penalty for unmatched letter
843
+ defp calc_unmatched_penalty ( score , _traits ) , do: score - 1
760
844
end
0 commit comments