Skip to content

Commit

Permalink
use completionItem/resolve to fetch docs
Browse files Browse the repository at this point in the history
- this makes completion faster because we defer fetching docs
    - we binary_to_term/encode the relevant data to put in the data
      which then gets sent back up on the resolve request
- also includes signature in docs
- makes cursor finding faster by skipping subtrees that don't contain
  the cursor position

code is still WIP, which is why its ugly

on average of typing around in the next_ls.ex file, completions seem to
be around 45-55ms. still not as fast as I'd like them, but still
decent-ish
  • Loading branch information
mhanberg committed May 1, 2024
1 parent e1e3701 commit 3aa90ec
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 108 deletions.
120 changes: 99 additions & 21 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ defmodule NextLS do
completion_provider:
if init_opts.experimental.completions.enable do
%GenLSP.Structures.CompletionOptions{
trigger_characters: [".", "@", "&", "%", "^", ":", "!", "-", "~", "/", "{"]
trigger_characters: [".", "@", "&", "%", "^", ":", "!", "-", "~", "/", "{"],
resolve_provider: true
}
else
nil
Expand Down Expand Up @@ -583,6 +584,76 @@ defmodule NextLS do
resp
end

def handle_request(%GenLSP.Requests.CompletionItemResolve{params: completion_item}, lsp) do
completion_item =
with nil <- completion_item.data do
completion_item
else
%{"uri" => uri, "data" => data} ->
docs =
case data |> Base.decode64!() |> :erlang.binary_to_term() do
{mod, function, arity} ->
result =
dispatch(lsp.assigns.registry, :runtimes, fn entries ->
[result] =
for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do
Runtime.call(runtime, {Code, :fetch_docs, [mod]})
end

result
end)

with {:ok, {:docs_v1, _, _lang, content_type, %{"en" => _mod_doc}, _, fdocs}} <- result do
doc =
Enum.find(fdocs, fn {{type, name, a}, _some_number, _signature, doc, _other} ->
type in [:function, :macro] and to_string(name) == function and doc != :hidden and a >= arity
end)

case doc do
{_, _, [signature], %{"en" => fdoc}, _} ->
"""
## #{Macro.to_string(mod)}.#{function}/#{arity}
`#{signature}`
#{NextLS.HoverHelpers.to_markdown(content_type, fdoc)}
"""

_ ->
nil
end
else
_ -> nil
end

mod ->
result =
dispatch(lsp.assigns.registry, :runtimes, fn entries ->
[result] =
for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do
Runtime.call(runtime, {Code, :fetch_docs, [mod]})
end

result
end)

with {:ok, {:docs_v1, _, _lang, content_type, %{"en" => doc}, _, _fdocs}} <- result do
"""
## #{Macro.to_string(mod)}
#{NextLS.HoverHelpers.to_markdown(content_type, doc)}
"""
else
_ -> nil
end
end

%{completion_item | documentation: docs}
end

{:reply, completion_item, lsp}
end

def handle_request(%TextDocumentCompletion{params: %{text_document: %{uri: uri}, position: position}}, lsp) do
document = lsp.assigns.documents[uri]

