Skip to content

Commit

Permalink
Added unit test, handled include_delcaration parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
scohen committed Oct 19, 2023
1 parent d3cab0d commit 397d77b
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 897 deletions.
13 changes: 11 additions & 2 deletions apps/remote_control/lib/lexical/remote_control/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,17 @@ defmodule Lexical.RemoteControl.Api do
])
end

def references(%Project{} = project, resolved_reference) do
RemoteControl.call(project, CodeIntelligence.References, :references, [resolved_reference])
def references(
%Project{} = project,
%Document{} = document,
%Position{} = position,
include_definitions?
) do
RemoteControl.call(project, CodeIntelligence.References, :references, [
document,
position,
include_definitions?
])
end

def modules_with_prefix(%Project{} = project, prefix)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ defmodule Lexical.RemoteControl.Application do
RemoteControl.Build,
RemoteControl.Build.CaptureServer,
RemoteControl.Plugin.Runner.Supervisor,
RemoteControl.Plugin.Runner.Coordinator
RemoteControl.Plugin.Runner.Coordinator,
{RemoteControl.Search.Store,
[
&RemoteControl.Search.Indexer.create_index/1,
&RemoteControl.Search.Indexer.update_index/2
]}
]
else
[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,60 +267,4 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Entity do

# Catch-all:
defp kind_of_alias(_), do: :module

def references(%Project{} = project, %Document{} = document, %Position{} = position) do
with {:ok, resolved, _range} <- resolve(document, position) do
RemoteControl.Api.references(project, resolved)
end
end

@doc """
Returns the source location of the entity at the given position in the document.
"""
def definition(%Project{} = project, %Document{} = document, %Position{} = position) do
project
|> RemoteControl.Api.definition(document, position)
|> parse_location(document)
end

defp parse_location(%ElixirSense.Location{} = location, %Document{} = document) do
%{file: file, line: line, column: column} = location
file_path = file || document.path
uri = Document.Path.ensure_uri(file_path)

with {:ok, document} <- Document.Store.open_temporary(uri),
{:ok, text} <- Document.fetch_text_at(document, line) do
range = to_precise_range(document, text, line, column)

{:ok, Location.new(range, document)}
else
_ ->
{:error, "Could not open source file or fetch line text: #{inspect(file_path)}"}
end
end

defp parse_location(nil, _) do
{:ok, nil}
end

defp to_precise_range(%Document{} = document, text, line, column) do
case Code.Fragment.surround_context(text, {line, column}) do
%{begin: start_pos, end: end_pos} ->
to_range(document, start_pos, end_pos)

_ ->
# If the column is 1, but the code doesn't start on the first column, which isn't what we want.
# The cursor will be placed to the left of the actual definition.
column = if column == 1, do: Text.count_leading_spaces(text) + 1, else: column
pos = {line, column}
to_range(document, pos, pos)
end
end

defp to_range(%Document{} = document, {begin_line, begin_column}, {end_line, end_column}) do
Range.new(
Position.new(document, begin_line, begin_column),
Position.new(document, end_line, end_column)
)
end
end
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
defmodule Lexical.RemoteControl.CodeIntelligence.References do
alias Lexical.Document
alias Lexical.Document.Location
alias Lexical.RemoteControl.CodeIntelligence.Entity
alias Lexical.RemoteControl.Search.Indexer.Entry
alias Lexical.RemoteControl.Search.Store

require Logger

def references({:module, module}) do
module_references(module)
def references(%Document{} = document, %Document.Position{} = position, include_definitions?) do
with {:ok, resolved, _range} <- Entity.resolve(document, position) do
find_references(resolved, include_definitions?)
end
end

defp find_references({:module, module}, include_definitions?) do
module_references(module, include_definitions?)
end

def references({:struct, struct_module}) do
module_references(struct_module)
defp find_references({:struct, struct_module}, include_definitions?) do
module_references(struct_module, include_definitions?)
end

def references(resolved) do
defp find_references(resolved, _include_definitions?) do
Logger.info("Not attempting to find references for unhandled type: #{inspect(resolved)}")
[]
end

defp module_references(module) do
with {:ok, entities} <- Store.exact(module, type: :module, subtype: :reference) do
defp module_references(module, include_definitions?) do
with {:ok, references} <- Store.exact(module, type: :module, subtype: :reference) do
entities = maybe_fetch_module_definitions(module, include_definitions?) ++ references
locations = Enum.map(entities, &to_location/1)
{:ok, locations}
end
Expand All @@ -30,4 +38,15 @@ defmodule Lexical.RemoteControl.CodeIntelligence.References do
uri = Document.Path.ensure_uri(entry.path)
Location.new(entry.range, uri)
end

defp maybe_fetch_module_definitions(module, true) do
case Store.exact(module, type: :module, subtype: :definition) do
{:ok, definitions} -> definitions
_ -> []
end
end

defp maybe_fetch_module_definitions(_module, false) do
[]
end
end
3 changes: 2 additions & 1 deletion apps/remote_control/lib/lexical/remote_control/dispatch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ defmodule Lexical.RemoteControl.Dispatch do
`Lexical.RemoteControl.Dispatch.Handler` behaviour and add the module to the @handlers module attribute.
"""
alias Lexical.RemoteControl
alias Lexical.RemoteControl.Dispatch.Handlers
alias Lexical.RemoteControl.Dispatch.PubSub
import Lexical.RemoteControl.Api.Messages

@handlers [PubSub]
@handlers [PubSub, Handlers.Indexing]

# public API

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ defmodule Lexical.RemoteControl.Search.Store do
@impl GenServer
# handle the result from `State.async_load/1`
def handle_info({ref, result}, {update_ref, %State{async_load_ref: ref} = state}) do
RemoteControl.Dispatch.broadcast(index_ready(project: state.project))
{:noreply, {update_ref, State.async_load_complete(state, result)}}
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ defmodule Lexical.RemoteControl.Search.Store.State do
project: project,
loaded?: false,
update_index: update_index,
update_buffer: %{}
update_buffer: %{},
fuzzy: Fuzzy.from_entries([])
}
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
defmodule Lexical.RemoteControl.CodeIntelligence.ReferencesTest do
alias Lexical.Document
alias Lexical.Document.Location
alias Lexical.RemoteControl
alias Lexical.RemoteControl.CodeIntelligence.References
alias Lexical.RemoteControl.Search

use ExUnit.Case, async: false

import Lexical.Test.CodeSigil
import Lexical.Test.CursorSupport
import Lexical.Test.Fixtures
import Lexical.Test.RangeSupport
import Lexical.Test.EventualAssertions

setup do
project = project()
RemoteControl.set_project(project)
start_supervised!(Document.Store)

start_supervised!(
{Search.Store,
[
project,
fn _ -> {:ok, []} end,
fn _, _ -> {:ok, [], []} end,
Search.Store.Backends.Ets
]}
)

assert_eventually Search.Store.loaded?()
{:ok, project: project}
end

defp module_uri(project) do
project
|> file_path(Path.join("lib", "my_module.ex"))
|> Document.Path.ensure_uri()
end

defp project_module(project, content) do
uri = module_uri(project)

with :ok <- Document.Store.open(uri, content, 1) do
Document.Store.fetch(uri)
end
end

describe "module references" do
# Note: These tests aren't exhaustive, as that is covered by Search.StoreTest.
test "are found in an alias", %{project: project} do
code = ~q[
defmodule ReferencesInAlias do
alias ReferencedModule
end
]

assert {:ok, [%Location{} = location]} = references(project, "ReferencedModule|", code)
assert decorate(code, location.range) =~ ~s[alias «ReferencedModule»]
end

test "are found in a module attribute", %{project: project} do
code = ~q[
defmodule ReferenceInAttribute do
@attr ReferencedModule
end
]

assert {:ok, [%Location{} = location]} = references(project, "ReferencedModule|", code)
assert decorate(code, location.range) =~ ~s[@attr «ReferencedModule»]
end

test "are found in a variable", %{project: project} do
code = ~q[
some_module = ReferencedModule
]

assert {:ok, [%Location{} = location]} = references(project, "ReferencedModule|", code)
assert decorate(code, location.range) =~ ~s[some_module = «ReferencedModule»]
end

test "are found in a function's parameters", %{project: project} do
code = ~q[
def some_fn(ReferencedModule) do
end
]

assert {:ok, [%Location{} = location]} = references(project, "ReferencedModule|", code)
assert decorate(code, location.range) =~ ~s[def some_fn(«ReferencedModule») do]
end

test "includes struct definitions", %{project: project} do
code = ~q[
%ReferencedModule{} = something_else
]

assert {:ok, [%Location{} = location]} = references(project, "ReferencedModule|", code)
assert decorate(code, location.range) =~ ~s[%«ReferencedModule»{} = something_else]
end

test "includes modules when a struct is requested", %{project: project} do
code = ~q[
ReferencedModule = something_else
]

assert {:ok, [%Location{} = location]} = references(project, "%ReferencedModule|{}", code)
assert decorate(code, location.range) =~ ~s[«ReferencedModule» = something_else]
end

test "includes definitions if the parameter is true", %{project: project} do
code = ~q[
defmodule DefinedModule do
end
defmodule OtherModule do
@attr DefinedModule
end
]

assert {:ok, [location_1, location_2]} = references(project, "DefinedModule|", code, true)
assert decorate(code, location_1.range) =~ ~s[defmodule «DefinedModule» do]
assert decorate(code, location_2.range) =~ ~s[@attr «DefinedModule»]
end
end

defp references(project, referenced_item, code, include_definitions? \\ false) do
with {position, referenced_item} <- pop_cursor(referenced_item, as: :document),
{:ok, document} <- project_module(project, code),
{:ok, entries} <- Search.Indexer.Source.index(document.path, code),
:ok <- Search.Store.replace(entries) do
References.references(referenced_item, position, include_definitions?)
end
end
end
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
defmodule Lexical.Server.Provider.Handlers.FindReferences do
alias Lexical.Protocol.Requests.FindReferences
alias Lexical.Protocol.Responses
alias Lexical.Protocol.Responses
alias Lexical.Server.CodeIntelligence.Entity
alias Lexical.RemoteControl.Api
alias Lexical.Server.Provider.Env

require Logger

def handle(%FindReferences{} = request, %Env{} = env) do
include_declaration? = !!request.context.include_declaration

locations =
case Entity.references(env.project, request.document, request.position) do
case Api.references(env.project, request.document, request.position, include_declaration?) do
{:ok, locations} ->
locations

Expand Down

0 comments on commit 397d77b

Please sign in to comment.