diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 9081ff7a..24c98f67 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -21,6 +21,7 @@ defmodule NextLS do Initialize, Shutdown, TextDocumentDocumentSymbol, + TextDocumentDefinition, TextDocumentFormatting, WorkspaceSymbol } @@ -45,6 +46,7 @@ defmodule NextLS do alias NextLS.DiagnosticCache alias NextLS.Runtime alias NextLS.SymbolTable + alias NextLS.Definition def start_link(args) do {args, opts} = @@ -97,12 +99,43 @@ defmodule NextLS do }, document_formatting_provider: true, workspace_symbol_provider: true, - document_symbol_provider: true + document_symbol_provider: true, + definition_provider: true }, server_info: %{name: "NextLS"} }, assign(lsp, root_uri: root_uri)} end + def handle_request(%TextDocumentDefinition{params: %{text_document: %{uri: uri}, position: position}}, lsp) do + result = + case Definition.fetch( + URI.parse(uri).path, + {position.line + 1, position.character + 1}, + :symbol_table, + :reference_table + ) do + nil -> + nil + + [{file, line, column} | _] -> + %Location{ + uri: "file://#{file}", + range: %Range{ + start: %Position{ + line: line - 1, + character: column - 1 + }, + end: %Position{ + line: line - 1, + character: column - 1 + } + } + } + end + + {:reply, result, lsp} + end + def handle_request(%TextDocumentDocumentSymbol{params: %{text_document: %{uri: uri}}}, lsp) do symbols = try do @@ -343,6 +376,12 @@ defmodule NextLS do {:noreply, lsp} end + def handle_info({{:tracer, :local_function}, payload}, lsp) do + SymbolTable.put_reference(lsp.assigns.symbol_table, payload) + GenLSP.log(lsp, "[NextLS] Updated the reference table!") + {:noreply, lsp} + end + def handle_info(:publish, lsp) do GenLSP.log(lsp, "[NextLS] Compiled!") diff --git a/lib/next_ls/definition.ex b/lib/next_ls/definition.ex new file mode 100644 index 00000000..a0e8cced --- /dev/null +++ b/lib/next_ls/definition.ex @@ -0,0 +1,32 @@ +defmodule NextLS.Definition do + def fetch(file, {line, col}, dets_symbol_table, dets_ref_table) do + ref = + :dets.select( + dets_ref_table, + [ + {{{:"$1", {{:"$2", :"$3"}, {:"$4", :"$5"}}}, :"$6"}, + [ + {:andalso, + {:andalso, {:andalso, {:andalso, {:==, :"$1", file}, {:"=<", :"$2", line}}, {:"=<", :"$3", col}}, + {:"=<", line, :"$4"}}, {:"=<", col, :"$5"}} + ], [:"$6"]} + ] + ) + + :dets.traverse(dets_symbol_table, fn x -> {:continue, x} end) + + case ref do + [ref] -> + :dets.select( + dets_symbol_table, + [ + {{:_, %{line: :"$3", name: :"$2", module: :"$1", col: :"$4", file: :"$5"}}, + [{:andalso, {:==, :"$1", ref.module}, {:==, :"$2", ref.func}}], [{{:"$5", :"$3", :"$4"}}]} + ] + ) + + _ -> + nil + end + end +end diff --git a/lib/next_ls/symbol_table.ex b/lib/next_ls/symbol_table.ex index 8fc4a95d..a5cd9355 100644 --- a/lib/next_ls/symbol_table.ex +++ b/lib/next_ls/symbol_table.ex @@ -22,6 +22,9 @@ defmodule NextLS.SymbolTable do @spec put_symbols(pid() | atom(), list(tuple())) :: :ok def put_symbols(server, symbols), do: GenServer.cast(server, {:put_symbols, symbols}) + @spec put_reference(pid() | atom(), map()) :: :ok + def put_reference(server, reference), do: GenServer.cast(server, {:put_reference, reference}) + @spec symbols(pid() | atom()) :: list(struct()) def symbols(server), do: GenServer.call(server, :symbols) @@ -42,7 +45,13 @@ defmodule NextLS.SymbolTable do type: :duplicate_bag ) - {:ok, %{table: name}} + {:ok, ref_name} = + :dets.open_file(:reference_table, + file: Path.join(path, "reference_table.dets") |> String.to_charlist(), + type: :duplicate_bag + ) + + {:ok, %{table: name, reference_table: ref_name}} end def handle_call({:symbols, file}, _, state) do @@ -74,10 +83,30 @@ defmodule NextLS.SymbolTable do def handle_call(:close, _, state) do :dets.close(state.table) + :dets.close(state.reference_table) {:reply, :ok, state} end + def handle_cast({:put_reference, reference}, state) do + %{ + meta: meta, + func: func, + arity: _arity, + file: file, + module: _module + } = reference + + range = {{meta[:line], meta[:column]}, {meta[:line], meta[:column] + String.length(to_string(func))}} + + :dets.insert(state.reference_table, { + {file, range}, + reference + }) + + {:noreply, state} + end + def handle_cast({:put_symbols, symbols}, state) do %{ module: mod, @@ -120,6 +149,9 @@ defmodule NextLS.SymbolTable do end for {name, {:v1, type, _meta, clauses}} <- defs, {meta, _, _, _} <- clauses do + _ = foo() + _ = foo() + :dets.insert( state.table, {mod, @@ -136,4 +168,6 @@ defmodule NextLS.SymbolTable do {:noreply, state} end + + def foo(), do: :ok end diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index 1a649e99..62de067b 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -1,4 +1,33 @@ defmodule NextLSPrivate.Tracer do + def trace(:start, env) do + :ok + end + + def trace({:local_function, meta, func, arity}, env) do + parent = "NEXTLS_PARENT_PID" |> System.get_env() |> Base.decode64!() |> :erlang.binary_to_term() + + module = + case Macro.Env.lookup_import(env, {func, arity}) do + [{_, module}] -> module + [] -> env.module + end + + Process.send( + parent, + {{:tracer, :local_function}, + %{ + meta: meta, + func: func, + arity: arity, + file: env.file, + module: module + }}, + [] + ) + + :ok + end + def trace({:on_module, bytecode, _}, env) do parent = "NEXTLS_PARENT_PID" |> System.get_env() |> Base.decode64!() |> :erlang.binary_to_term() @@ -15,7 +44,14 @@ defmodule NextLSPrivate.Tracer do Process.send( parent, - {:tracer, %{file: env.file, module: env.module, module_line: line, struct: struct, defs: defs}}, + {:tracer, + %{ + file: env.file, + module: env.module, + module_line: line, + struct: struct, + defs: defs + }}, [] ) diff --git a/test/next_ls/symbol_table_test.exs b/test/next_ls/symbol_table_test.exs index 3afe720b..ed1afbc4 100644 --- a/test/next_ls/symbol_table_test.exs +++ b/test/next_ls/symbol_table_test.exs @@ -5,6 +5,7 @@ defmodule NextLS.SymbolTableTest do alias NextLS.SymbolTable setup %{tmp_dir: dir} do + File.mkdir_p!(dir) pid = start_supervised!({SymbolTable, [path: dir]}) Process.link(pid) diff --git a/test/next_ls_test.exs b/test/next_ls_test.exs index 878ee801..a2de804b 100644 --- a/test/next_ls_test.exs +++ b/test/next_ls_test.exs @@ -7,486 +7,565 @@ defmodule NextLSTest do import GenLSP.Test setup %{tmp_dir: tmp_dir} do - File.write!(Path.join(tmp_dir, "mix.exs"), mix_exs()) File.mkdir_p!(Path.join(tmp_dir, "lib")) + File.write!(Path.join(tmp_dir, "mix.exs"), mix_exs()) + [cwd: tmp_dir] + end - File.write!(Path.join(tmp_dir, "lib/bar.ex"), """ - defmodule Bar do - defstruct [:foo] + describe "one" do + setup %{tmp_dir: tmp_dir} do + File.write!(Path.join(tmp_dir, "lib/bar.ex"), """ + defmodule Bar do + defstruct [:foo] - def foo(arg1) do + def foo(arg1) do + end end - end - """) + """) - File.write!(Path.join(tmp_dir, "lib/code_action.ex"), """ - defmodule Foo.CodeAction do - # some comment + File.write!(Path.join(tmp_dir, "lib/code_action.ex"), """ + defmodule Foo.CodeAction do + # some comment - defmodule NestedMod do - def foo do - :ok + defmodule NestedMod do + def foo do + :ok + end end end - end - """) + """) - File.write!(Path.join(tmp_dir, "lib/foo.ex"), """ - defmodule Foo do - end - """) - - File.write!(Path.join(tmp_dir, "lib/project.ex"), """ - defmodule Project do - def hello do - :world + File.write!(Path.join(tmp_dir, "lib/foo.ex"), """ + defmodule Foo do end - end - """) - - File.rm_rf!(Path.join(tmp_dir, ".elixir-tools")) - - root_path = Path.absname(tmp_dir) - - tvisor = start_supervised!(Task.Supervisor) - rvisor = start_supervised!({DynamicSupervisor, [strategy: :one_for_one]}) - start_supervised!({Registry, [keys: :unique, name: Registry.NextLSTest]}) - extensions = [NextLS.ElixirExtension] - cache = start_supervised!(NextLS.DiagnosticCache) - symbol_table = start_supervised!({NextLS.SymbolTable, path: tmp_dir}) - - server = - server(NextLS, - task_supervisor: tvisor, - dynamic_supervisor: rvisor, - extension_registry: Registry.NextLSTest, - extensions: extensions, - cache: cache, - symbol_table: symbol_table - ) + """) - Process.link(server.lsp) - - client = client(server) - - assert :ok == - request(client, %{ - method: "initialize", - id: 1, - jsonrpc: "2.0", - params: %{capabilities: %{}, rootUri: "file://#{root_path}"} - }) + File.write!(Path.join(tmp_dir, "lib/project.ex"), """ + defmodule Project do + def hello do + :world + end + end + """) - [server: server, client: client, cwd: root_path] - end + File.rm_rf!(Path.join(tmp_dir, ".elixir-tools")) - test "can start the LSP server", %{server: server} do - assert alive?(server) - end - - test "responds correctly to a shutdown request", %{client: client} do - assert :ok == - notify(client, %{ - method: "initialized", - jsonrpc: "2.0", - params: %{} - }) + :ok + end - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + setup :with_lsp - assert :ok == - request(client, %{ - method: "shutdown", - id: 2, - jsonrpc: "2.0", - params: nil - }) + test "can start the LSP server", %{server: server} do + assert alive?(server) + end - assert_result 2, nil - end + test "responds correctly to a shutdown request", %{client: client} do + assert :ok == + notify(client, %{ + method: "initialized", + jsonrpc: "2.0", + params: %{} + }) + + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + + assert :ok == + request(client, %{ + method: "shutdown", + id: 2, + jsonrpc: "2.0", + params: nil + }) + + assert_result 2, nil + end - test "returns method not found for unimplemented requests", %{client: client} do - id = System.unique_integer([:positive]) + test "returns method not found for unimplemented requests", %{client: client} do + id = System.unique_integer([:positive]) + + assert :ok == + notify(client, %{ + method: "initialized", + jsonrpc: "2.0", + params: %{} + }) + + assert :ok == + request(client, %{ + method: "textDocument/signatureHelp", + id: id, + jsonrpc: "2.0", + params: %{position: %{line: 0, character: 0}, textDocument: %{uri: ""}} + }) + + assert_notification "window/logMessage", %{ + "message" => "[NextLS] Method Not Found: textDocument/signatureHelp", + "type" => 2 + } - assert :ok == - notify(client, %{ - method: "initialized", - jsonrpc: "2.0", - params: %{} - }) + assert_error ^id, %{ + "code" => -32_601, + "message" => "Method Not Found: textDocument/signatureHelp" + } + end - assert :ok == - request(client, %{ - method: "textDocument/signatureHelp", - id: id, - jsonrpc: "2.0", - params: %{position: %{line: 0, character: 0}, textDocument: %{uri: ""}} - }) + test "can initialize the server" do + assert_result 1, %{ + "capabilities" => %{ + "textDocumentSync" => %{ + "openClose" => true, + "save" => %{ + "includeText" => true + }, + "change" => 1 + } + }, + "serverInfo" => %{"name" => "NextLS"} + } + end - assert_notification "window/logMessage", %{ - "message" => "[NextLS] Method Not Found: textDocument/signatureHelp", - "type" => 2 - } + test "publishes diagnostics once the client has initialized", %{client: client, cwd: cwd} do + assert :ok == + notify(client, %{ + method: "initialized", + jsonrpc: "2.0", + params: %{} + }) + + assert_notification "window/logMessage", %{ + "message" => "[NextLS] NextLS v" <> _, + "type" => 4 + } - assert_error ^id, %{ - "code" => -32_601, - "message" => "Method Not Found: textDocument/signatureHelp" - } - end + assert_notification "$/progress", %{"value" => %{"kind" => "begin", "title" => "Initializing NextLS runtime..."}} - test "can initialize the server" do - assert_result 1, %{ - "capabilities" => %{ - "textDocumentSync" => %{ - "openClose" => true, - "save" => %{ - "includeText" => true - }, - "change" => 1 + assert_notification "$/progress", %{ + "value" => %{ + "kind" => "end", + "message" => "NextLS runtime has initialized!" } - }, - "serverInfo" => %{"name" => "NextLS"} - } - end - - test "publishes diagnostics once the client has initialized", %{client: client, cwd: cwd} do - assert :ok == - notify(client, %{ - method: "initialized", - jsonrpc: "2.0", - params: %{} - }) - - assert_notification "window/logMessage", %{ - "message" => "[NextLS] NextLS v" <> _, - "type" => 4 - } - - assert_notification "$/progress", %{"value" => %{"kind" => "begin", "title" => "Initializing NextLS runtime..."}} - - assert_notification "$/progress", %{ - "value" => %{ - "kind" => "end", - "message" => "NextLS runtime has initialized!" } - } - assert_notification "$/progress", %{"value" => %{"kind" => "begin", "title" => "Compiling..."}} + assert_notification "$/progress", %{"value" => %{"kind" => "begin", "title" => "Compiling..."}} - assert_notification "$/progress", %{ - "value" => %{ - "kind" => "end", - "message" => "Compiled!" + assert_notification "$/progress", %{ + "value" => %{ + "kind" => "end", + "message" => "Compiled!" + } } - } - - for file <- ["bar.ex"] do - uri = - to_string(%URI{ - host: "", - scheme: "file", - path: Path.join([cwd, "lib", file]) - }) - - char = if Version.match?(System.version(), ">= 1.15.0"), do: 10, else: 0 - - assert_notification "textDocument/publishDiagnostics", %{ - "uri" => ^uri, - "diagnostics" => [ - %{ - "source" => "Elixir", - "severity" => 2, - "message" => - "variable \"arg1\" is unused (if the variable is not meant to be used, prefix it with an underscore)", - "range" => %{ - "start" => %{"line" => 3, "character" => ^char}, - "end" => %{"line" => 3, "character" => 999} + + for file <- ["bar.ex"] do + uri = + to_string(%URI{ + host: "", + scheme: "file", + path: Path.join([cwd, "lib", file]) + }) + + char = if Version.match?(System.version(), ">= 1.15.0"), do: 10, else: 0 + + assert_notification "textDocument/publishDiagnostics", %{ + "uri" => ^uri, + "diagnostics" => [ + %{ + "source" => "Elixir", + "severity" => 2, + "message" => + "variable \"arg1\" is unused (if the variable is not meant to be used, prefix it with an underscore)", + "range" => %{ + "start" => %{"line" => 3, "character" => ^char}, + "end" => %{"line" => 3, "character" => 999} + } } - } - ] - } + ] + } + end end - end - - test "formats", %{client: client} do - assert :ok == - notify(client, %{ - method: "initialized", - jsonrpc: "2.0", - params: %{} - }) - - notify client, %{ - method: "textDocument/didOpen", - jsonrpc: "2.0", - params: %{ - textDocument: %{ - uri: "file://lib/foo/bar.ex", - languageId: "elixir", - version: 1, - text: """ - defmodule Foo.Bar do - def run() do - - :ok + test "formats", %{client: client} do + assert :ok == + notify(client, %{ + method: "initialized", + jsonrpc: "2.0", + params: %{} + }) + + notify client, %{ + method: "textDocument/didOpen", + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: "file://lib/foo/bar.ex", + languageId: "elixir", + version: 1, + text: """ + defmodule Foo.Bar do + def run() do + + + :ok + end end - end - """ + """ + } } } - } - - request client, %{ - method: "textDocument/formatting", - id: 2, - jsonrpc: "2.0", - params: %{ - textDocument: %{ - uri: "file://lib/foo/bar.ex" - }, - options: %{ - insertSpaces: true, - tabSize: 2 + + request client, %{ + method: "textDocument/formatting", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: "file://lib/foo/bar.ex" + }, + options: %{ + insertSpaces: true, + tabSize: 2 + } } } - } - assert_result 2, nil + assert_result 2, nil - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} - request client, %{ - method: "textDocument/formatting", - id: 3, - jsonrpc: "2.0", - params: %{ - textDocument: %{ - uri: "file://lib/foo/bar.ex" - }, - options: %{ - insertSpaces: true, - tabSize: 2 + request client, %{ + method: "textDocument/formatting", + id: 3, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: "file://lib/foo/bar.ex" + }, + options: %{ + insertSpaces: true, + tabSize: 2 + } } } - } - new_text = """ - defmodule Foo.Bar do - def run() do - :ok + new_text = """ + defmodule Foo.Bar do + def run() do + :ok + end end - end - """ - - assert_result 3, [ - %{ - "newText" => ^new_text, - "range" => %{"start" => %{"character" => 0, "line" => 0}, "end" => %{"character" => 0, "line" => 8}} - } - ] - end - - test "formatting gracefully handles files with syntax errors", %{client: client} do - assert :ok == - notify(client, %{ - method: "initialized", - jsonrpc: "2.0", - params: %{} - }) - - notify client, %{ - method: "textDocument/didOpen", - jsonrpc: "2.0", - params: %{ - textDocument: %{ - uri: "file://lib/foo/bar.ex", - languageId: "elixir", - version: 1, - text: """ - defmodule Foo.Bar do - def run() do + """ + assert_result 3, [ + %{ + "newText" => ^new_text, + "range" => %{"start" => %{"character" => 0, "line" => 0}, "end" => %{"character" => 0, "line" => 8}} + } + ] + end - :ok - end - """ + test "formatting gracefully handles files with syntax errors", %{client: client} do + assert :ok == + notify(client, %{ + method: "initialized", + jsonrpc: "2.0", + params: %{} + }) + + notify client, %{ + method: "textDocument/didOpen", + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: "file://lib/foo/bar.ex", + languageId: "elixir", + version: 1, + text: """ + defmodule Foo.Bar do + def run() do + + + :ok + end + """ + } } } - } - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} - request client, %{ - method: "textDocument/formatting", - id: 2, - jsonrpc: "2.0", - params: %{ - textDocument: %{ - uri: "file://lib/foo/bar.ex" - }, - options: %{ - insertSpaces: true, - tabSize: 2 + request client, %{ + method: "textDocument/formatting", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: "file://lib/foo/bar.ex" + }, + options: %{ + insertSpaces: true, + tabSize: 2 + } } } - } - - assert_result 2, nil - end - - test "workspace symbols", %{client: client, cwd: cwd} do - assert :ok == - notify(client, %{ - method: "initialized", - jsonrpc: "2.0", - params: %{} - }) - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} - assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + assert_result 2, nil + end - request client, %{ - method: "workspace/symbol", - id: 2, - jsonrpc: "2.0", - params: %{ - query: "" + test "workspace symbols", %{client: client, cwd: cwd} do + assert :ok == + notify(client, %{ + method: "initialized", + jsonrpc: "2.0", + params: %{} + }) + + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + + request client, %{ + method: "workspace/symbol", + id: 2, + jsonrpc: "2.0", + params: %{ + query: "" + } } - } - assert_result 2, symbols + assert_result 2, symbols - assert %{ - "kind" => 12, - "location" => %{ - "range" => %{ - "start" => %{ - "line" => 3, - "character" => 0 - }, - "end" => %{ - "line" => 3, - "character" => 0 - } - }, - "uri" => "file://#{cwd}/lib/bar.ex" - }, - "name" => "def foo" - } in symbols - - assert %{ - "kind" => 2, - "location" => %{ - "range" => %{ - "start" => %{ - "line" => 0, - "character" => 0 - }, - "end" => %{ - "line" => 0, - "character" => 0 - } - }, - "uri" => "file://#{cwd}/lib/bar.ex" - }, - "name" => "defmodule Bar" - } in symbols - - assert %{ - "kind" => 23, - "location" => %{ - "range" => %{ - "start" => %{ - "line" => 1, - "character" => 0 + assert %{ + "kind" => 12, + "location" => %{ + "range" => %{ + "start" => %{ + "line" => 3, + "character" => 0 + }, + "end" => %{ + "line" => 3, + "character" => 0 + } }, - "end" => %{ - "line" => 1, - "character" => 0 - } + "uri" => "file://#{cwd}/lib/bar.ex" }, - "uri" => "file://#{cwd}/lib/bar.ex" - }, - "name" => "%Bar{}" - } in symbols - - assert %{ - "kind" => 2, - "location" => %{ - "range" => %{ - "start" => %{ - "line" => 3, - "character" => 0 + "name" => "def foo" + } in symbols + + assert %{ + "kind" => 2, + "location" => %{ + "range" => %{ + "start" => %{ + "line" => 0, + "character" => 0 + }, + "end" => %{ + "line" => 0, + "character" => 0 + } }, - "end" => %{ - "line" => 3, - "character" => 0 - } + "uri" => "file://#{cwd}/lib/bar.ex" }, - "uri" => "file://#{cwd}/lib/code_action.ex" - }, - "name" => "defmodule Foo.CodeAction.NestedMod" - } in symbols - end - - test "workspace symbols with query", %{client: client, cwd: cwd} do - assert :ok == - notify(client, %{ - method: "initialized", - jsonrpc: "2.0", - params: %{} - }) - - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} - assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} - - request client, %{ - method: "workspace/symbol", - id: 2, - jsonrpc: "2.0", - params: %{ - query: "fo" - } - } - - assert_result 2, symbols + "name" => "defmodule Bar" + } in symbols - assert [ - %{ - "kind" => 12, + assert %{ + "kind" => 23, "location" => %{ "range" => %{ "start" => %{ - "line" => 3, + "line" => 1, "character" => 0 }, "end" => %{ - "line" => 3, + "line" => 1, "character" => 0 } }, "uri" => "file://#{cwd}/lib/bar.ex" }, - "name" => "def foo" - }, - %{ - "kind" => 12, + "name" => "%Bar{}" + } in symbols + + assert %{ + "kind" => 2, "location" => %{ "range" => %{ "start" => %{ - "line" => 4, + "line" => 3, "character" => 0 }, "end" => %{ - "line" => 4, + "line" => 3, "character" => 0 } }, "uri" => "file://#{cwd}/lib/code_action.ex" }, - "name" => "def foo" - } - ] == symbols + "name" => "defmodule Foo.CodeAction.NestedMod" + } in symbols + end + + test "workspace symbols with query", %{client: client, cwd: cwd} do + assert :ok == + notify(client, %{ + method: "initialized", + jsonrpc: "2.0", + params: %{} + }) + + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + + request client, %{ + method: "workspace/symbol", + id: 2, + jsonrpc: "2.0", + params: %{ + query: "fo" + } + } + + assert_result 2, symbols + + assert [ + %{ + "kind" => 12, + "location" => %{ + "range" => %{ + "start" => %{ + "line" => 3, + "character" => 0 + }, + "end" => %{ + "line" => 3, + "character" => 0 + } + }, + "uri" => "file://#{cwd}/lib/bar.ex" + }, + "name" => "def foo" + }, + %{ + "kind" => 12, + "location" => %{ + "range" => %{ + "start" => %{ + "line" => 4, + "character" => 0 + }, + "end" => %{ + "line" => 4, + "character" => 0 + } + }, + "uri" => "file://#{cwd}/lib/code_action.ex" + }, + "name" => "def foo" + } + ] == symbols + end + end + + describe "two" do + setup %{cwd: cwd} do + path = Path.join(cwd, "lib/bar.ex") + + File.write!(path, """ + defmodule Foo do + def run() do + process() + end + + defp process() do + :ok + end + end + """) + + [path: path] + end + + setup :with_lsp + + test "go to local function definition", %{client: client, path: path} do + assert :ok == + notify(client, %{ + method: "initialized", + jsonrpc: "2.0", + params: %{} + }) + + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + + uri = uri(path) + + request(client, %{ + method: "textDocument/definition", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 2, character: 6}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, %{ + "range" => %{ + "start" => %{ + "line" => 5, + "character" => 0 + }, + "end" => %{ + "line" => 5, + "character" => 0 + } + }, + "uri" => ^uri + } + end + end + + defp with_lsp(%{tmp_dir: tmp_dir}) do + root_path = Path.absname(tmp_dir) + + tvisor = start_supervised!(Task.Supervisor) + rvisor = start_supervised!({DynamicSupervisor, [strategy: :one_for_one]}) + start_supervised!({Registry, [keys: :unique, name: Registry.NextLSTest]}) + extensions = [NextLS.ElixirExtension] + cache = start_supervised!(NextLS.DiagnosticCache) + symbol_table = start_supervised!({NextLS.SymbolTable, path: tmp_dir}) + + server = + server(NextLS, + task_supervisor: tvisor, + dynamic_supervisor: rvisor, + extension_registry: Registry.NextLSTest, + extensions: extensions, + cache: cache, + symbol_table: symbol_table + ) + + Process.link(server.lsp) + + client = client(server) + + assert :ok == + request(client, %{ + method: "initialize", + id: 1, + jsonrpc: "2.0", + params: %{capabilities: %{}, rootUri: "file://#{root_path}"} + }) + + [server: server, client: client, cwd: root_path] + end + + defp uri(path) when is_binary(path) do + URI.to_string(%URI{ + scheme: "file", + host: "", + path: path + }) end end