Expand All @@ -592,7 +663,7 @@ defmodule NextLS do
:timer.tc(
fn ->
source
|> Spitfire.parse()
|> Spitfire.parse(literal_encoder: &{:ok, {:__block__, &2, [&1]}})
|> then(fn
{:ok, ast} -> ast
{:error, ast, _} -> ast
Expand All @@ -616,15 +687,15 @@ defmodule NextLS do

# dbg(Sourceror.Zipper.node(with_cursor_zipper))

{ms, env} =
:timer.tc(
fn ->
NextLS.ASTHelpers.Env.build(with_cursor_zipper, %{line: position.line + 1, column: position.character + 1})
end,
:millisecond
)
# {ms, env} =
# :timer.tc(
# fn ->
# NextLS.ASTHelpers.Env.build(with_cursor_zipper, %{line: position.line + 1, column: position.character + 1})
# end,
# :millisecond
# )

Logger.debug("build env: #{ms}ms")
# Logger.debug("build env: #{ms}ms")

document_slice =
document
Expand Down Expand Up @@ -653,12 +724,13 @@ defmodule NextLS do

Logger.debug("expand: #{ms}ms")

env =
env
|> Map.put(:functions, macro_env.functions)
|> Map.put(:macros, macro_env.macros)
|> Map.put(:aliases, macro_env.aliases)
|> Map.put(:attrs, macro_env.attrs)
env = macro_env
# env
# |> Map.put(:functions, macro_env.functions)
# |> Map.put(:macros, macro_env.macros)
# |> Map.put(:aliases, macro_env.aliases)
# |> Map.put(:attrs, macro_env.attrs)
# |> Map.put(:variables, macro_env.variables)

doc =
document_slice
Expand Down Expand Up @@ -693,13 +765,13 @@ defmodule NextLS do
{name, GenLSP.Enumerations.CompletionItemKind.struct(), ""}

:function ->
{"#{name}/#{symbol.arity}", GenLSP.Enumerations.CompletionItemKind.function(), symbol.docs}
{"#{name}/#{symbol.arity}", GenLSP.Enumerations.CompletionItemKind.function(), symbol[:docs]}

:module ->
{name, GenLSP.Enumerations.CompletionItemKind.module(), symbol.docs}
{name, GenLSP.Enumerations.CompletionItemKind.module(), symbol[:docs]}

:variable ->
{name, GenLSP.Enumerations.CompletionItemKind.variable(), ""}
{to_string(name), GenLSP.Enumerations.CompletionItemKind.variable(), ""}

:dir ->
{name, GenLSP.Enumerations.CompletionItemKind.folder(), ""}
Expand All @@ -724,8 +796,14 @@ defmodule NextLS do
%GenLSP.Structures.CompletionItem{
label: label,
kind: kind,
insert_text: name,
documentation: docs
insert_text: to_string(name),
documentation: docs,
data:
if symbol[:data] do
%{uri: uri, data: symbol[:data] |> :erlang.term_to_binary() |> Base.encode64()}
else
nil
end
}

root_path = root_path |> URI.parse() |> Map.get(:path)
Expand Down
151 changes: 78 additions & 73 deletions lib/next_ls/autocomplete.ex
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ defmodule NextLS.Autocomplete do
defp match_var(code, hint, _runtime, env) do
code
|> variables_from_binding(env)
|> Enum.filter(&String.starts_with?(&1, hint))
|> Enum.filter(&String.starts_with?(to_string(&1), hint))
|> Enum.sort()
|> Enum.map(&%{kind: :variable, name: &1})
end
Expand All @@ -304,23 +304,24 @@ defmodule NextLS.Autocomplete do

defp match_erlang_modules(hint, runtime) do
for mod <- match_modules(hint, false, runtime), usable_as_unquoted_module?(mod) do
{content_type, mdoc} =
case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(mod)) do
{:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} ->
{content_type, mdoc}
# {content_type, mdoc} =
# case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(mod)) do
# {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} ->
# {content_type, mdoc}

_ ->
{"text/markdown", nil}
end
# _ ->
# {"text/markdown", nil}
# end

%{
kind: :module,
name: mod,
docs: """
## #{Macro.to_string(mod)}
data: mod
# docs: """
### #{Macro.to_string(mod)}

#{NextLS.HoverHelpers.to_markdown(content_type, mdoc)}
"""
## {NextLS.HoverHelpers.to_markdown(content_type, mdoc)}
# """
}
end
end
Expand Down Expand Up @@ -512,28 +513,29 @@ defmodule NextLS.Autocomplete do
end
end

defp match_aliases(hint, runtime, env) do
defp match_aliases(hint, _runtime, env) do
for {alias, module} <- aliases_from_env(env),
[name] = Module.split(alias),
String.starts_with?(name, hint) do
{content_type, mdoc} =
case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(module)) do
{:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} ->
{content_type, mdoc}
# {content_type, mdoc} =
# case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(module)) do
# {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} ->
# {content_type, mdoc}

_ ->
{"text/markdown", nil}
end
# _ ->
# {"text/markdown", nil}
# end

%{
kind: :module,
name: name,
module: module,
docs: """
## #{Macro.to_string(module)}
data: module,
module: module
# docs: """
### #{Macro.to_string(module)}

#{NextLS.HoverHelpers.to_markdown(content_type, mdoc)}
"""
## {NextLS.HoverHelpers.to_markdown(content_type, mdoc)}
# """
}
end
end
Expand All @@ -551,23 +553,24 @@ defmodule NextLS.Autocomplete do
valid_alias_piece?("." <> name) do
alias = Module.concat([mod])

