Skip to content

Commit

Permalink
WIP: Display docs on module hover
Browse files Browse the repository at this point in the history
  • Loading branch information
zachallaun committed Aug 18, 2023
1 parent 99de01c commit ffc81be
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 58 deletions.
9 changes: 4 additions & 5 deletions apps/remote_control/lib/lexical/remote_control/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,9 @@ defmodule Lexical.RemoteControl.Api do
])
end

def docs(%Project{} = project, %Document{} = document, %Position{} = position) do
RemoteControl.call(project, CodeIntelligence.Definition, :docs, [
document,
position
])
def docs(%Project{} = project, module) when is_atom(module) do
with {:module, _} <- RemoteControl.call(project, Code, :ensure_compiled, [module]) do
RemoteControl.call(project, Code, :fetch_docs, [module])
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,4 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Definition do
|> Document.to_string()
|> ElixirSense.definition(position.line, position.character)
end

def docs(%Document{} = document, %Position{} = position) do
document
|> Document.to_string()
|> ElixirSense.docs(position.line, position.character)
end
end
51 changes: 37 additions & 14 deletions apps/remote_control/test/fixtures/project/lib/docs.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Project.Docs do
defmodule ModuleWithDocs do
defmodule PublicModule do
@moduledoc """
This module has docs. See `Project.ModuleWithoutDocs` for a module that does not.
This module has docs.
"""

@typedoc "type docs for my_type"
Expand All @@ -13,44 +13,67 @@ defmodule Project.Docs do
@doc """
Docs for `fun/0`.
iex> Project.ModuleWithDocs.fun()
iex> Project.Docs.PublicModule.fun()
:ok
"""
def fun, do: :ok

@doc """
Docs for `fun/1`.
iex> Project.ModuleWithDocs.fun(1)
iex> Project.Docs.PublicModule.fun(1)
:ok
"""
def fun(_), do: :ok

@doc """
Docs for `fun/2`.
iex> Project.ModuleWithDocs.fun(1, 2)
iex> Project.Docs.PublicModule.fun(1, 2)
:ok
"""
def fun(_, _), do: :ok
end

defmodule ModuleWithoutDocs do
defmodule PrivateModule do
@moduledoc false

@typedoc false
@type my_type :: :ok

@typedoc false
@opaque my_opaque :: :ok

@doc false
def fun, do: :ok

@doc false
def fun(_), do: :ok

@doc false
def fun(_, _), do: :ok
end

defmodule UndocumentedModule do
@type my_type :: :ok
@opaque my_opaque :: :ok
def fun, do: :ok
def fun(_), do: :ok
def fun(_, _), do: :ok
end

alias Project.Docs.ModuleWithDocs
@type t1 :: ModuleWithDocs.my_type()
@type t2 :: ModuleWithDocs.my_opaque()
ModuleWithDocs.fun()
alias Project.Docs.PublicModule
@type t1 :: PublicModule.my_type()
@type t2 :: PublicModule.my_opaque()
PublicModule.fun()

alias Project.Docs.PrivateModule
@type t3 :: PrivateModule.my_type()
@type t4 :: PrivateModule.my_opaque()
PrivateModule.fun()

alias Project.Docs.ModuleWithoutDocs
@type t3 :: ModuleWithoutDocs.my_type()
@type t4 :: ModuleWithoutDocs.my_opaque()
ModuleWithoutDocs.fun()
alias Project.Docs.UndocumentedModule
@type t5 :: UndocumentedModule.my_type()
@type t6 :: UndocumentedModule.my_opaque()
UndocumentedModule.fun()
end
8 changes: 6 additions & 2 deletions apps/server/lib/lexical/server/code_intelligence/entity.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ defmodule Lexical.Server.CodeIntelligence.Entity do
@spec resolve(Document.t(), Position.t()) :: {:ok, resolved} | {:error, term()}
def resolve(%Document{} = document, %Position{} = position) do
with {:ok, ast} <- CodeMod.Ast.from(document),
zipper when not is_nil(zipper) <- innermost_zipper_at(ast, position),
{:ok, zipper} <- innermost_zipper_at(ast, position),
{:ok, resolved} <- resolve(Zipper.node(zipper), zipper, document, position) do
Logger.info("Resolved entity: #{inspect(resolved)}")
{:ok, resolved}
Expand Down Expand Up @@ -127,7 +127,11 @@ defmodule Lexical.Server.CodeIntelligence.Entity do
{:cont, zipper, acc}
end)

innermost
if innermost do
{:ok, innermost}
else
{:error, :not_found}
end
end

# Corrects aliases issue: https://github.com/doorgan/sourceror/issues/99
Expand Down
57 changes: 42 additions & 15 deletions apps/server/lib/lexical/server/provider/handlers/hover.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,57 @@ defmodule Lexical.Server.Provider.Handlers.Hover do
alias Lexical.Protocol.Responses
alias Lexical.Protocol.Types.Hover
alias Lexical.Protocol.Types.Markup
alias Lexical.RemoteControl
alias Lexical.Server.CodeIntelligence.Entity
alias Lexical.Server.Provider.Env

