mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-13 08:24:22 +08:00
Implements the Go to Definition keyboard shortcut (#2741)
Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com> Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
parent
ae5e546bb2
commit
87a49b1bdb
9 changed files with 297 additions and 121 deletions
|
@ -367,7 +367,7 @@ const Cell = {
|
||||||
|
|
||||||
scrollIntoView(element, {
|
scrollIntoView(element, {
|
||||||
scrollMode: "if-needed",
|
scrollMode: "if-needed",
|
||||||
behavior: "smooth",
|
behavior: "instant",
|
||||||
block: "center",
|
block: "center",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,7 +46,7 @@ import { settingsStore } from "../../lib/settings";
|
||||||
import Delta from "../../lib/delta";
|
import Delta from "../../lib/delta";
|
||||||
import Markdown from "../../lib/markdown";
|
import Markdown from "../../lib/markdown";
|
||||||
import { readOnlyHint } from "./live_editor/codemirror/read_only_hint";
|
import { readOnlyHint } from "./live_editor/codemirror/read_only_hint";
|
||||||
import { wait } from "../../lib/utils";
|
import { isMacOS, wait } from "../../lib/utils";
|
||||||
import Emitter from "../../lib/emitter";
|
import Emitter from "../../lib/emitter";
|
||||||
import CollabClient from "./live_editor/collab_client";
|
import CollabClient from "./live_editor/collab_client";
|
||||||
import { languages } from "./live_editor/codemirror/languages";
|
import { languages } from "./live_editor/codemirror/languages";
|
||||||
|
@ -363,14 +363,27 @@ export default class LiveEditor {
|
||||||
settings.editor_mode === "emacs" ? [emacs()] : [],
|
settings.editor_mode === "emacs" ? [emacs()] : [],
|
||||||
language ? language.support : [],
|
language ? language.support : [],
|
||||||
EditorView.domEventHandlers({
|
EditorView.domEventHandlers({
|
||||||
|
click: this.handleEditorClick.bind(this),
|
||||||
keydown: this.handleEditorKeydown.bind(this),
|
keydown: this.handleEditorKeydown.bind(this),
|
||||||
blur: this.handleEditorBlur.bind(this),
|
blur: this.handleEditorBlur.bind(this),
|
||||||
focus: this.handleEditorFocus.bind(this),
|
focus: this.handleEditorFocus.bind(this),
|
||||||
}),
|
}),
|
||||||
|
EditorView.clickAddsSelectionRange.of((event) => event.altKey),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
handleEditorClick(event) {
|
||||||
|
const cmd = isMacOS() ? event.metaKey : event.ctrlKey;
|
||||||
|
|
||||||
|
if (cmd) {
|
||||||
|
this.jumpToDefinition(this.view);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
handleEditorKeydown(event) {
|
handleEditorKeydown(event) {
|
||||||
// We dispatch escape event, but only if it is not consumed by any
|
// We dispatch escape event, but only if it is not consumed by any
|
||||||
|
@ -541,6 +554,29 @@ export default class LiveEditor {
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
jumpToDefinition(view) {
|
||||||
|
const pos = view.state.selection.main.head;
|
||||||
|
const line = view.state.doc.lineAt(pos);
|
||||||
|
const lineLength = line.to - line.from;
|
||||||
|
const text = line.text;
|
||||||
|
|
||||||
|
const column = pos - line.from;
|
||||||
|
if (column < 1 || column > lineLength) return null;
|
||||||
|
|
||||||
|
return this.connection
|
||||||
|
.intellisenseRequest("definition", { line: text, column })
|
||||||
|
.then((response) => {
|
||||||
|
globalPubsub.broadcast("jump_to_editor", {
|
||||||
|
line: response.line,
|
||||||
|
file: response.file,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
signatureSource({ state, pos }) {
|
signatureSource({ state, pos }) {
|
||||||
const textUntilCursor = this.getSignatureHint(state, pos);
|
const textUntilCursor = this.getSignatureHint(state, pos);
|
||||||
|
|
|
@ -157,6 +157,12 @@ const Session = {
|
||||||
(event) => this.toggleCollapseAllSections(),
|
(event) => this.toggleCollapseAllSections(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.subscriptions = [
|
||||||
|
globalPubsub.subscribe("jump_to_editor", ({ line, file }) =>
|
||||||
|
this.jumpToLine(file, line),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
this.initializeDragAndDrop();
|
this.initializeDragAndDrop();
|
||||||
|
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
|
@ -270,6 +276,7 @@ const Session = {
|
||||||
leaveChannel();
|
leaveChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.subscriptions.forEach((subscription) => subscription.destroy());
|
||||||
this.store.destroy();
|
this.store.destroy();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -546,12 +553,9 @@ const Session = {
|
||||||
const search = event.target.hash.replace("#go-to-definition", "");
|
const search = event.target.hash.replace("#go-to-definition", "");
|
||||||
const params = new URLSearchParams(search);
|
const params = new URLSearchParams(search);
|
||||||
const line = parseInt(params.get("line"), 10);
|
const line = parseInt(params.get("line"), 10);
|
||||||
const [_filename, cellId] = params.get("file").split("#cell:");
|
const file = params.get("file");
|
||||||
|
|
||||||
this.setFocusedEl(cellId);
|
this.jumpToLine(file, line);
|
||||||
this.setInsertMode(true);
|
|
||||||
|
|
||||||
globalPubsub.broadcast(`cells:${cellId}`, { type: "jump_to_line", line });
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -1442,6 +1446,15 @@ const Session = {
|
||||||
getElement(name) {
|
getElement(name) {
|
||||||
return this.el.querySelector(`[data-el-${name}]`);
|
return this.el.querySelector(`[data-el-${name}]`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
jumpToLine(file, line) {
|
||||||
|
const [_filename, cellId] = file.split("#cell:");
|
||||||
|
|
||||||
|
this.setFocusedEl(cellId, { scroll: false });
|
||||||
|
this.setInsertMode(true);
|
||||||
|
|
||||||
|
globalPubsub.broadcast(`cells:${cellId}`, { type: "jump_to_line", line });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Session;
|
export default Session;
|
||||||
|
|
|
@ -53,6 +53,10 @@ defmodule Livebook.Intellisense do
|
||||||
get_details(line, column, context, node)
|
get_details(line, column, context, node)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_request({:definition, line, column}, context, node) do
|
||||||
|
get_definitions(line, column, context, node)
|
||||||
|
end
|
||||||
|
|
||||||
def handle_request({:signature, hint}, context, node) do
|
def handle_request({:signature, hint}, context, node) do
|
||||||
get_signature_items(hint, context, node)
|
get_signature_items(hint, context, node)
|
||||||
end
|
end
|
||||||
|
@ -452,7 +456,7 @@ defmodule Livebook.Intellisense do
|
||||||
) do
|
) do
|
||||||
join_with_divider([
|
join_with_divider([
|
||||||
code(inspect(module)),
|
code(inspect(module)),
|
||||||
format_definition_link(module, context),
|
format_definition_link(module, context, {:module, module}),
|
||||||
format_docs_link(module),
|
format_docs_link(module),
|
||||||
format_documentation(documentation, :all)
|
format_documentation(documentation, :all)
|
||||||
])
|
])
|
||||||
|
@ -539,23 +543,9 @@ defmodule Livebook.Intellisense do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp format_definition_link(module, context, function_or_type \\ nil) do
|
defp format_definition_link(module, context, identifier) do
|
||||||
if context.ebin_path do
|
if query = get_definition_location(module, context, identifier) do
|
||||||
path = Path.join(context.ebin_path, "#{module}.beam")
|
"[Go to definition](#go-to-definition?#{URI.encode_query(query)})"
|
||||||
|
|
||||||
identifier =
|
|
||||||
if function_or_type,
|
|
||||||
do: function_or_type,
|
|
||||||
else: {:module, module}
|
|
||||||
|
|
||||||
with true <- File.exists?(path),
|
|
||||||
{:ok, line} <- Docs.locate_definition(path, identifier) do
|
|
||||||
file = module.module_info(:compile)[:source]
|
|
||||||
query_string = URI.encode_query(%{file: to_string(file), line: line})
|
|
||||||
"[Go to definition](#go-to-definition?#{query_string})"
|
|
||||||
else
|
|
||||||
_otherwise -> nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -710,6 +700,56 @@ defmodule Livebook.Intellisense do
|
||||||
raise "unknown documentation format #{inspect(format)}"
|
raise "unknown documentation format #{inspect(format)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the identifier definition located in `column` in `line`.
|
||||||
|
"""
|
||||||
|
@spec get_definitions(String.t(), pos_integer(), context(), node()) ::
|
||||||
|
Runtime.definition_response() | nil
|
||||||
|
def get_definitions(line, column, context, node) do
|
||||||
|
case IdentifierMatcher.locate_identifier(line, column, context, node) do
|
||||||
|
%{matches: []} ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
%{matches: matches, range: range} ->
|
||||||
|
matches
|
||||||
|
|> Enum.sort_by(& &1[:arity], :asc)
|
||||||
|
|> Enum.flat_map(&List.wrap(get_definition_location(&1, context)))
|
||||||
|
|> case do
|
||||||
|
[%{file: file, line: line} | _] -> %{range: range, file: file, line: line}
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_definition_location(%{kind: :module, module: module}, context) do
|
||||||
|
get_definition_location(module, context, {:module, module})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_definition_location(
|
||||||
|
%{kind: :function, module: module, name: name, arity: arity},
|
||||||
|
context
|
||||||
|
) do
|
||||||
|
get_definition_location(module, context, {:function, name, arity})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_definition_location(%{kind: :type, module: module, name: name, arity: arity}, context) do
|
||||||
|
get_definition_location(module, context, {:type, name, arity})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_definition_location(module, context, identifier) do
|
||||||
|
if context.ebin_path do
|
||||||
|
path = Path.join(context.ebin_path, "#{module}.beam")
|
||||||
|
|
||||||
|
with true <- File.exists?(path),
|
||||||
|
{:ok, line} <- Docs.locate_definition(path, identifier) do
|
||||||
|
file = module.module_info(:compile)[:source]
|
||||||
|
%{file: to_string(file), line: line}
|
||||||
|
else
|
||||||
|
_otherwise -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Erlang HTML AST
|
# Erlang HTML AST
|
||||||
# See https://erlang.org/doc/apps/erl_docgen/doc_storage.html#erlang-documentation-format
|
# See https://erlang.org/doc/apps/erl_docgen/doc_storage.html#erlang-documentation-format
|
||||||
|
|
||||||
|
|
|
@ -627,19 +627,35 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_matching_modules(hint, ctx) do
|
defp get_matching_modules(hint, ctx) do
|
||||||
ctx.node
|
ctx
|
||||||
|> get_modules()
|
|> get_modules()
|
||||||
|> Enum.filter(&ctx.matcher.(Atom.to_string(&1), hint))
|
|> Enum.filter(&ctx.matcher.(Atom.to_string(&1), hint))
|
||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_modules(node) do
|
defp get_modules(%{node: node} = ctx) do
|
||||||
modules = cached_all_loaded(node)
|
# On interactive mode, we load modules from the application
|
||||||
|
# and then the ones from runtime. For a remote node, ideally
|
||||||
|
# we would get the applications one, but there is no cheap
|
||||||
|
# way to do such, so we get :code.all_loaded and cache it
|
||||||
|
# instead (which includes all modules anyway on embedded mode).
|
||||||
if node == node() and :code.get_mode() == :interactive do
|
if node == node() and :code.get_mode() == :interactive do
|
||||||
modules ++ get_modules_from_applications()
|
runtime_modules(ctx.intellisense_context.ebin_path) ++ get_modules_from_applications()
|
||||||
else
|
else
|
||||||
modules
|
cached_all_loaded(node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp runtime_modules(path) do
|
||||||
|
with true <- is_binary(path),
|
||||||
|
{:ok, beams} <- File.ls(path) do
|
||||||
|
for beam <- beams, String.ends_with?(beam, ".beam") do
|
||||||
|
beam
|
||||||
|
|> binary_slice(0..-6//1)
|
||||||
|
|> String.to_atom()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
_ -> []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -503,6 +503,7 @@ defprotocol Livebook.Runtime do
|
||||||
@type intellisense_request ::
|
@type intellisense_request ::
|
||||||
completion_request()
|
completion_request()
|
||||||
| details_request()
|
| details_request()
|
||||||
|
| definition_request()
|
||||||
| signature_request()
|
| signature_request()
|
||||||
| format_request()
|
| format_request()
|
||||||
|
|
||||||
|
@ -516,6 +517,7 @@ defprotocol Livebook.Runtime do
|
||||||
nil
|
nil
|
||||||
| completion_response()
|
| completion_response()
|
||||||
| details_response()
|
| details_response()
|
||||||
|
| definition_response()
|
||||||
| signature_response()
|
| signature_response()
|
||||||
| format_response()
|
| format_response()
|
||||||
|
|
||||||
|
@ -553,6 +555,21 @@ defprotocol Livebook.Runtime do
|
||||||
contents: list(String.t())
|
contents: list(String.t())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@typedoc """
|
||||||
|
Looks up more the definition about an identifier found in `column` in
|
||||||
|
`line`.
|
||||||
|
"""
|
||||||
|
@type definition_request :: {:definition, line :: String.t(), column :: pos_integer()}
|
||||||
|
|
||||||
|
@type definition_response :: %{
|
||||||
|
range: %{
|
||||||
|
from: non_neg_integer(),
|
||||||
|
to: non_neg_integer()
|
||||||
|
},
|
||||||
|
line: pos_integer(),
|
||||||
|
file: String.t()
|
||||||
|
}
|
||||||
|
|
||||||
@typedoc """
|
@typedoc """
|
||||||
Looks up a list of function signatures matching the given hint.
|
Looks up a list of function signatures matching the given hint.
|
||||||
|
|
||||||
|
|
|
@ -616,6 +616,10 @@ defmodule LivebookWeb.SessionLive do
|
||||||
column = Text.JS.js_column_to_elixir(column, line)
|
column = Text.JS.js_column_to_elixir(column, line)
|
||||||
{:details, line, column}
|
{:details, line, column}
|
||||||
|
|
||||||
|
%{"type" => "definition", "line" => line, "column" => column} ->
|
||||||
|
column = Text.JS.js_column_to_elixir(column, line)
|
||||||
|
{:definition, line, column}
|
||||||
|
|
||||||
%{"type" => "signature", "hint" => hint} ->
|
%{"type" => "signature", "hint" => hint} ->
|
||||||
{:signature, hint}
|
{:signature, hint}
|
||||||
|
|
||||||
|
|
|
@ -251,16 +251,6 @@ defmodule Livebook.IntellisenseTest do
|
||||||
] = Intellisense.get_completion_items("RuntimeE", context, node())
|
] = Intellisense.get_completion_items("RuntimeE", context, node())
|
||||||
end
|
end
|
||||||
|
|
||||||
test "caches all loaded modules" do
|
|
||||||
context = eval(do: nil)
|
|
||||||
Intellisense.get_completion_items("Hub", context, node())
|
|
||||||
|
|
||||||
key = {Intellisense.IdentifierMatcher, node()}
|
|
||||||
assert [_ | _] = :persistent_term.get(key, :error)
|
|
||||||
Intellisense.IdentifierMatcher.clear_all_loaded(node())
|
|
||||||
assert :error = :persistent_term.get(key, :error)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "Elixir struct completion lists nested options" do
|
test "Elixir struct completion lists nested options" do
|
||||||
context = eval(do: nil)
|
context = eval(do: nil)
|
||||||
|
|
||||||
|
@ -999,11 +989,16 @@ defmodule Livebook.IntellisenseTest do
|
||||||
] = Intellisense.get_completion_items("^my_va", context, node())
|
] = Intellisense.get_completion_items("^my_va", context, node())
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule SublevelTest.LevelA.LevelB do
|
@tag :tmp_dir
|
||||||
end
|
test "Elixir completion sublevel", %{tmp_dir: tmp_dir} do
|
||||||
|
context =
|
||||||
|
eval tmp_dir do
|
||||||
|
end
|
||||||
|
|
||||||
test "Elixir completion sublevel" do
|
compile_and_save_bytecode(tmp_dir, ~S'''
|
||||||
context = eval(do: nil)
|
defmodule Livebook.IntellisenseTest.SublevelTest.LevelA.LevelB do
|
||||||
|
end
|
||||||
|
''')
|
||||||
|
|
||||||
assert [%{label: "LevelA"}] =
|
assert [%{label: "LevelA"}] =
|
||||||
Intellisense.get_completion_items(
|
Intellisense.get_completion_items(
|
||||||
|
@ -1100,12 +1095,17 @@ defmodule Livebook.IntellisenseTest do
|
||||||
:code.delete(Sample)
|
:code.delete(Sample)
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule MyStruct do
|
@tag :tmp_dir
|
||||||
defstruct [:my_val]
|
test "completion for struct names", %{tmp_dir: tmp_dir} do
|
||||||
end
|
context =
|
||||||
|
eval tmp_dir do
|
||||||
|
end
|
||||||
|
|
||||||
test "completion for struct names" do
|
compile_and_save_bytecode(tmp_dir, ~S'''
|
||||||
context = eval(do: nil)
|
defmodule Livebook.IntellisenseTest.MyStruct do
|
||||||
|
defstruct [:my_val]
|
||||||
|
end
|
||||||
|
''')
|
||||||
|
|
||||||
assert [
|
assert [
|
||||||
%{label: "MyStruct"}
|
%{label: "MyStruct"}
|
||||||
|
@ -1117,9 +1117,16 @@ defmodule Livebook.IntellisenseTest do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "completion for struct keys" do
|
@tag :tmp_dir
|
||||||
|
test "completion for struct keys", %{tmp_dir: tmp_dir} do
|
||||||
|
compile_and_save_bytecode(tmp_dir, ~S'''
|
||||||
|
defmodule Livebook.IntellisenseTest.MyStruct do
|
||||||
|
defstruct [:my_val]
|
||||||
|
end
|
||||||
|
''')
|
||||||
|
|
||||||
context =
|
context =
|
||||||
eval do
|
eval tmp_dir do
|
||||||
struct = %Livebook.IntellisenseTest.MyStruct{}
|
struct = %Livebook.IntellisenseTest.MyStruct{}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1128,8 +1135,17 @@ defmodule Livebook.IntellisenseTest do
|
||||||
] = Intellisense.get_completion_items("struct.my", context, node())
|
] = Intellisense.get_completion_items("struct.my", context, node())
|
||||||
end
|
end
|
||||||
|
|
||||||
test "completion for struct keys inside struct" do
|
@tag :tmp_dir
|
||||||
context = eval(do: nil)
|
test "completion for struct keys inside struct", %{tmp_dir: tmp_dir} do
|
||||||
|
context =
|
||||||
|
eval tmp_dir do
|
||||||
|
end
|
||||||
|
|
||||||
|
compile_and_save_bytecode(tmp_dir, ~S'''
|
||||||
|
defmodule Livebook.IntellisenseTest.MyStruct do
|
||||||
|
defstruct [:my_val]
|
||||||
|
end
|
||||||
|
''')
|
||||||
|
|
||||||
assert [
|
assert [
|
||||||
%{
|
%{
|
||||||
|
@ -1154,8 +1170,18 @@ defmodule Livebook.IntellisenseTest do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "completion for struct keys inside struct removes filled keys" do
|
@tag :tmp_dir
|
||||||
context = eval(do: nil)
|
test "completion for struct keys inside struct removes filled keys",
|
||||||
|
%{tmp_dir: tmp_dir} do
|
||||||
|
context =
|
||||||
|
eval tmp_dir do
|
||||||
|
end
|
||||||
|
|
||||||
|
compile_and_save_bytecode(tmp_dir, ~S'''
|
||||||
|
defmodule Livebook.IntellisenseTest.MyStruct do
|
||||||
|
defstruct [:my_val]
|
||||||
|
end
|
||||||
|
''')
|
||||||
|
|
||||||
assert [] =
|
assert [] =
|
||||||
Intellisense.get_completion_items(
|
Intellisense.get_completion_items(
|
||||||
|
@ -1173,8 +1199,17 @@ defmodule Livebook.IntellisenseTest do
|
||||||
refute Enum.find(completions, &match?(%{label: "__exception__"}, &1))
|
refute Enum.find(completions, &match?(%{label: "__exception__"}, &1))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "completion for struct keys in update syntax" do
|
@tag :tmp_dir
|
||||||
context = eval(do: nil)
|
test "completion for struct keys in update syntax", %{tmp_dir: tmp_dir} do
|
||||||
|
context =
|
||||||
|
eval tmp_dir do
|
||||||
|
end
|
||||||
|
|
||||||
|
compile_and_save_bytecode(tmp_dir, ~S'''
|
||||||
|
defmodule Livebook.IntellisenseTest.MyStruct do
|
||||||
|
defstruct [:my_val]
|
||||||
|
end
|
||||||
|
''')
|
||||||
|
|
||||||
assert [
|
assert [
|
||||||
%{
|
%{
|
||||||
|
@ -1581,77 +1616,75 @@ defmodule Livebook.IntellisenseTest do
|
||||||
|
|
||||||
assert content =~ ~r"https://www.erlang.org/doc/man/string.html#uppercase-1"
|
assert content =~ ~r"https://www.erlang.org/doc/man/string.html#uppercase-1"
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@tag :tmp_dir
|
@tag :tmp_dir
|
||||||
test "returns the go to definition link", %{tmp_dir: tmp_dir} do
|
test "get_definitions/4 returns the go to definition query string", %{tmp_dir: tmp_dir} do
|
||||||
Code.put_compiler_option(:debug_info, true)
|
Code.put_compiler_option(:debug_info, true)
|
||||||
|
|
||||||
context =
|
context =
|
||||||
eval tmp_dir do
|
eval tmp_dir do
|
||||||
alias Livebook.IntellisenseTest.GoToDefinition
|
alias Livebook.IntellisenseTest.GoToDefinition
|
||||||
end
|
end
|
||||||
|
|
||||||
code = ~S'''
|
code = ~S'''
|
||||||
defmodule Livebook.IntellisenseTest.GoToDefinition do
|
defmodule Livebook.IntellisenseTest.GoToDefinition do
|
||||||
@type t :: term()
|
@type t :: term()
|
||||||
@type foo :: foo(:bar)
|
@type foo :: foo(:bar)
|
||||||
@type foo(var) :: {var, t()}
|
@type foo(var) :: {var, t()}
|
||||||
|
|
||||||
defmacro with_logging(do: block) do
|
defmacro with_logging(do: block) do
|
||||||
quote do
|
quote do
|
||||||
require Logger
|
require Logger
|
||||||
Logger.debug("Running code")
|
Logger.debug("Running code")
|
||||||
result = unquote(block)
|
result = unquote(block)
|
||||||
Logger.debug("Result: #{inspect(result)}")
|
Logger.debug("Result: #{inspect(result)}")
|
||||||
result
|
result
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec hello(var :: term()) :: foo(term())
|
|
||||||
def hello(message) do
|
|
||||||
{:bar, message}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
'''
|
|
||||||
|
|
||||||
file = "#{__ENV__.file}#cell:#{Livebook.Utils.random_short_id()}"
|
@spec hello(var :: term()) :: foo(term())
|
||||||
path = Path.join(tmp_dir, "Elixir.Livebook.IntellisenseTest.GoToDefinition.beam")
|
def hello(message) do
|
||||||
|
{:bar, message}
|
||||||
[{_module, bytecode}] = Code.compile_string(code, file)
|
end
|
||||||
File.write!(path, bytecode)
|
|
||||||
Code.prepend_path(tmp_dir)
|
|
||||||
|
|
||||||
assert %{contents: [content]} =
|
|
||||||
Intellisense.get_details("GoToDefinition", 14, context, node())
|
|
||||||
|
|
||||||
assert content =~ "#go-to-definition?#{URI.encode_query(%{file: file, line: 1})}"
|
|
||||||
|
|
||||||
assert %{contents: [content]} =
|
|
||||||
Intellisense.get_details("GoToDefinition.t", 16, context, node())
|
|
||||||
|
|
||||||
assert content =~ "#go-to-definition?#{URI.encode_query(%{file: file, line: 2})}"
|
|
||||||
|
|
||||||
assert %{contents: [arity_0_content, arity_1_content]} =
|
|
||||||
Intellisense.get_details("GoToDefinition.foo", 18, context, node())
|
|
||||||
|
|
||||||
assert arity_0_content =~ "#go-to-definition?#{URI.encode_query(%{file: file, line: 3})}"
|
|
||||||
assert arity_1_content =~ "#go-to-definition?#{URI.encode_query(%{file: file, line: 4})}"
|
|
||||||
|
|
||||||
assert %{contents: [content]} =
|
|
||||||
Intellisense.get_details("GoToDefinition.with_logging", 20, context, node())
|
|
||||||
|
|
||||||
assert content =~ "#go-to-definition?#{URI.encode_query(%{file: file, line: 6})}"
|
|
||||||
|
|
||||||
assert %{contents: [content]} =
|
|
||||||
Intellisense.get_details("GoToDefinition.hello", 18, context, node())
|
|
||||||
|
|
||||||
assert content =~ "#go-to-definition?#{URI.encode_query(%{file: file, line: 17})}"
|
|
||||||
after
|
|
||||||
Code.put_compiler_option(:debug_info, false)
|
|
||||||
Code.delete_path(tmp_dir)
|
|
||||||
:code.purge(:"Elixir.Livebook.IntellisenseTest.GoToDefinition")
|
|
||||||
:code.delete(:"Elixir.Livebook.IntellisenseTest.GoToDefinition")
|
|
||||||
end
|
end
|
||||||
|
'''
|
||||||
|
|
||||||
|
file = "#{__ENV__.file}#cell:#{Livebook.Utils.random_short_id()}"
|
||||||
|
compile_and_save_bytecode(tmp_dir, code, file)
|
||||||
|
|
||||||
|
assert Intellisense.get_definitions("GoToDefinition", 14, context, node()) == %{
|
||||||
|
line: 1,
|
||||||
|
file: file,
|
||||||
|
range: %{to: 15, from: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert Intellisense.get_definitions("GoToDefinition.t", 16, context, node()) == %{
|
||||||
|
line: 2,
|
||||||
|
file: file,
|
||||||
|
range: %{to: 17, from: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
# For now, we aren't fetching the expected arity but we will address it later.
|
||||||
|
assert Intellisense.get_definitions("GoToDefinition.foo", 18, context, node()) == %{
|
||||||
|
line: 3,
|
||||||
|
file: file,
|
||||||
|
range: %{to: 19, from: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert Intellisense.get_definitions("GoToDefinition.with_logging", 20, context, node()) == %{
|
||||||
|
line: 6,
|
||||||
|
file: file,
|
||||||
|
range: %{to: 28, from: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert Intellisense.get_definitions("GoToDefinition.hello", 18, context, node()) == %{
|
||||||
|
line: 17,
|
||||||
|
file: file,
|
||||||
|
range: %{to: 21, from: 1}
|
||||||
|
}
|
||||||
|
after
|
||||||
|
Code.put_compiler_option(:debug_info, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "get_signature_items/3" do
|
describe "get_signature_items/3" do
|
||||||
|
@ -2032,4 +2065,18 @@ defmodule Livebook.IntellisenseTest do
|
||||||
assert content =~ "No documentation available"
|
assert content =~ "No documentation available"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp compile_and_save_bytecode(dir, code, file \\ "nofile") do
|
||||||
|
[{module, bytecode}] = Code.compile_string(code, file)
|
||||||
|
path = Path.join(dir, "#{module}.beam")
|
||||||
|
|
||||||
|
File.write!(path, bytecode)
|
||||||
|
Code.prepend_path(dir)
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
Code.delete_path(dir)
|
||||||
|
:code.purge(module)
|
||||||
|
:code.delete(module)
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,9 @@ Livebook.Runtime.ErlDist.NodeManager.start(
|
||||||
unload_modules_on_termination: false
|
unload_modules_on_termination: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# We load erts so we can access its modules for completion.
|
||||||
|
Application.load(:erts)
|
||||||
|
|
||||||
# Use the embedded runtime in tests by default, so they are cheaper
|
# Use the embedded runtime in tests by default, so they are cheaper
|
||||||
# to run. Other runtimes can be tested by setting them explicitly
|
# to run. Other runtimes can be tested by setting them explicitly
|
||||||
Application.put_env(:livebook, :default_runtime, Livebook.Runtime.Embedded.new())
|
Application.put_env(:livebook, :default_runtime, Livebook.Runtime.Embedded.new())
|
||||||
|
|
Loading…
Add table
Reference in a new issue