{content_type, mdoc} =
case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(alias)) do
{:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} ->
{content_type, mdoc}
# {content_type, mdoc} =
# case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(alias)) do
# {:ok, {:docs_v1, _, _lang, content_type, %{"en" => mdoc}, _, _fdocs}} ->
# {content_type, mdoc}

_ ->
{"text/markdown", nil}
end
# _ ->
# {"text/markdown", nil}
# end

%{
kind: :module,
name: name,
docs: """
## #{Macro.to_string(alias)}
data: alias,
name: name
# docs: """
### #{Macro.to_string(alias)}

#{NextLS.HoverHelpers.to_markdown(content_type, mdoc)}
"""
## {NextLS.HoverHelpers.to_markdown(content_type, mdoc)}
# """
}
end

Expand Down Expand Up @@ -678,43 +681,44 @@ defmodule NextLS.Autocomplete do
apps
end

defp match_module_funs(runtime, mod, funs, hint, exact?) do
{content_type, fdocs} =
case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(mod)) do
{:ok, {:docs_v1, _, _lang, content_type, _, _, fdocs}} ->
{content_type, fdocs}
defp match_module_funs(_runtime, mod, funs, hint, exact?) do
# {content_type, fdocs} =
# case NextLS.Runtime.execute(runtime, do: Code.fetch_docs(mod)) do
# {:ok, {:docs_v1, _, _lang, content_type, _, _, fdocs}} ->
# {content_type, fdocs}

_ ->
{"text/markdown", []}
end
# _ ->
# {"text/markdown", []}
# end

functions =
for {fun, arity} <- funs,
name = Atom.to_string(fun),
if(exact?, do: name == hint, else: String.starts_with?(name, hint)) do
doc =
Enum.find(fdocs, fn {{type, fname, _a}, _, _, _doc, _} ->
type in [:function, :macro] and to_string(fname) == name
end)
# doc =
# Enum.find(fdocs, fn {{type, fname, _a}, _, _, _doc, _} ->
# type in [:function, :macro] and to_string(fname) == name
# end)

doc =
case doc do
{_, _, _, %{"en" => fdoc}, _} ->
"""
## #{Macro.to_string(mod)}.#{name}/#{arity}
# doc =
# case doc do
# {_, _, _, %{"en" => fdoc}, _} ->
# """
# ## #{Macro.to_string(mod)}.#{name}/#{arity}

#{NextLS.HoverHelpers.to_markdown(content_type, fdoc)}
"""
# #{NextLS.HoverHelpers.to_markdown(content_type, fdoc)}
# """

_ ->
nil
end
# _ ->
# nil
# end

%{
kind: :function,
data: {mod, name, arity},
name: name,
arity: arity,
docs: doc
arity: arity
# docs: doc
}
end

Expand Down Expand Up @@ -783,18 +787,19 @@ defmodule NextLS.Autocomplete do
# end
# end

defp get_docs(mod, kinds, fun \\ nil) do
case Code.fetch_docs(mod) do
{:docs_v1, _, _, _, _, _, docs} ->
if is_nil(fun) do
for {{kind, _, _}, _, _, _, _} = doc <- docs, kind in kinds, do: doc
else
for {{kind, ^fun, _}, _, _, _, _} = doc <- docs, kind in kinds, do: doc
end

{:error, _} ->
nil
end
defp get_docs(_mod, _kinds, _fun \\ nil) do
# case Code.fetch_docs(mod) do
# {:docs_v1, _, _, _, _, _, docs} ->
# if is_nil(fun) do
# for {{kind, _, _}, _, _, _, _} = doc <- docs, kind in kinds, do: doc
# else
# for {{kind, ^fun, _}, _, _, _, _} = doc <- docs, kind in kinds, do: doc
# end

# {:error, _} ->
# nil
# end
nil
end

defp default_arg_functions_with_doc_false(docs) do

Check warning on line 805 in lib/next_ls/autocomplete.ex

View workflow job for this annotation

GitHub Actions / dialyzer

unused_fun

Function default_arg_functions_with_doc_false/1 will never be called.
Expand Down
Loading

0 comments on commit 3aa90ec

Please sign in to comment.