def handle(%Requests.Hover{} = request, %Env{}) do
content =
case Entity.resolve(request.document, request.position) do
{:ok, {:module, module}} ->
module_name = module |> to_string() |> String.replace_prefix("Elixir.", "")
require Logger

"""
## #{module_name}
"""
def handle(%Requests.Hover{} = request, %Env{} = env) do
maybe_hover =
with {:ok, {:module, module}} <- Entity.resolve(request.document, request.position),
{:ok, doc_content} <- module_doc_content(env.project, module) do
module_name = module |> to_string() |> String.replace_prefix("Elixir.", "")

_ ->
nil
end
content = """
### #{module_name}
#{doc_content}\
"""

hover =
if content do
%Hover{contents: %Markup.Content{kind: :markdown, value: content}}
else
nil
error ->
Logger.warning("Could not resolve hover request, got: #{inspect(error)}")
nil
end

{:reply, Responses.Hover.new(request.id, hover)}
{:reply, Responses.Hover.new(request.id, maybe_hover)}
end

defp module_doc_content(project, module) do
case fetch_docs(project, module) do
{:ok, %{module_doc: doc}} when is_binary(doc) ->
{:ok, doc}

{:ok, %{module_doc: :none}} ->
{:ok, "*This module is undocumented.*\n"}

{:ok, %{module_doc: :hidden}} ->
{:ok, "*This module is private.*\n"}

_ ->
:error
end
end

defp fetch_docs(project, module) do
with {:docs_v1, _annotation, _lang, _format, module_doc, _meta, docs} <-
RemoteControl.Api.docs(project, module) do
{:ok, %{module_doc: parse_module_doc(module_doc), docs: docs}}
end
end

defp parse_module_doc(%{"en" => module_doc}), do: module_doc
defp parse_module_doc(other) when is_atom(other), do: other
end
49 changes: 33 additions & 16 deletions apps/server/test/lexical/server/provider/handlers/hover_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ defmodule Lexical.Server.Provider.Handlers.HoverTest do
alias Lexical.Proto.Convert
alias Lexical.Protocol.Requests
alias Lexical.Protocol.Types
alias Lexical.RemoteControl
alias Lexical.Server
alias Lexical.Server.Project.Dispatch
alias Lexical.Server.Provider.Env
alias Lexical.Server.Provider.Handlers

Expand All @@ -16,16 +14,13 @@ defmodule Lexical.Server.Provider.Handlers.HoverTest do
use ExUnit.Case, async: false

setup_all do
start_supervised(Document.Store)
project = project(:project)
project = project()

{:ok, _} = start_supervised(Document.Store)
{:ok, _} = start_supervised({DynamicSupervisor, Server.Project.Supervisor.options()})

{:ok, _} = start_supervised({Server.Project.Supervisor, project})

Dispatch.register(project, [project_compiled()])
RemoteControl.Api.schedule_compile(project, true)

:ok = Server.Project.Dispatch.register(project, [project_compiled()])
assert_receive project_compiled(), 5000

{:ok, project: project}
Expand Down Expand Up @@ -54,33 +49,55 @@ defmodule Lexical.Server.Provider.Handlers.HoverTest do
Handlers.Hover.handle(request, %Env{project: project})
end

describe "hover" do
describe "module hover" do
setup [:with_uri]
@describetag uri_for: "docs.ex"

test "module with docs", %{project: project, uri: uri} do
# alias Project.Docs.ModuleWithDocs
# alias Project.Docs.PublicModule
# ^
{:ok, request} = build_request(uri, 46, 21)
{:ok, request} = build_request(uri, 64, 21)

assert {:reply, %{result: %Types.Hover{contents: content}}} = handle(request, project)

assert content.kind == :markdown

assert content.value == """
## Project.Docs.ModuleWithDocs
### Project.Docs.PublicModule
This module has docs.
"""
end

# alias Project.Docs.ModuleWithDocs
# ^
{:ok, request} = build_request(uri, 46, 19)
test "hidden module", %{project: project, uri: uri} do
# alias Project.Docs.PrivateModule
# ^
{:ok, request} = build_request(uri, 69, 21)

assert {:reply, %{result: %Types.Hover{contents: content}}} = handle(request, project)

assert content.kind == :markdown

assert content.value == """
## Project.Docs
### Project.Docs.PrivateModule
*This module is private.*
"""
end

test "undocumented module", %{project: project, uri: uri} do
# alias Project.Docs.UndocumentedModule
# ^
{:ok, request} = build_request(uri, 74, 21)

assert {:reply, %{result: %Types.Hover{contents: content}}} = handle(request, project)

assert content.kind == :markdown

assert content.value == """
### Project.Docs.UndocumentedModule
*This module is undocumented.*
"""
end
end
Expand Down

0 comments on commit ffc81be

Please sign in to comment.