A apps/protocol/lib/generated/lexical/protocol/types/document/symbol.ex => apps/protocol/lib/generated/lexical/protocol/types/document/symbol.ex +15 -0
@@ 0,0 1,15 @@
+# This file's contents are auto-generated. Do not edit.
+defmodule Lexical.Protocol.Types.Document.Symbol do
+ alias Lexical.Proto
+ alias Lexical.Protocol.Types
+ use Proto
+
+ deftype children: optional(list_of(Types.Document.Symbol)),
+ deprecated: optional(boolean()),
+ detail: optional(string()),
+ kind: Types.Symbol.Kind,
+ name: string(),
+ range: Types.Range,
+ selection_range: Types.Range,
+ tags: optional(list_of(Types.Symbol.Tag))
+end
A apps/protocol/lib/generated/lexical/protocol/types/document/symbol/params.ex => apps/protocol/lib/generated/lexical/protocol/types/document/symbol/params.ex +10 -0
@@ 0,0 1,10 @@
+# This file's contents are auto-generated. Do not edit.
+defmodule Lexical.Protocol.Types.Document.Symbol.Params do
+ alias Lexical.Proto
+ alias Lexical.Protocol.Types
+ use Proto
+
+ deftype partial_result_token: optional(Types.Progress.Token),
+ text_document: Types.TextDocument.Identifier,
+ work_done_token: optional(Types.Progress.Token)
+end
M apps/protocol/lib/lexical/protocol/requests.ex => apps/protocol/lib/lexical/protocol/requests.ex +6 -0
@@ 76,6 76,12 @@ defmodule Lexical.Protocol.Requests do
defrequest "workspace/executeCommand", Types.ExecuteCommand.Params
end
+ defmodule DocumentSymbols do
+ use Proto
+
+ defrequest "textDocument/documentSymbol", Types.Document.Symbol.Params
+ end
+
# Server -> Client requests
defmodule RegisterCapability do
M apps/protocol/lib/lexical/protocol/responses.ex => apps/protocol/lib/lexical/protocol/responses.ex +6 -0
@@ 50,6 50,12 @@ defmodule Lexical.Protocol.Responses do
defresponse optional(list_of(one_of([list_of(Types.Completion.Item), Types.Completion.List])))
end
+ defmodule DocumentSymbols do
+ use Proto
+
+ defresponse optional(list_of(Types.Document.Symbol))
+ end
+
defmodule Shutdown do
use Proto
# yeah, this is odd... it has no params
M apps/remote_control/lib/lexical/remote_control/api.ex => apps/remote_control/lib/lexical/remote_control/api.ex +4 -0
@@ 130,4 130,8 @@ defmodule Lexical.RemoteControl.Api do
def struct_definitions(%Project{} = project) do
RemoteControl.call(project, CodeIntelligence.Structs, :for_project, [])
end
+
+ def document_symbols(%Project{} = project, %Document{} = document) do
+ RemoteControl.call(project, CodeIntelligence.Symbols, :for_document, [document])
+ end
end
A apps/remote_control/lib/lexical/remote_control/code_intelligence/symbols.ex => apps/remote_control/lib/lexical/remote_control/code_intelligence/symbols.ex +70 -0
@@ 0,0 1,70 @@
+defmodule Lexical.RemoteControl.CodeIntelligence.Symbols do
+ alias Lexical.Document
+ alias Lexical.RemoteControl.CodeIntelligence.Symbols
+ alias Lexical.RemoteControl.Search.Indexer
+ alias Lexical.RemoteControl.Search.Indexer.Entry
+ alias Lexical.RemoteControl.Search.Indexer.Extractors
+
+ @block_types [
+ :ex_unit_describe,
+ :ex_unit_setup,
+ :ex_unit_setup_all,
+ :ex_unit_test,
+ :module,
+ :private_function,
+ :public_function
+ ]
+
+ @symbol_extractors [
+ Extractors.FunctionDefinition,
+ Extractors.Module,
+ Extractors.ModuleAttribute,
+ Extractors.StructDefinition,
+ Extractors.ExUnit
+ ]
+
+ def for_document(%Document{} = document) do
+ {:ok, entries} = Indexer.Source.index_document(document, @symbol_extractors)
+
+ definitions = Enum.filter(entries, &(&1.subtype == :definition))
+ to_symbols(document, definitions)
+ end
+
+ defp to_symbols(%Document{} = document, entries) do
+ entries_by_block_id = Enum.group_by(entries, & &1.block_id)
+ rebuild_structure(entries_by_block_id, document, :root)
+ end
+
+ defp rebuild_structure(entries_by_block_id, %Document{} = document, block_id) do
+ block_entries = Map.get(entries_by_block_id, block_id, [])
+
+ Enum.flat_map(block_entries, fn
+ %Entry{type: type, subtype: :definition} = entry when type in @block_types ->
+ result =
+ if Map.has_key?(entries_by_block_id, entry.id) do
+ children =
+ entries_by_block_id
+ |> rebuild_structure(document, entry.id)
+ |> Enum.sort_by(fn %Symbols.Document{} = symbol ->
+ start = symbol.range.start
+ {start.line, start.character}
+ end)
+
+ Symbols.Document.from(document, entry, children)
+ else
+ Symbols.Document.from(document, entry)
+ end
+
+ case result do
+ {:ok, symbol} -> [symbol]
+ _ -> []
+ end
+
+ %Entry{} = entry ->
+ case Symbols.Document.from(document, entry) do
+ {:ok, symbol} -> [symbol]
+ _ -> []
+ end
+ end)
+ end
+end
A apps/remote_control/lib/lexical/remote_control/code_intelligence/symbols/document.ex => apps/remote_control/lib/lexical/remote_control/code_intelligence/symbols/document.ex +89 -0
@@ 0,0 1,89 @@
+defmodule Lexical.RemoteControl.CodeIntelligence.Symbols.Document do
+ alias Lexical.Document
+ alias Lexical.Formats
+ alias Lexical.RemoteControl.Search.Indexer.Entry
+
+ defstruct [:name, :type, :range, :detail_range, :detail, children: []]
+
+ def from(%Document{} = document, %Entry{} = entry, children \\ []) do
+ case name_and_type(entry.type, entry, document) do
+ {name, type} ->
+ range = entry.block_range || entry.range
+
+ {:ok,
+ %__MODULE__{
+ name: name,
+ type: type,
+ range: range,
+ detail_range: entry.range,
+ children: children
+ }}
+
+ _ ->
+ :error
+ end
+ end
+
+ @def_regex ~r/def\w*\s+/
+ @do_regex ~r/\s*do\s*$/
+
+ defp name_and_type(function, %Entry{} = entry, %Document{} = document)
+ when function in [:public_function, :private_function] do
+ fragment = Document.fragment(document, entry.range.start, entry.range.end)
+
+ name =
+ fragment
+ |> String.replace(@def_regex, "")
+ |> String.replace(@do_regex, "")
+
+ {name, function}
+ end
+
+ @ignored_attributes ~w[spec doc moduledoc derive impl tag]
+ @type_name_regex ~r/@type\s+[^\s]+/
+
+ defp name_and_type(:module_attribute, %Entry{} = entry, document) do
+ case String.split(entry.subject, "@") do
+ [_, name] when name in @ignored_attributes ->
+ nil
+
+ [_, "type"] ->
+ type_text = Document.fragment(document, entry.range.start, entry.range.end)
+
+ name =
+ case Regex.scan(@type_name_regex, type_text) do
+ [[match]] -> match
+ _ -> "@type ??"
+ end
+
+ {name, :type}
+
+ [_, name] ->
+ {"@#{name}", :module_attribute}
+ end
+ end
+
+ defp name_and_type(ex_unit, %Entry{} = entry, document)
+ when ex_unit in [:ex_unit_describe, :ex_unit_setup, :ex_unit_test] do
+ name =
+ document
+ |> Document.fragment(entry.range.start, entry.range.end)
+ |> String.trim()
+ |> String.replace(@do_regex, "")
+
+ {name, ex_unit}
+ end
+
+ defp name_and_type(:struct, %Entry{} = entry, _document) do
+ module_name = Formats.module(entry.subject)
+ {"%#{module_name}{}", :struct}
+ end
+
+ defp name_and_type(type, %Entry{subject: name}, _document) when is_atom(name) do
+ {Formats.module(name), type}
+ end
+
+ defp name_and_type(type, %Entry{} = entry, _document) do
+ {to_string(entry.subject), type}
+ end
+end
M apps/remote_control/lib/lexical/remote_control/search/indexer/entry.ex => apps/remote_control/lib/lexical/remote_control/search/indexer/entry.ex +23 -10
@@ 10,6 10,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Entry do
:application,
:id,
:block_id,
+ :block_range,
:path,
:range,
:subject,
@@ 21,6 22,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Entry do
application: module(),
subject: subject(),
block_id: block_id(),
+ block_range: Lexical.Document.Range.t() | nil,
path: Path.t(),
range: Lexical.Document.Range.t(),
subtype: entry_subtype(),
@@ 55,16 57,27 @@ defmodule Lexical.RemoteControl.Search.Indexer.Entry do
new(path, Identifier.next_global!(), block.id, subject, type, :definition, range, application)
end
- def block_definition(path, %Block{} = block, subject, type, range, application) do
- definition(
- path,
- block.id,
- block.parent_id,
- subject,
- type,
- range,
- application
- )
+ def block_definition(
+ path,
+ %Block{} = block,
+ subject,
+ type,
+ block_range,
+ detail_range,
+ application
+ ) do
+ definition =
+ definition(
+ path,
+ block.id,
+ block.parent_id,
+ subject,
+ type,
+ detail_range,
+ application
+ )
+
+ %__MODULE__{definition | block_range: block_range}
end
defp definition(path, id, block_id, subject, type, range, application) do
A => +127 -0
@@ 0,0 1,127 @@
defmodule Lexical.RemoteControl.Search.Indexer.Extractors.ExUnit do
alias Lexical.Ast
alias Lexical.Ast.Analysis
alias Lexical.Document.Position
alias Lexical.Document.Range
alias Lexical.Formats
alias Lexical.RemoteControl.Analyzer
alias Lexical.RemoteControl.Search.Indexer.Entry
alias Lexical.RemoteControl.Search.Indexer.Metadata
alias Lexical.RemoteControl.Search.Indexer.Source.Reducer
require Logger
# setup block i.e. setup do... or setup arg do...
def extract({setup_fn, _, args} = setup, %Reducer{} = reducer)
when setup_fn in [:setup, :setup_all] and length(args) > 0 do
{:ok, module} = Analyzer.current_module(reducer.analysis, Reducer.position(reducer))
arity = arity_for(args)
subject = Formats.mfa(module, setup_fn, arity)
setup_type = :"ex_unit_#{setup_fn}"
entry =
case Metadata.location(setup) do
{:block, _, _, _} ->
block_entry(reducer, setup, setup_type, subject)
{:expression, _} ->
expression_entry(reducer, setup, setup_type, subject)
end
{:ok, entry}
end
# Test block test "test name" do ... or test "test name", arg do
def extract({:test, _, [{_, _, [test_name]} | _] = args} = test, %Reducer{} = reducer) do
{:ok, module} = Analyzer.current_module(reducer.analysis, Reducer.position(reducer))
arity = arity_for(args)
module_name = Formats.module(module)
subject = "#{module_name}.[\"#{test_name}\"]/#{arity}"
entry =
case Metadata.location(test) do
{:block, _, _, _} ->
# a test with a body
block_entry(reducer, test, :ex_unit_test, subject)
{:expression, _} ->
# a pending test
expression_entry(reducer, test, :ex_unit_test, subject)
end
{:ok, entry}
end
# describe blocks
def extract({:describe, _, [{_, _, [describe_name]} | _] = args} = test, %Reducer{} = reducer) do
{:ok, module} = Analyzer.current_module(reducer.analysis, Reducer.position(reducer))
arity = arity_for(args)
module_name = Formats.module(module)
subject = "#{module_name}[\"#{describe_name}\"]/#{arity}"
entry = block_entry(reducer, test, :ex_unit_describe, subject)
{:ok, entry}
end
def extract(_ign, _) do
:ignored
end
defp expression_entry(%Reducer{} = reducer, ast, type, subject) do
path = reducer.analysis.document.path
block = Reducer.current_block(reducer)
{:ok, module} = Analyzer.current_module(reducer.analysis, Reducer.position(reducer))
app = Application.get_application(module)
detail_range = detail_range(reducer.analysis, ast)
Entry.definition(path, block, subject, type, detail_range, app)
end
defp block_entry(%Reducer{} = reducer, ast, type, subject) do
path = reducer.analysis.document.path
block = Reducer.current_block(reducer)
{:ok, module} = Analyzer.current_module(reducer.analysis, Reducer.position(reducer))
app = Application.get_application(module)
detail_range = detail_range(reducer.analysis, ast)
block_range = block_range(reducer.analysis, ast)
Entry.block_definition(path, block, subject, type, block_range, detail_range, app)
end
defp block_range(%Analysis{} = analysis, ast) do
case Ast.Range.fetch(ast, analysis.document) do
{:ok, range} -> range
_ -> nil
end
end
defp detail_range(%Analysis{} = analysis, ast) do
case Metadata.location(ast) do
{:block, {start_line, start_column}, {do_line, do_column}, _} ->
Range.new(
Position.new(analysis.document, start_line, start_column),
Position.new(analysis.document, do_line, do_column + 2)
)
{:expression, {start_line, start_column}} ->
%{end: [line: end_line, column: end_column]} = Sourceror.get_range(ast)
Range.new(
Position.new(analysis.document, start_line, start_column),
Position.new(analysis.document, end_line, end_column)
)
end
end
defp arity_for([{:__block__, _meta, labels}]) do
length(labels)
end
defp arity_for(args) when is_list(args) do
length(args)
end
defp arity_for(_), do: 0
end
M => +22 -4
@@ 1,5 1,6 @@
defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinition do
alias Lexical.Ast.Analysis
alias Lexical.Ast.Range
alias Lexical.Document.Position
alias Lexical.Document.Range
alias Lexical.RemoteControl
@@ 13,9 14,9 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinition do
def extract({definition, metadata, [{fn_name, _, args}, body]}, %Reducer{} = reducer)
when is_atom(fn_name) and definition in @function_definitions do
range = get_definition_range(reducer.analysis, metadata, body)
detail_range = detail_range(reducer.analysis, metadata, body)
{:ok, module} = RemoteControl.Analyzer.current_module(reducer.analysis, range.start)
{:ok, module} = RemoteControl.Analyzer.current_module(reducer.analysis, detail_range.start)
arity =
case args do
@@ 36,8 37,18 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinition do
%Block{} = block = Reducer.current_block(reducer)
path = reducer.analysis.document.path
block_range = block_range(reducer.analysis, ast)
entry =
Entry.block_definition(path, block, mfa, type, range, Application.get_application(module))
Entry.block_definition(
path,
block,
mfa,
type,
block_range,
detail_range,
Application.get_application(module)
)
{:ok, entry, [args, body]}
end
@@ 46,7 57,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinition do
:ignored
end
defp get_definition_range(%Analysis{} = analysis, def_metadata, block) do
defp detail_range(%Analysis{} = analysis, def_metadata, block) do
{line, column} = Metadata.position(def_metadata)
{do_line, do_column} =
@@ 66,4 77,11 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinition do
do_pos = Position.new(analysis.document, do_line, do_column)
Range.new(start_pos, do_pos)
end
defp block_range(%Analysis{} = analysis, def_ast) do
case Lexical.Ast.Range.fetch(def_ast, analysis.document) do
{:ok, range} -> range
_ -> nil
end
end
end
M => +9 -1
@@ 19,7 19,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.Module do
# extract a module definition
def extract(
{:defmodule, defmodule_meta,
[{:__aliases__, module_name_meta, module_name}, module_block]},
[{:__aliases__, module_name_meta, module_name}, module_block]} = defmodule_ast,
%Reducer{} = reducer
) do
%Block{} = block = Reducer.current_block(reducer)
@@ 35,6 35,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.Module do
block,
Subject.module(aliased_module),
:module,
block_range(reducer.analysis.document, defmodule_ast),
range,
Application.get_application(aliased_module)
)
@@ 196,4 197,11 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.Module do
Position.new(document, line, column + module_length)
)
end
defp block_range(document, ast) do
case Ast.Range.fetch(ast, document) do
{:ok, range} -> range
_ -> nil
end
end
end
M => +11 -0
@@ 69,6 69,10 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.Variable do
:ignored
end
def extract({:@, _, _}, %Reducer{}) do
{:ok, nil, nil}
end
# Generic variable reference
def extract({var_name, _, _} = ast, %Reducer{} = reducer) when is_atom(var_name) do
case extract_reference(ast, reducer, get_current_app(reducer)) do
@@ 103,6 107,9 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.Variable do
{entries, ast} when is_list(entries) ->
{ast, entries ++ acc}
{_, ast} ->
{ast, acc}
_ ->
{ast, acc}
end
@@ 125,6 132,10 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.Variable do
{reference, nil}
end
defp extract_definition({:@, _, _}, %Reducer{}, _current_app) do
{nil, []}
end
# when clauses actually contain parameters and references
defp extract_definition({:when, _, when_args}, %Reducer{} = reducer, _current_app) do
{definitions, references} =
M apps/remote_control/lib/lexical/remote_control/search/indexer/source.ex => apps/remote_control/lib/lexical/remote_control/search/indexer/source.ex +5 -0
@@ 8,6 8,11 @@ defmodule Lexical.RemoteControl.Search.Indexer.Source do
def index(path, source, extractors \\ nil) do
path
|> Document.new(source, 1)
+ |> index_document(extractors)
+ end
+
+ def index_document(%Document{} = document, extractors \\ nil) do
+ document
|> Ast.analyze()
|> Indexer.Quoted.index(extractors)
end
A apps/remote_control/test/lexical/remote_control/code_intelligence/symbols_test.exs => apps/remote_control/test/lexical/remote_control/code_intelligence/symbols_test.exs +271 -0
@@ 0,0 1,271 @@
+defmodule Lexical.RemoteControl.CodeIntelligence.SymbolsTest do
+ alias Lexical.Document
+ alias Lexical.RemoteControl.CodeIntelligence.Symbols
+ use ExUnit.Case
+
+ import Lexical.Test.CodeSigil
+ import Lexical.Test.RangeSupport
+
+ def document_symbols(code) do
+ doc = Document.new("file:///file.ex", code, 1)
+ symbols = Symbols.for_document(doc)
+ {symbols, doc}
+ end
+
+ test "a top level module is found" do
+ {[%Symbols.Document{} = module], doc} =
+ ~q[
+ defmodule MyModule do
+ end
+ ]
+ |> document_symbols()
+
+ assert decorate(doc, module.detail_range) =~ "defmodule «MyModule» do"
+ assert module.name == "MyModule"
+ assert module.type == :module
+ end
+
+ test "multiple top-level modules are found" do
+ {[first, second], doc} =
+ ~q[
+ defmodule First do
+ end
+
+ defmodule Second do
+ end
+ ]
+ |> document_symbols()
+
+ assert decorate(doc, first.detail_range) =~ "defmodule «First» do"
+ assert first.name == "First"
+ assert first.type == :module
+
+ assert decorate(doc, second.detail_range) =~ "defmodule «Second» do"
+ assert second.name == "Second"
+ assert second.type == :module
+ end
+
+ test "nested modules are found" do
+ {[outer], doc} =
+ ~q[
+ defmodule Outer do
+ defmodule Inner do
+ defmodule Innerinner do
+ end
+ end
+ end
+ ]
+ |> document_symbols()
+
+ assert decorate(doc, outer.detail_range) =~ "defmodule «Outer» do"
+ assert outer.name == "Outer"
+ assert outer.type == :module
+
+ assert [inner] = outer.children
+ assert decorate(doc, inner.detail_range) =~ "defmodule «Inner» do"
+ assert inner.name == "Outer.Inner"
+ assert inner.type == :module
+
+ assert [inner_inner] = inner.children
+ assert decorate(doc, inner_inner.detail_range) =~ "defmodule «Innerinner» do"
+ assert inner_inner.name == "Outer.Inner.Innerinner"
+ assert inner_inner.type == :module
+ end
+
+ test "module attribute definitions are found" do
+ {[module], doc} =
+ ~q[
+ defmodule Module do
+ @first 3
+ @second 4
+ end
+ ]
+ |> document_symbols()
+
+ assert [first, second] = module.children
+ assert decorate(doc, first.detail_range) =~ " «@first 3»"
+ assert first.name == "@first"
+
+ assert decorate(doc, second.detail_range) =~ " «@second 4»"
+ assert second.name == "@second"
+ end
+
+ test "module attribute references are skipped" do
+ {[module], _doc} =
+ ~q[
+ defmodule Parent do
+ @attr 3
+ def my_fun() do
+ @attr
+ end
+ end
+
+ ]
+ |> document_symbols()
+
+ [_attr_def, function_def] = module.children
+ [] = function_def.children
+ end
+
+ test "public function definitions are found" do
+ {[module], doc} =
+ ~q[
+ defmodule Module do
+ def my_fn do
+ end
+ end
+ ]
+ |> document_symbols()
+
+ assert [function] = module.children
+ assert decorate(doc, function.detail_range) =~ " «def my_fn do»"
+ end
+
+ test "private function definitions are found" do
+ {[module], doc} =
+ ~q[
+ defmodule Module do
+ defp my_fn do
+ end
+ end
+ ]
+ |> document_symbols()
+
+ assert [function] = module.children
+ assert decorate(doc, function.detail_range) =~ " «defp my_fn do»"
+ assert function.name == "my_fn"
+ end
+
+ test "struct definitions are found" do
+ {[module], doc} =
+ ~q{
+ defmodule Module do
+ defstruct [:name, :value]
+ end
+ }
+ |> document_symbols()
+
+ assert [struct] = module.children
+ assert decorate(doc, struct.detail_range) =~ " «defstruct [:name, :value]»"
+ assert struct.name == "%Module{}"
+ assert struct.type == :struct
+ end
+
+ test "struct references are skippedd" do
+ assert {[], _doc} =
+ ~q[%OtherModule{}]
+ |> document_symbols()
+ end
+
+ test "variable definitions are skipped" do
+ {[module], _doc} =
+ ~q[
+ defmodule Module do
+ defp my_fn do
+ my_var = 3
+ end
+ end
+ ]
+ |> document_symbols()
+
+ assert [function] = module.children
+ assert [] = function.children
+ end
+
+ test "variable references are skipped" do
+ {[module], _doc} =
+ ~q[
+ defmodule Module do
+ defp my_fn do
+ my_var = 3
+ my_var
+ end
+ end
+ ]
+ |> document_symbols()
+
+ assert [function] = module.children
+ assert [] = function.children
+ end
+
+ test "guards shown in the name" do
+ {[module], doc} =
+ ~q[
+ defmodule Module do
+ def my_fun(x) when x > 0 do
+ end
+ end
+ ]
+ |> document_symbols()
+
+ [fun] = module.children
+ assert decorate(doc, fun.detail_range) =~ " «def my_fun(x) when x > 0 do»"
+ assert fun.type == :public_function
+ assert fun.name == "my_fun(x) when x > 0"
+ assert [] == fun.children
+ end
+
+ test "types show only their name" do
+ {[type], doc} =
+ ~q[
+ @type something :: :ok
+ ]
+ |> document_symbols()
+
+ assert decorate(doc, type.detail_range) =~ "«@type something :: :ok»"
+ assert type.name == "@type something"
+ assert type.type == :type
+ end
+
+ test "specs are ignored" do
+ {[], _doc} =
+ ~q[
+ @spec my_fun(integer()) :: :ok
+ ]
+ |> document_symbols()
+ end
+
+ test "docs are ignored" do
+ assert {[], _doc} =
+ ~q[
+ @doc """
+ Hello
+ """
+ ]
+ |> document_symbols()
+ end
+
+ test "moduledocs are ignored" do
+ assert {[], _doc} =
+ ~q[
+ @moduledoc """
+ Hello
+ """
+ ]
+ |> document_symbols()
+ end
+
+ test "derives are ignored" do
+ assert {[], _doc} =
+ ~q[
+ @derive {Something, other}
+ ]
+ |> document_symbols()
+ end
+
+ test "impl declarations are ignored" do
+ assert {[], _doc} =
+ ~q[
+ @impl GenServer
+ ]
+ |> document_symbols()
+ end
+
+ test "tags ignored" do
+ assert {[], _doc} =
+ ~q[
+ @tag :skip
+ ]
+ |> document_symbols()
+ end
+end
A => +323 -0
@@ 0,0 1,323 @@
defmodule Lexical.RemoteControl.Search.Indexer.Extractors.ExUnitTest do
alias Lexical.RemoteControl.Search.Indexer.Extractors
use Lexical.Test.ExtractorCase
import Lexical.Test.RangeSupport
@test_types [
:ex_unit_setup,
:ex_unit_setup_all,
:ex_unit_test,
:ex_unit_describe
]
def index_definitions(source) do
do_index(source, fn entry -> entry.type in @test_types and entry.subtype == :definition end, [
Extractors.ExUnit
])
end
def index_with_structure(source) do
do_index(source, fn entry -> entry.type != :metadata end, [
Extractors.ExUnit,
Extractors.Module
])
end
describe "finds setup" do
test "in blocks without an argument" do
{:ok, [setup], doc} =
~q[
defmodule SomeTest do
setup do
:ok
end
end
]
|> index_definitions()
assert setup.type == :ex_unit_setup
assert setup.subject == "SomeTest.setup/1"
assert decorate(doc, setup.range) =~ " «setup do»"
assert decorate(doc, setup.block_range) =~ " «setup do\n :ok\n end»"
end
test "in blocks with an argument" do
{:ok, [setup], doc} =
~q[
defmodule SomeTest do
setup arg do
:ok
end
end
]
|> index_definitions()
assert setup.type == :ex_unit_setup
assert setup.subject == "SomeTest.setup/2"
assert decorate(doc, setup.range) =~ " «setup arg do»"
assert decorate(doc, setup.block_range) =~ " «setup arg do\n :ok\n end»"
end
test "as an atom" do
{:ok, [setup], doc} =
~q[
defmodule SomeTest do
setup :other_function
end
]
|> index_definitions()
assert setup.type == :ex_unit_setup
assert setup.subject == "SomeTest.setup/1"
refute setup.block_range
assert decorate(doc, setup.range) =~ " «setup :other_function»"
end
test "as a list of atoms" do
{:ok, [setup], doc} =
~q{
defmodule SomeTest do
setup [:other_function, :second_function]
end
}
|> index_definitions()
assert setup.type == :ex_unit_setup
assert setup.subject == "SomeTest.setup/1"
refute setup.block_range
assert decorate(doc, setup.range) =~ " «setup [:other_function, :second_function]»"
end
test "as a MF tuple" do
{:ok, [setup], doc} =
~q[
defmodule SomeTest do
setup {OtherModule, :setup}
end
]
|> index_definitions()
assert setup.type == :ex_unit_setup
assert setup.subject == "SomeTest.setup/1"
refute setup.block_range
assert decorate(doc, setup.range) =~ " «setup {OtherModule, :setup}»"
end
test "unless setup is a variable" do
{:ok, [test], _doc} =
~q[
defmodule SomeTest do
test "something" do
setup = 3
setup
end
end
]
|> index_definitions()
assert test.type == :ex_unit_test
end
end
describe "finds setup_all" do
test "as a block without an argument" do
{:ok, [setup], doc} =
~q[
defmodule SomeTest do
setup_all do
:ok
end
end
]
|> index_definitions()
assert setup.type == :ex_unit_setup_all
assert setup.subject == "SomeTest.setup_all/1"
assert decorate(doc, setup.range) =~ " «setup_all do»"
assert decorate(doc, setup.block_range) =~ " «setup_all do\n :ok\n end"
end
test "as a block with an argument" do
{:ok, [setup], doc} =
~q[
defmodule SomeTest do
setup_all arg do
:ok
end
end
]
|> index_definitions()
assert setup.type == :ex_unit_setup_all
assert setup.subject == "SomeTest.setup_all/2"
assert decorate(doc, setup.range) =~ " «setup_all arg do»"
assert decorate(doc, setup.block_range) =~ " «setup_all arg do\n :ok\n end"
end
test "as an atom" do
{:ok, [setup], doc} =
~q[
defmodule SomeTest do
setup_all :other_function
end
]
|> index_definitions()
assert setup.type == :ex_unit_setup_all
assert setup.subject == "SomeTest.setup_all/1"
refute setup.block_range
assert decorate(doc, setup.range) =~ " «setup_all :other_function»"
end
test "as a list of atoms" do
{:ok, [setup], doc} =
~q{
defmodule SomeTest do
setup_all [:other_function, :second_function]
end
}
|> index_definitions()
assert setup.type == :ex_unit_setup_all
assert setup.subject == "SomeTest.setup_all/1"
refute setup.block_range
assert decorate(doc, setup.range) =~ " «setup_all [:other_function, :second_function]»"
end
test "as a MF tuple" do
{:ok, [setup], doc} =
~q[
defmodule SomeTest do
setup_all {OtherModule, :setup}
end
]
|> index_definitions()
assert setup.type == :ex_unit_setup_all
assert setup.subject == "SomeTest.setup_all/1"
refute setup.block_range
assert decorate(doc, setup.range) =~ " «setup_all {OtherModule, :setup}»"
end
end
describe "finds describe blocks" do
test "with an empty block" do
{:ok, [describe], doc} =
~q[
defmodule SomeTest do
describe "something" do
end
end
]
|> index_definitions()
assert describe.type == :ex_unit_describe
assert describe.subtype == :definition
assert decorate(doc, describe.range) =~ " «describe \"something\" do»"
assert decorate(doc, describe.block_range) =~ " «describe \"something\" do\n end»"
end
test "with tests" do
{:ok, [describe, _test], doc} =
~q[
defmodule SomeTest do
describe "something" do
test "something"
end
end
]
|> index_definitions()
assert describe.type == :ex_unit_describe
assert describe.subtype == :definition
assert decorate(doc, describe.range) =~ " «describe \"something\" do»"
assert decorate(doc, describe.block_range) =~
" «describe \"something\" do\n test \"something\"\n end»"
end
end
describe "finds tests" do
test "when pending" do
{:ok, [test], doc} =
~q[
defmodule SomeTest do
test "my test"
end
]
|> index_definitions()
assert test.type == :ex_unit_test
assert test.subject == "SomeTest.[\"my test\"]/1"
refute test.block_range
assert decorate(doc, test.range) =~ ~s[ «test "my test"»]
end
test "when they only have a block" do
{:ok, [test], doc} =
~q[
defmodule SomeTest do
test "my test" do
end
end
]
|> index_definitions()
assert test.type == :ex_unit_test
assert test.subject == "SomeTest.[\"my test\"]/2"
assert decorate(doc, test.range) =~ ~s[ «test "my test" do»]
assert decorate(doc, test.block_range) =~ ~s[ «test "my test" do\n end»]
end
test "when they have a block and a context" do
{:ok, [test], doc} =
~q[
defmodule SomeTest do
test "my test", context do
end
end
]
|> index_definitions()
assert test.type == :ex_unit_test
assert test.subject =~ "SomeTest.[\"my test\"]/3"
expected_detail = " «test \"my test\", context do»"
assert decorate(doc, test.range) =~ expected_detail
expected_block = " «test \"my test\", context do\n end»"
assert decorate(doc, test.block_range) =~ expected_block
end
end
describe "block structure" do
test "describe contains tests" do
{:ok, [module, describe, test], _} =
~q[
defmodule SomeTexst do
describe "outer" do
test "my test", context do
end
end
end
]
|> index_with_structure()
assert module.type == :module
assert module.block_id == :root
assert describe.type == :ex_unit_describe
assert describe.block_id == module.id
assert test.type == :ex_unit_test
assert test.block_id == describe.id
end
end
end
M => +17 -5
@@ 43,6 43,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinitionTest
assert zero_arity.subtype == :definition
assert zero_arity.subject == "Parent.zero_arity/0"
assert "def zero_arity, do" == extract(code, zero_arity.range)
assert "def zero_arity, do: true" == extract(code, zero_arity.block_range)
end
test "finds zero arity public functions (with parens)" do
@@ 59,6 60,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinitionTest
assert zero_arity.subtype == :definition
assert zero_arity.subject == "Parent.zero_arity/0"
assert "def zero_arity() do" == extract(code, zero_arity.range)
assert "def zero_arity() do\nend" == extract(code, zero_arity.block_range)
end
test "finds one arity public function" do
@@ 76,6 78,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinitionTest
assert one_arity.subtype == :definition
assert one_arity.subject == "Parent.one_arity/1"
assert "def one_arity(a) do" == extract(code, one_arity.range)
assert "def one_arity(a) do\na + 1\nend" == extract(code, one_arity.block_range)
end
test "finds multi arity public function" do
@@ 93,6 96,9 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinitionTest
assert multi_arity.subtype == :definition
assert multi_arity.subject == "Parent.multi_arity/4"
assert "def multi_arity(a, b, c, d) do" == extract(code, multi_arity.range)
assert "def multi_arity(a, b, c, d) do\n{a, b, c, d}\nend" ==
extract(code, multi_arity.block_range)
end
test "finds multi-line function definitions" do
@@ 165,6 171,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinitionTest
assert zero_arity.subtype == :definition
assert zero_arity.subject == "Parent.zero_arity/0"
assert "defp zero_arity, do" == extract(code, zero_arity.range)
assert "defp zero_arity, do: true" == extract(code, zero_arity.block_range)
end
test "finds zero arity one-line private functions (with parens)" do
@@ 180,6 187,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinitionTest
assert zero_arity.subtype == :definition
assert zero_arity.subject == "Parent.zero_arity/0"
assert "defp zero_arity(), do" == extract(code, zero_arity.range)
assert "defp zero_arity(), do: true" == extract(code, zero_arity.block_range)
end
test "finds zero arity private functions (no parens)" do
@@ 196,6 204,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinitionTest
assert zero_arity.subtype == :definition
assert zero_arity.subject == "Parent.zero_arity/0"
assert "defp zero_arity do" == extract(code, zero_arity.range)
assert "defp zero_arity do\nend" == extract(code, zero_arity.block_range)
end
test "finds zero arity private functions (with parens)" do
@@ 212,6 221,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinitionTest
assert zero_arity.subtype == :definition
assert zero_arity.subject == "Parent.zero_arity/0"
assert "defp zero_arity() do" == extract(code, zero_arity.range)
assert "defp zero_arity() do\nend" == extract(code, zero_arity.block_range)
end
test "finds one arity one-line private functions" do
@@ 227,6 237,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinitionTest
assert one_arity.subtype == :definition
assert one_arity.subject == "Parent.one_arity/1"
assert "defp one_arity(a), do" == extract(code, one_arity.range)
assert "defp one_arity(a), do: a + 1" == extract(code, one_arity.block_range)
end
test "finds one arity private functions" do
@@ 253,12 264,13 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.FunctionDefinitionTest
]
|> in_a_module()
{:ok, [one_arity], _} = index(code)
{:ok, [multi_arity], _} = index(code)
assert one_arity.type == :private_function
assert one_arity.subtype == :definition
assert one_arity.subject == "Parent.multi_arity/3"
assert "defp multi_arity(a, b, c), do" == extract(code, one_arity.range)
assert multi_arity.type == :private_function
assert multi_arity.subtype == :definition
assert multi_arity.subject == "Parent.multi_arity/3"
assert "defp multi_arity(a, b, c), do" == extract(code, multi_arity.range)
assert "defp multi_arity(a, b, c), do: {a, b, c}" = extract(code, multi_arity.block_range)
end
test "finds multi arity private functions" do
M apps/server/lib/lexical/server/provider/handlers.ex => apps/server/lib/lexical/server/provider/handlers.ex +4 -0
@@ 1,3 1,4 @@
+# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
defmodule Lexical.Server.Provider.Handlers do
alias Lexical.Protocol.Requests
alias Lexical.Server.Provider.Handlers
@@ 28,6 29,9 @@ defmodule Lexical.Server.Provider.Handlers do
%Requests.ExecuteCommand{} ->
{:ok, Handlers.Commands}
+ %Requests.DocumentSymbols{} ->
+ {:ok, Handlers.DocumentSymbols}
+
%request_module{} ->
{:error, {:unhandled, request_module}}
end
A apps/server/lib/lexical/server/provider/handlers/document_symbols.ex => apps/server/lib/lexical/server/provider/handlers/document_symbols.ex +58 -0
@@ 0,0 1,58 @@
+defmodule Lexical.Server.Provider.Handlers.DocumentSymbols do
+ alias Lexical.Document
+ alias Lexical.Protocol.Requests.DocumentSymbols
+ alias Lexical.Protocol.Responses
+ alias Lexical.Protocol.Types.Document.Symbol
+ alias Lexical.Protocol.Types.Symbol.Kind, as: SymbolKind
+ alias Lexical.RemoteControl.Api
+ alias Lexical.RemoteControl.CodeIntelligence.Symbols
+ alias Lexical.Server.Provider.Env
+
+ require SymbolKind
+
+ def handle(%DocumentSymbols{} = request, %Env{} = env) do
+ symbols =
+ env.project
+ |> Api.document_symbols(request.document)
+ |> Enum.map(&to_response(&1, request.document))
+
+ response = Responses.DocumentSymbols.new(request.id, symbols)
+
+ {:reply, response}
+ end
+
+ def to_response(%Symbols.Document{} = root, %Document{} = document) do
+ children =
+ case root.children do
+ list when is_list(list) ->
+ Enum.map(list, &to_response(&1, document))
+
+ _ ->
+ nil
+ end
+
+ Symbol.new(
+ children: children,
+ detail: root.detail,
+ kind: to_kind(root.type),
+ name: root.name,
+ range: root.range,
+ selection_range: root.detail_range
+ )
+ end
+
+ defp to_kind(:struct), do: :struct
+ defp to_kind(:module), do: :module
+ defp to_kind(:variable), do: :variable
+ defp to_kind(:public_function), do: :function
+ defp to_kind(:private_function), do: :function
+ defp to_kind(:module_attribute), do: :constant
+ defp to_kind(:ex_unit_test), do: :method
+ defp to_kind(:ex_unit_describe), do: :method
+ defp to_kind(:ex_unit_setup), do: :method
+ defp to_kind(:ex_unit_setup_all), do: :method
+ defp to_kind(:type), do: :type_parameter
+ defp to_kind(:spec), do: :interface
+ defp to_kind(:file), do: :file
+ defp to_kind(_), do: :string
+end
M apps/server/lib/lexical/server/state.ex => apps/server/lib/lexical/server/state.ex +1 -0
@@ 285,6 285,7 @@ defmodule Lexical.Server.State do
completion_provider: completion_options,
definition_provider: true,
document_formatting_provider: true,
+ document_symbol_provider: true,
execute_command_provider: command_options,
hover_provider: true,
references_provider: true,
M mix.lock => mix.lock +1 -1
@@ 27,7 27,7 @@
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"snowflake": {:hex, :snowflake, "1.0.4", "8433b4e04fbed19272c55e1b7de0f7a1ee1230b3ae31a813b616fd6ef279e87a", [:mix], [], "hexpm", "badb07ebb089a5cff737738297513db3962760b10fe2b158ae3bebf0b4d5be13"},
- "sourceror": {:hex, :sourceror, "1.0.1", "ec2c41726d181adce888ac94b3f33b359a811b46e019c084509e02c70042e424", [:mix], [], "hexpm", "28225464ffd68bda1843c974f3ff7ccef35e29be09a65dfe8e3df3f7e3600c57"},
+ "sourceror": {:hex, :sourceror, "1.0.2", "c5e86fdc14881f797749d1fe5df017ca66727a8146e7ee3e736605a3df78f3e6", [:mix], [], "hexpm", "832335e87d0913658f129d58b2a7dc0490ddd4487b02de6d85bca0169ec2bd79"},
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},