Skip to content

Fuzzy matching #121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions lib/elixir_sense/providers/suggestion/complete.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
alias ElixirSense.Core.Struct
alias ElixirSense.Core.TypeInfo

alias ElixirSense.Providers.Suggestion.Matcher

@erlang_module_builtin_functions [{:module_info, 0}, {:module_info, 1}]
@elixir_module_builtin_functions [{:__info__, 1}]
@builtin_functions @erlang_module_builtin_functions ++ @elixir_module_builtin_functions
Expand Down Expand Up @@ -467,7 +469,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
defp match_aliases(hint, env) do
for {alias, _mod} <- env.aliases,
[name] = Module.split(alias),
starts_with?(name, hint) do
String.starts_with?(name, hint) do
%{kind: :module, type: :alias, name: name, desc: {"", %{}}, subtype: nil}
end
end
Expand Down Expand Up @@ -528,8 +530,8 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
|> get_modules(env)
|> Enum.sort()
|> Enum.dedup()
|> Enum.drop_while(&(not starts_with?(&1, hint)))
|> Enum.take_while(&starts_with?(&1, hint))
|> Enum.drop_while(&(not String.starts_with?(&1, hint)))
|> Enum.take_while(&String.starts_with?(&1, hint))
end

defp get_modules(true, env) do
Expand Down Expand Up @@ -591,7 +593,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do

for {fun, arities, def_arities, func_kind, docs, specs, args} <- list,
name = Atom.to_string(fun),
starts_with?(name, hint) do
Matcher.match?(name, hint) do
%{
kind: :function,
name: name,
Expand Down Expand Up @@ -758,9 +760,6 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
defp ensure_loaded(Elixir), do: {:error, :nofile}
defp ensure_loaded(mod), do: Code.ensure_compiled(mod)

defp starts_with?(_string, ""), do: true
defp starts_with?(string, hint), do: String.starts_with?(string, hint)

defp match_map_fields(fields, hint, type) do
for {key, value} when is_atom(key) <- fields,
key = Atom.to_string(key),
Expand Down
72 changes: 72 additions & 0 deletions lib/elixir_sense/providers/suggestion/matcher.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
defmodule ElixirSense.Providers.Suggestion.Matcher do
@moduledoc """
## Suggestion Matching
"""

import Kernel, except: [match?: 2]

@doc """
Naive sequential fuzzy matching without weight and requiring first char to match.

## Examples

iex> ElixirSense.Providers.Suggestion.Matcher.match?("map", "map")
true

iex> ElixirSense.Providers.Suggestion.Matcher.match?("map", "m")
true

iex> ElixirSense.Providers.Suggestion.Matcher.match?("map", "ma")
true

iex> ElixirSense.Providers.Suggestion.Matcher.match?("map", "mp")
true

iex> ElixirSense.Providers.Suggestion.Matcher.match?("map", "")
true

iex> ElixirSense.Providers.Suggestion.Matcher.match?("map", "ap")
false

iex> ElixirSense.Providers.Suggestion.Matcher.match?("", "")
true

iex> ElixirSense.Providers.Suggestion.Matcher.match?("chunk_by", "chub")
true

iex> ElixirSense.Providers.Suggestion.Matcher.match?("chunk_by", "chug")
false
"""
@spec match?(name :: String.t(), hint :: String.t()) :: boolean()
def match?(<<name_head::utf8, _name_rest::binary>>, <<hint_head::utf8, _hint_rest::binary>>)
when name_head != hint_head do
false
end

def match?(name, hint) do
do_match?(name, hint)
end

defp do_match?(<<head::utf8, name_rest::binary>>, <<head::utf8, hint_rest::binary>>) do
do_match?(name_rest, hint_rest)
end

defp do_match?(
<<_head::utf8, name_rest::binary>>,
<<_not_head::utf8, _hint_rest::binary>> = hint
) do
do_match?(name_rest, hint)
end

defp do_match?(_name_rest, <<>>) do
true
end

defp do_match?(<<>>, <<>>) do
true
end

defp do_match?(<<>>, _) do
false
end
end
27 changes: 14 additions & 13 deletions test/elixir_sense/providers/suggestion/complete_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,7 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do
end

test "complete build in functions on non local calls" do
assert [] = expand('mo')
assert [] = expand('module_')
assert [] = expand('__in')

assert [] = expand('Elixir.mo')
Expand Down Expand Up @@ -1080,7 +1080,7 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do
spec:
"@spec module_info(:module) :: atom\n@spec module_info(:attributes | :compile) :: [{atom, term}]\n@spec module_info(:md5) :: binary\n@spec module_info(:exports | :functions | :nifs) :: [{atom, non_neg_integer}]\n@spec module_info(:native) :: boolean"
}
] = expand(':ets.mo')
] = expand(':ets.module_')

assert [] = expand(':ets.__in')

Expand All @@ -1097,7 +1097,7 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do
}
}

assert [] = expand('mo', env)
assert [] = expand('module_', env)
assert [] = expand('__in', env)

assert [
Expand Down Expand Up @@ -1243,6 +1243,7 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do
@tag requires_otp_23: true
test "complete build in :erlang functions" do
assert [
%{arity: 2, name: "open_port", origin: ":erlang"},
%{
arity: 2,
name: "or",
Expand Down Expand Up @@ -1281,22 +1282,22 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do
spec: "",
summary: "",
type: :function
}
},
%{arity: 2, name: "append", origin: ":erlang"},
%{arity: 2, name: "append_element", origin: ":erlang"}
] = expand(':erlang.and')
end

test "profide specs for erlang functions" do
test "provide specs for erlang functions" do
assert [
%{
arity: 0,
name: "date",
spec: "@spec date :: date when date: :calendar.date",
type: :function,
args: "",
arity: 1,
name: "whereis",
origin: ":erlang",
summary: ""
spec: "@spec whereis(regName) :: pid | port | :undefined when regName: atom",
type: :function
}
] = expand(':erlang.dat')
] = expand(':erlang.where')

assert [
%{
Expand Down Expand Up @@ -1348,7 +1349,7 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do
end

test "complete after ! operator" do
assert [%{name: "is_binary"}] = expand('!is_bin')
assert [%{name: "is_binary"}] = expand('!is_bina')
end

test "correctly find subtype and doc for modules that have submodule" do
Expand Down
4 changes: 4 additions & 0 deletions test/elixir_sense/providers/suggestion/matcher_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
defmodule ElixirSense.Providers.Suggestion.MatcherTest do
use ExUnit.Case, async: true
doctest ElixirSense.Providers.Suggestion.Matcher
end
8 changes: 7 additions & 1 deletion test/elixir_sense/providers/suggestion_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,13 @@ defmodule ElixirSense.Providers.SuggestionTest do

test "local calls should not return built-in functions" do
list =
Suggestion.find("mo", @env, %Metadata{}, @cursor_context)
Suggestion.find(
# Trying to find module_info
"module_",
@env,
%Metadata{},
@cursor_context
)
|> Enum.filter(fn item -> item.type in [:function] end)

assert list == []
Expand Down
Loading