Skip to content
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

Allow to lookup Elixir source files under configured location #277

Merged
merged 2 commits into from
Aug 29, 2023
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
57 changes: 39 additions & 18 deletions lib/elixir_sense/location.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ defmodule ElixirSense.Location do
}
defstruct [:type, :file, :line, :column]

defguardp file_exists(file) when file not in ["non_existing", nil, ""]

@spec find_mod_fun_source(module, atom, non_neg_integer | {:gte, non_neg_integer} | :any) ::
Location.t() | nil
def find_mod_fun_source(mod, fun, arity) do
case find_mod_file(mod) do
{mod, file} when file_exists(file) ->
file when is_binary(file) ->
find_fun_position({mod, file}, fun, arity)

_ ->
Expand All @@ -34,10 +32,10 @@ defmodule ElixirSense.Location do

@spec find_type_source(module, atom, non_neg_integer | {:gte, non_neg_integer} | :any) ::
Location.t() | nil
def find_type_source(mod, fun, arity) do
def find_type_source(mod, type, arity) do
case find_mod_file(mod) do
{mod, file} when file_exists(file) ->
find_type_position({mod, file}, fun, arity)
file when is_binary(file) ->
find_type_position({mod, file}, type, arity)

_ ->
nil
Expand All @@ -47,6 +45,10 @@ defmodule ElixirSense.Location do
defp find_mod_file(Elixir), do: nil

defp find_mod_file(module) do
find_elixir_file(module) || find_erlang_file(module)
end

defp find_elixir_file(module) do
file =
if Code.ensure_loaded?(module) do
case module.module_info(:compile)[:source] do
Expand All @@ -55,26 +57,45 @@ defmodule ElixirSense.Location do
end
end

file =
if file && File.exists?(file, [:raw]) do
if file do
if File.exists?(file, [:raw]) do
file
else
with {_module, _binary, beam_filename} <- :code.get_object_code(module),
erl_file =
beam_filename
|> to_string
|> String.replace(
~r/(.+)\/ebin\/([^\s]+)\.beam$/,
"\\1/src/\\2.erl"
# If Elixir was built in a sandboxed environment,
# `module.module_info(:compile)[:source]` would point to a non-existing
# location; in this case try to find a "core" Elixir source file under
# the configured Elixir source path.
with elixir_src when is_binary(elixir_src) <-
Application.get_env(:elixir_sense, :elixir_src),
file =
String.replace(
file,
Regex.recompile!(~r<^(?:.+)(/lib/.+\.ex)$>U),
elixir_src <> "\\1"
),
true <- File.exists?(erl_file, [:raw]) do
erl_file
true <- File.exists?(file, [:raw]) do
file
else
_ -> nil
end
end
end
end

{module, file}
defp find_erlang_file(module) do
with {_module, _binary, beam_filename} <- :code.get_object_code(module),
erl_file =
beam_filename
|> to_string
|> String.replace(
Regex.recompile!(~r/(.+)\/ebin\/([^\s]+)\.beam$/),
"\\1/src/\\2.erl"
),
true <- File.exists?(erl_file, [:raw]) do
erl_file
else
_ -> nil
end
end

defp find_fun_position({mod, file}, fun, arity) do
Expand Down
32 changes: 32 additions & 0 deletions test/elixir_sense/location_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule ElixirSense.LocationTest do
use ExUnit.Case, async: false

import ElixirSense.Location

setup do
elixir_src = Path.join(File.cwd!(), "/test/misc/mock_elixir_src")
Application.put_env(:elixir_sense, :elixir_src, elixir_src)

on_exit(fn ->
Application.delete_env(:elixir_sense, :elixir_src)
end)
end

describe "find_mod_fun_source/3" do
test "returns location of a core Elixir function" do
assert %ElixirSense.Location{type: :function, line: 26, column: 3, file: file} =
find_mod_fun_source(String, :length, 1)

assert String.ends_with?(file, "/mock_elixir_src/lib/elixir/lib/string.ex")
end
end

describe "find_type_source/3" do
test "returns location of a core Elixir type" do
assert %ElixirSense.Location{type: :typespec, line: 11, column: 3, file: file} =
find_type_source(String, :t, 0)

assert String.ends_with?(file, "/mock_elixir_src/lib/elixir/lib/string.ex")
end
end
end
35 changes: 35 additions & 0 deletions test/misc/mock_elixir_src/lib/elixir/lib/string.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Kernel, except: [length: 1]

defmodule String do
@typedoc """
A UTF-8 encoded binary.

The types `String.t()` and `binary()` are equivalent to analysis tools.
Although, for those reading the documentation, `String.t()` implies
it is a UTF-8 encoded binary.
"""
@type t :: binary

@doc """
Returns the number of Unicode graphemes in a UTF-8 string.

## Examples

iex> String.length("elixir")
6

iex> String.length("եոգլի")
5

"""
@spec length(t) :: non_neg_integer
def length(string) when is_binary(string), do: length(string, 0)

defp length(gcs, acc) do
case :unicode_util.gc(gcs) do
[_ | rest] -> length(rest, acc + 1)
[] -> acc
{:error, <<_, rest::bits>>} -> length(rest, acc + 1)
end
end
end
Loading