mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-17 14:19:53 +08:00
Show full documentation when hovering over an identifier (#453)
* Show full documentation when hovering over an identifier * Remove leftover function * Improve determining subject boundaries
This commit is contained in:
parent
6276aafa72
commit
ef06e49d18
20 changed files with 1979 additions and 1345 deletions
|
|
@ -1,12 +1,68 @@
|
|||
/* Monaco overrides */
|
||||
/* === Monaco overrides === */
|
||||
|
||||
/* Add some spacing to code snippets in completion suggestions */
|
||||
div.suggest-details-container div.monaco-tokenized-source {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
/*
|
||||
CSS normalization removes the default styles of HTML elements,
|
||||
so we need to adjust styles of Monaco-rendered Markdown docs.
|
||||
Also some spacing adjustments.
|
||||
*/
|
||||
|
||||
.suggest-details .header p {
|
||||
@apply pb-0 pt-3 !important;
|
||||
}
|
||||
|
||||
/* Monaco cursor widget */
|
||||
.monaco-hover p,
|
||||
.suggest-details .docs p {
|
||||
@apply my-2 !important;
|
||||
}
|
||||
|
||||
.suggest-details h1,
|
||||
.monaco-hover h1 {
|
||||
@apply text-xl font-semibold mt-4 mb-2;
|
||||
}
|
||||
|
||||
.suggest-details h2,
|
||||
.monaco-hover h2 {
|
||||
@apply text-lg font-medium mt-4 mb-2;
|
||||
}
|
||||
|
||||
.suggest-details h3,
|
||||
.monaco-hover h3 {
|
||||
@apply font-medium mt-4 mb-2;
|
||||
}
|
||||
|
||||
.suggest-details ul,
|
||||
.monaco-hover ul {
|
||||
@apply list-disc;
|
||||
}
|
||||
|
||||
.suggest-details ol,
|
||||
.monaco-hover ol {
|
||||
@apply list-decimal;
|
||||
}
|
||||
|
||||
.suggest-details hr,
|
||||
.monaco-hover hr {
|
||||
@apply my-2 !important;
|
||||
}
|
||||
|
||||
.suggest-details blockquote,
|
||||
.monaco-hover blockquote {
|
||||
@apply border-l-4 border-gray-200 pl-4 py-0.5 my-2;
|
||||
}
|
||||
|
||||
/* Add some spacing to code snippets in completion suggestions */
|
||||
.suggest-details div.monaco-tokenized-source,
|
||||
.monaco-hover div.monaco-tokenized-source {
|
||||
@apply my-2;
|
||||
}
|
||||
|
||||
/* Increase the hover box limits */
|
||||
.monaco-hover-content {
|
||||
max-width: 1000px !important;
|
||||
max-height: 300px !important;
|
||||
}
|
||||
|
||||
/* === Monaco cursor widget === */
|
||||
|
||||
.monaco-cursor-widget-container {
|
||||
pointer-events: none;
|
||||
|
|
|
|||
|
|
@ -22,8 +22,7 @@ class LiveEditor {
|
|||
this.__mountEditor();
|
||||
|
||||
if (type === "elixir") {
|
||||
this.__setupCompletion();
|
||||
this.__setupFormatting();
|
||||
this.__setupIntellisense();
|
||||
}
|
||||
|
||||
const serverAdapter = new HookServerAdapter(hook, cellId);
|
||||
|
|
@ -196,9 +195,17 @@ class LiveEditor {
|
|||
});
|
||||
}
|
||||
|
||||
__setupCompletion() {
|
||||
/**
|
||||
* Defines cell-specific providers for various editor features.
|
||||
*/
|
||||
__setupIntellisense() {
|
||||
this.handlerByRef = {};
|
||||
|
||||
/**
|
||||
* Completion happens asynchronously, the flow goes as follows:
|
||||
* Intellisense requests such as completion or formatting are
|
||||
* handled asynchronously by the runtime.
|
||||
*
|
||||
* As an example, let's go through the steps for completion:
|
||||
*
|
||||
* * the user opens the completion list, which triggers the global
|
||||
* completion provider registered in `live_editor/monaco.js`
|
||||
|
|
@ -210,101 +217,124 @@ class LiveEditor {
|
|||
* * then `__getCompletionItems` sends a completion request to the LV process
|
||||
* and gets a unique reference, under which it keeps completion callback
|
||||
*
|
||||
* * finally the hook receives the "completion_response" event with completion items,
|
||||
* it looks up completion callback for the received reference and calls it
|
||||
* with the received items list
|
||||
* * finally the hook receives the "intellisense_response" event with completion
|
||||
* response, it looks up completion callback for the received reference and calls
|
||||
* it with the response, which finally returns the completion items to the editor
|
||||
*/
|
||||
|
||||
const completionHandlerByRef = {};
|
||||
|
||||
this.editor.getModel().__getCompletionItems = (model, position) => {
|
||||
const line = model.getLineContent(position.lineNumber);
|
||||
const lineUntilCursor = line.slice(0, position.column - 1);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.hook.pushEvent(
|
||||
"completion_request",
|
||||
{
|
||||
hint: lineUntilCursor,
|
||||
cell_id: this.cellId,
|
||||
},
|
||||
({ completion_ref: completionRef }) => {
|
||||
if (completionRef) {
|
||||
completionHandlerByRef[completionRef] = (items) => {
|
||||
const suggestions = completionItemsToSuggestions(items);
|
||||
resolve({ suggestions });
|
||||
};
|
||||
} else {
|
||||
resolve({ suggestions: [] });
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
return this.__asyncIntellisenseRequest("completion", {
|
||||
hint: lineUntilCursor,
|
||||
})
|
||||
.then((response) => {
|
||||
const suggestions = completionItemsToSuggestions(response.items);
|
||||
return { suggestions };
|
||||
})
|
||||
.catch(() => null);
|
||||
};
|
||||
|
||||
this.hook.handleEvent(
|
||||
"completion_response",
|
||||
({ completion_ref: completionRef, items }) => {
|
||||
const handler = completionHandlerByRef[completionRef];
|
||||
this.editor.getModel().__getHover = (model, position) => {
|
||||
const line = model.getLineContent(position.lineNumber);
|
||||
const index = position.column - 1;
|
||||
|
||||
if (handler) {
|
||||
handler(items);
|
||||
delete completionHandlerByRef[completionRef];
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
return this.__asyncIntellisenseRequest("details", { line, index })
|
||||
.then((response) => {
|
||||
const contents = response.contents.map((content) => ({
|
||||
value: content,
|
||||
isTrusted: true,
|
||||
}));
|
||||
|
||||
__setupFormatting() {
|
||||
/**
|
||||
* Similarly to completion, formatting is delegated to the function
|
||||
* defined below, where we simply communicate with LV to get
|
||||
* a formatted version of the current editor content.
|
||||
*/
|
||||
const range = new monaco.Range(
|
||||
position.lineNumber,
|
||||
response.range.from + 1,
|
||||
position.lineNumber,
|
||||
response.range.to + 1
|
||||
);
|
||||
|
||||
return { contents, range };
|
||||
})
|
||||
.catch(() => null);
|
||||
};
|
||||
|
||||
this.editor.getModel().__getDocumentFormattingEdits = (model) => {
|
||||
const content = model.getValue();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.hook.pushEvent(
|
||||
"format_code",
|
||||
{ code: content },
|
||||
({ code: formatted }) => {
|
||||
/**
|
||||
* We use a single edit replacing the whole editor content,
|
||||
* but the editor itself optimises this into a list of edits
|
||||
* that produce minimal diff using the Myers string difference.
|
||||
*
|
||||
* References:
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L324
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/common/services/editorSimpleWorker.ts#L489
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/base/common/diff/diff.ts#L227-L231
|
||||
*
|
||||
* Eventually the editor will received the optimised list of edits,
|
||||
* which we then convert to Delta and send to the server.
|
||||
* Consequently, the Delta carries only the minimal formatting diff.
|
||||
*
|
||||
* Also, if edits are applied to the editor, either by typing
|
||||
* or receiving remote changes, the formatting is cancelled.
|
||||
* In other words the formatting changes are actually applied
|
||||
* only if the editor stays intact.
|
||||
*
|
||||
* References:
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L313
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/browser/core/editorState.ts#L137
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L326
|
||||
*/
|
||||
return this.__asyncIntellisenseRequest("format", { code: content })
|
||||
.then((response) => {
|
||||
/**
|
||||
* We use a single edit replacing the whole editor content,
|
||||
* but the editor itself optimises this into a list of edits
|
||||
* that produce minimal diff using the Myers string difference.
|
||||
*
|
||||
* References:
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L324
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/common/services/editorSimpleWorker.ts#L489
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/base/common/diff/diff.ts#L227-L231
|
||||
*
|
||||
* Eventually the editor will received the optimised list of edits,
|
||||
* which we then convert to Delta and send to the server.
|
||||
* Consequently, the Delta carries only the minimal formatting diff.
|
||||
*
|
||||
* Also, if edits are applied to the editor, either by typing
|
||||
* or receiving remote changes, the formatting is cancelled.
|
||||
* In other words the formatting changes are actually applied
|
||||
* only if the editor stays intact.
|
||||
*
|
||||
* References:
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L313
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/browser/core/editorState.ts#L137
|
||||
* * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L326
|
||||
*/
|
||||
|
||||
const replaceEdit = {
|
||||
range: model.getFullModelRange(),
|
||||
text: formatted,
|
||||
};
|
||||
const replaceEdit = {
|
||||
range: model.getFullModelRange(),
|
||||
text: response.code,
|
||||
};
|
||||
|
||||
resolve([replaceEdit]);
|
||||
}
|
||||
);
|
||||
});
|
||||
return [replaceEdit];
|
||||
})
|
||||
.catch(() => null);
|
||||
};
|
||||
|
||||
this.hook.handleEvent("intellisense_response", ({ ref, response }) => {
|
||||
const handler = this.handlerByRef[ref];
|
||||
|
||||
if (handler) {
|
||||
handler(response);
|
||||
delete this.handlerByRef[ref];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes an intellisense request.
|
||||
*
|
||||
* The returned promise is either resolved with a valid
|
||||
* response or rejected with null.
|
||||
*/
|
||||
__asyncIntellisenseRequest(type, props) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.hook.pushEvent(
|
||||
"intellisense_request",
|
||||
{ cell_id: this.cellId, type, ...props },
|
||||
({ ref }) => {
|
||||
if (ref) {
|
||||
this.handlerByRef[ref] = (response) => {
|
||||
if (response) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(null);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
reject(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,29 +26,43 @@ document.fonts.addEventListener("loadingdone", (event) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Define custom completion provider.
|
||||
// In our case the completion behaviour is cell-dependent,
|
||||
// so we delegate the implementation to the appropriate cell.
|
||||
// See cell/live_editor.js for more details.
|
||||
/**
|
||||
* Define custom providers for various editor features.
|
||||
*
|
||||
* In our case, each cell has its own editor and behaviour
|
||||
* of requests like completion and hover are cell dependent.
|
||||
* For this reason we delegate the implementation to the
|
||||
* specific cell by using its text model object.
|
||||
*
|
||||
* See cell/live_editor.js for more details.
|
||||
*/
|
||||
|
||||
monaco.languages.registerCompletionItemProvider("elixir", {
|
||||
provideCompletionItems: (model, position) => {
|
||||
provideCompletionItems: (model, position, context, token) => {
|
||||
if (model.__getCompletionItems) {
|
||||
return model.__getCompletionItems(model, position);
|
||||
} else {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
monaco.languages.registerHoverProvider("elixir", {
|
||||
provideHover: (model, position, token) => {
|
||||
if (model.__getHover) {
|
||||
return model.__getHover(model, position);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Define custom code formatting provider.
|
||||
// Formatting is cell agnostic, but we still delegate
|
||||
// to a cell specific implementation to communicate with LV.
|
||||
monaco.languages.registerDocumentFormattingEditProvider("elixir", {
|
||||
provideDocumentFormattingEdits: function (model, options, token) {
|
||||
provideDocumentFormattingEdits: (model, options, token) => {
|
||||
if (model.__getDocumentFormattingEdits) {
|
||||
return model.__getDocumentFormattingEdits(model);
|
||||
} else {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -118,15 +118,21 @@ defmodule Livebook.Evaluator do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously finds completion items matching the given `hint` text.
|
||||
Asynchronously handles the given intellisense request.
|
||||
|
||||
If `evaluation_ref` is given, its binding and environment are also
|
||||
used for the completion. Response is sent to the `send_to` process
|
||||
as `{:completion_response, ref, items}`.
|
||||
used as context for the intellisense. Response is sent to the `send_to`
|
||||
process as `{:intellisense_response, ref, response}`.
|
||||
"""
|
||||
@spec request_completion_items(t(), pid(), term(), String.t(), ref() | nil) :: :ok
|
||||
def request_completion_items(evaluator, send_to, ref, hint, evaluation_ref \\ nil) do
|
||||
GenServer.cast(evaluator, {:request_completion_items, send_to, ref, hint, evaluation_ref})
|
||||
@spec handle_intellisense(
|
||||
t(),
|
||||
pid(),
|
||||
term(),
|
||||
Livebook.Runtime.intellisense_request(),
|
||||
ref() | nil
|
||||
) :: :ok
|
||||
def handle_intellisense(evaluator, send_to, ref, request, evaluation_ref \\ nil) do
|
||||
GenServer.cast(evaluator, {:handle_intellisense, send_to, ref, request, evaluation_ref})
|
||||
end
|
||||
|
||||
## Callbacks
|
||||
|
|
@ -206,10 +212,10 @@ defmodule Livebook.Evaluator do
|
|||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:request_completion_items, send_to, ref, hint, evaluation_ref}, state) do
|
||||
def handle_cast({:handle_intellisense, send_to, ref, request, evaluation_ref}, state) do
|
||||
context = get_context(state, evaluation_ref)
|
||||
items = Livebook.Completion.get_completion_items(hint, context.binding, context.env)
|
||||
send(send_to, {:completion_response, ref, items})
|
||||
response = Livebook.Intellisense.handle_request(request, context.binding, context.env)
|
||||
send(send_to, {:intellisense_response, ref, response})
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
|
|
|||
516
lib/livebook/intellisense.ex
Normal file
516
lib/livebook/intellisense.ex
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
defmodule Livebook.Intellisense do
|
||||
@moduledoc false
|
||||
|
||||
# This module provides intellisense related operations
|
||||
# suitable for integration with a text editor.
|
||||
#
|
||||
# In a way, this provides the very basic features of a
|
||||
# language server that Livebook uses.
|
||||
|
||||
alias Livebook.Intellisense.Completion
|
||||
|
||||
# Configures width used for inspect and specs formatting.
|
||||
@line_length 30
|
||||
@extended_line_length 80
|
||||
|
||||
@doc """
|
||||
Resolves an intellisense request as defined by `Livebook.Runtime`.
|
||||
|
||||
In practice this function simply dispatches the request to one of
|
||||
the other public functions in this module.
|
||||
"""
|
||||
@spec handle_request(
|
||||
Livebook.Runtime.intellisense_request(),
|
||||
Code.binding(),
|
||||
Macro.Env.t()
|
||||
) :: Livebook.Runtime.intellisense_response()
|
||||
def handle_request(request, env, binding)
|
||||
|
||||
def handle_request({:completion, hint}, binding, env) do
|
||||
items = get_completion_items(hint, binding, env)
|
||||
%{items: items}
|
||||
end
|
||||
|
||||
def handle_request({:details, line, index}, binding, env) do
|
||||
get_details(line, index, binding, env)
|
||||
end
|
||||
|
||||
def handle_request({:format, code}, _binding, _env) do
|
||||
case format_code(code) do
|
||||
{:ok, code} -> %{code: code}
|
||||
:error -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats Elixir code.
|
||||
"""
|
||||
@spec format_code(String.t()) :: {:ok, String.t()} | :error
|
||||
def format_code(code) do
|
||||
try do
|
||||
formatted =
|
||||
code
|
||||
|> Code.format_string!()
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
{:ok, formatted}
|
||||
rescue
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of completion suggestions for the given `hint`.
|
||||
"""
|
||||
@spec get_completion_items(String.t(), Code.binding(), Macro.Env.t()) ::
|
||||
list(Livebook.Runtime.completion_item())
|
||||
def get_completion_items(hint, binding, env) do
|
||||
Completion.get_completion_items(hint, binding, env)
|
||||
|> Enum.map(&format_completion_item/1)
|
||||
|> Enum.sort_by(&completion_item_priority/1)
|
||||
end
|
||||
|
||||
defp format_completion_item({:variable, name, value}),
|
||||
do: %{
|
||||
label: name,
|
||||
kind: :variable,
|
||||
detail: "variable",
|
||||
documentation: value_snippet(value, @line_length),
|
||||
insert_text: name
|
||||
}
|
||||
|
||||
defp format_completion_item({:map_field, name, value}),
|
||||
do: %{
|
||||
label: name,
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: value_snippet(value, @line_length),
|
||||
insert_text: name
|
||||
}
|
||||
|
||||
defp format_completion_item({:module, name, doc_content}),
|
||||
do: %{
|
||||
label: name,
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: format_doc_content(doc_content, :short),
|
||||
insert_text: String.trim_leading(name, ":")
|
||||
}
|
||||
|
||||
defp format_completion_item({:function, module, name, arity, doc_content, signatures, spec}),
|
||||
do: %{
|
||||
label: "#{name}/#{arity}",
|
||||
kind: :function,
|
||||
detail: format_signatures(signatures, module),
|
||||
documentation:
|
||||
join_with_newlines([
|
||||
format_doc_content(doc_content, :short),
|
||||
format_spec(spec, @line_length) |> code()
|
||||
]),
|
||||
insert_text: name
|
||||
}
|
||||
|
||||
defp format_completion_item({:type, _module, name, arity, doc_content}),
|
||||
do: %{
|
||||
label: "#{name}/#{arity}",
|
||||
kind: :type,
|
||||
detail: "typespec",
|
||||
documentation: format_doc_content(doc_content, :short),
|
||||
insert_text: name
|
||||
}
|
||||
|
||||
defp format_completion_item({:module_attribute, name, doc_content}),
|
||||
do: %{
|
||||
label: name,
|
||||
kind: :variable,
|
||||
detail: "module attribute",
|
||||
documentation: format_doc_content(doc_content, :short),
|
||||
insert_text: name
|
||||
}
|
||||
|
||||
defp completion_item_priority(completion_item) do
|
||||
{completion_item_kind_priority(completion_item.kind), completion_item.label}
|
||||
end
|
||||
|
||||
@ordered_kinds [:field, :variable, :module, :function, :type]
|
||||
|
||||
defp completion_item_kind_priority(kind) when kind in @ordered_kinds do
|
||||
Enum.find_index(@ordered_kinds, &(&1 == kind))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns detailed information about identifier being
|
||||
at `index` in `line`.
|
||||
"""
|
||||
@spec get_details(String.t(), non_neg_integer(), Code.binding(), Macro.Env.t()) ::
|
||||
Livebook.Runtime.details() | nil
|
||||
def get_details(line, index, binding, env) do
|
||||
{from, to} = subject_range(line, index)
|
||||
|
||||
if from < to do
|
||||
subject = binary_part(line, from, to - from)
|
||||
|
||||
Completion.get_completion_items(subject, binding, env, exact: true)
|
||||
|> Enum.map(&format_details_item/1)
|
||||
|> Enum.uniq()
|
||||
|> case do
|
||||
[] -> nil
|
||||
contents -> %{range: %{from: from, to: to}, contents: contents}
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Reference: https://github.com/elixir-lang/elixir/blob/d1223e11fda880d5646f6385b33684d1b2ec0b9c/lib/elixir/lib/code.ex#L341-L345
|
||||
@operators '\\<>+-*/:=|&~^@%'
|
||||
@non_closing_punctuation '.,([{;'
|
||||
@closing_punctuation ')]}'
|
||||
@space '\t\s'
|
||||
@closing_identifier '?!'
|
||||
@punctuation @non_closing_punctuation ++ @closing_punctuation
|
||||
|
||||
defp subject_range(line, index) do
|
||||
{left, right} = String.split_at(line, index)
|
||||
|
||||
left =
|
||||
left
|
||||
|> String.to_charlist()
|
||||
|> Enum.reverse()
|
||||
|> consume_until(@space ++ @operators ++ (@punctuation -- '.') ++ @closing_identifier, ':@')
|
||||
|> List.to_string()
|
||||
|
||||
right =
|
||||
right
|
||||
|> String.to_charlist()
|
||||
|> consume_until(@space ++ @operators ++ @punctuation, @closing_identifier)
|
||||
|> List.to_string()
|
||||
|
||||
{index - byte_size(left), index + byte_size(right)}
|
||||
end
|
||||
|
||||
defp consume_until(acc \\ [], chars, stop, stop_include)
|
||||
|
||||
defp consume_until(acc, [], _, _), do: Enum.reverse(acc)
|
||||
|
||||
defp consume_until(acc, [char | chars], stop, stop_include) do
|
||||
cond do
|
||||
char in stop_include -> consume_until([char | acc], [], stop, stop_include)
|
||||
char in stop -> consume_until(acc, [], stop, stop_include)
|
||||
true -> consume_until([char | acc], chars, stop, stop_include)
|
||||
end
|
||||
end
|
||||
|
||||
defp format_details_item({:variable, name, value}) do
|
||||
join_with_divider([
|
||||
code(name),
|
||||
value_snippet(value, @extended_line_length)
|
||||
])
|
||||
end
|
||||
|
||||
defp format_details_item({:map_field, _name, value}) do
|
||||
join_with_divider([
|
||||
value_snippet(value, @extended_line_length)
|
||||
])
|
||||
end
|
||||
|
||||
defp format_details_item({:module, name, doc_content}) do
|
||||
join_with_divider([
|
||||
code(name),
|
||||
format_doc_content(doc_content, :all)
|
||||
])
|
||||
end
|
||||
|
||||
defp format_details_item({:function, module, _name, _arity, doc_content, signatures, spec}) do
|
||||
join_with_divider([
|
||||
format_signatures(signatures, module) |> code(),
|
||||
format_spec(spec, @extended_line_length) |> code(),
|
||||
format_doc_content(doc_content, :all)
|
||||
])
|
||||
end
|
||||
|
||||
defp format_details_item({:type, _module, name, _arity, doc_content}) do
|
||||
join_with_divider([
|
||||
code(name),
|
||||
format_doc_content(doc_content, :all)
|
||||
])
|
||||
end
|
||||
|
||||
defp format_details_item({:module_attribute, name, doc_content}) do
|
||||
join_with_divider([
|
||||
code("@" <> name),
|
||||
format_doc_content(doc_content, :all)
|
||||
])
|
||||
end
|
||||
|
||||
# Formatting helpers
|
||||
|
||||
defp join_with_divider(strings), do: join_with(strings, "\n\n---\n\n")
|
||||
|
||||
defp join_with_newlines(strings), do: join_with(strings, "\n\n")
|
||||
|
||||
defp join_with(strings, joiner) do
|
||||
case Enum.reject(strings, &is_nil/1) do
|
||||
[] -> nil
|
||||
parts -> Enum.join(parts, joiner)
|
||||
end
|
||||
end
|
||||
|
||||
defp code(nil), do: nil
|
||||
|
||||
defp code(code) do
|
||||
"""
|
||||
```
|
||||
#{code}
|
||||
```\
|
||||
"""
|
||||
end
|
||||
|
||||
defp value_snippet(value, line_length) do
|
||||
"""
|
||||
```
|
||||
#{inspect(value, pretty: true, width: line_length)}
|
||||
```\
|
||||
"""
|
||||
end
|
||||
|
||||
defp format_signatures([], _module), do: nil
|
||||
|
||||
defp format_signatures(signatures, module) do
|
||||
module_to_prefix(module) <> Enum.join(signatures, "\n")
|
||||
end
|
||||
|
||||
defp module_to_prefix(mod) do
|
||||
case Atom.to_string(mod) do
|
||||
"Elixir." <> name -> name <> "."
|
||||
name -> ":" <> name <> "."
|
||||
end
|
||||
end
|
||||
|
||||
defp format_spec(nil, _line_length), do: nil
|
||||
|
||||
defp format_spec({{name, _arity}, spec_ast_list}, line_length) do
|
||||
spec_lines =
|
||||
Enum.map(spec_ast_list, fn spec_ast ->
|
||||
spec =
|
||||
Code.Typespec.spec_to_quoted(name, spec_ast)
|
||||
|> Macro.to_string()
|
||||
|> Code.format_string!(line_length: line_length)
|
||||
|
||||
["@spec ", spec]
|
||||
end)
|
||||
|
||||
spec_lines
|
||||
|> Enum.intersperse("\n")
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
defp format_doc_content(doc, variant)
|
||||
|
||||
defp format_doc_content(nil, _variant) do
|
||||
"No documentation available"
|
||||
end
|
||||
|
||||
defp format_doc_content({"text/markdown", markdown}, :short) do
|
||||
# Extract just the first paragraph
|
||||
markdown
|
||||
|> String.split("\n\n")
|
||||
|> hd()
|
||||
|> String.trim()
|
||||
end
|
||||
|
||||
defp format_doc_content({"application/erlang+html", erlang_html}, :short) do
|
||||
# Extract just the first paragraph
|
||||
erlang_html
|
||||
|> Enum.find(&match?({:p, _, _}, &1))
|
||||
|> case do
|
||||
nil -> nil
|
||||
paragraph -> erlang_html_to_md([paragraph])
|
||||
end
|
||||
end
|
||||
|
||||
defp format_doc_content({"text/markdown", markdown}, :all) do
|
||||
markdown
|
||||
end
|
||||
|
||||
defp format_doc_content({"application/erlang+html", erlang_html}, :all) do
|
||||
erlang_html_to_md(erlang_html)
|
||||
end
|
||||
|
||||
defp format_doc_content({format, _content}, _variant) do
|
||||
raise "unknown documentation format #{inspect(format)}"
|
||||
end
|
||||
|
||||
# Erlang HTML AST
|
||||
# See https://erlang.org/doc/apps/erl_docgen/doc_storage.html#erlang-documentation-format
|
||||
|
||||
def erlang_html_to_md(ast) do
|
||||
build_md([], ast)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim()
|
||||
end
|
||||
|
||||
defp build_md(iodata, ast)
|
||||
|
||||
defp build_md(iodata, []), do: iodata
|
||||
|
||||
defp build_md(iodata, [string | ast]) when is_binary(string) do
|
||||
string |> append_inline(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{tag, _, content} | ast]) when tag in [:em, :i] do
|
||||
render_emphasis(content) |> append_inline(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{tag, _, content} | ast]) when tag in [:strong, :b] do
|
||||
render_strong(content) |> append_inline(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{:code, _, content} | ast]) do
|
||||
render_code_inline(content) |> append_inline(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{:a, attrs, content} | ast]) do
|
||||
render_link(content, attrs) |> append_inline(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{:br, _, []} | ast]) do
|
||||
render_line_break() |> append_inline(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{:p, _, content} | ast]) do
|
||||
render_paragraph(content) |> append_block(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
@headings ~w(h1 h2 h3 h4 h5 h6)a
|
||||
|
||||
defp build_md(iodata, [{tag, _, content} | ast]) when tag in @headings do
|
||||
n = 1 + Enum.find_index(@headings, &(&1 == tag))
|
||||
render_heading(n, content) |> append_block(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{:pre, _, [{:code, _, [content]}]} | ast]) do
|
||||
render_code_block(content, "erlang") |> append_block(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{:div, [{:class, class} | _], content} | ast]) do
|
||||
type = class |> to_string() |> String.upcase()
|
||||
|
||||
render_blockquote([{:p, [], [{:strong, [], [type]}]} | content])
|
||||
|> append_block(iodata)
|
||||
|> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{:ul, [{:class, "types"} | _], content} | ast]) do
|
||||
lines = Enum.map(content, fn {:li, _, line} -> line end)
|
||||
render_code_block(lines, "erlang") |> append_block(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{:ul, _, content} | ast]) do
|
||||
render_unordered_list(content) |> append_block(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{:ol, _, content} | ast]) do
|
||||
render_ordered_list(content) |> append_block(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp build_md(iodata, [{:dl, _, content} | ast]) do
|
||||
render_description_list(content) |> append_block(iodata) |> build_md(ast)
|
||||
end
|
||||
|
||||
defp append_inline(md, iodata), do: [iodata, md]
|
||||
defp append_block(md, iodata), do: [iodata, "\n", md, "\n"]
|
||||
|
||||
# Renderers
|
||||
|
||||
defp render_emphasis(content) do
|
||||
["*", build_md([], content), "*"]
|
||||
end
|
||||
|
||||
defp render_strong(content) do
|
||||
["**", build_md([], content), "**"]
|
||||
end
|
||||
|
||||
defp render_code_inline(content) do
|
||||
["`", build_md([], content), "`"]
|
||||
end
|
||||
|
||||
defp render_link(content, attrs) do
|
||||
caption = build_md([], content)
|
||||
|
||||
if href = attrs[:href] do
|
||||
["[", caption, "](", href, ")"]
|
||||
else
|
||||
caption
|
||||
end
|
||||
end
|
||||
|
||||
defp render_line_break(), do: "\\\n"
|
||||
|
||||
defp render_paragraph(content), do: erlang_html_to_md(content)
|
||||
|
||||
defp render_heading(n, content) do
|
||||
title = build_md([], content)
|
||||
[String.duplicate("#", n), " ", title]
|
||||
end
|
||||
|
||||
defp render_code_block(content, language) do
|
||||
["```", language, "\n", content, "\n```"]
|
||||
end
|
||||
|
||||
defp render_blockquote(content) do
|
||||
inner = erlang_html_to_md(content)
|
||||
|
||||
inner
|
||||
|> String.split("\n")
|
||||
|> Enum.map(&("> " <> &1))
|
||||
|> Enum.join("\n")
|
||||
end
|
||||
|
||||
defp render_unordered_list(content) do
|
||||
marker_fun = fn _index -> "* " end
|
||||
render_list(content, marker_fun, " ")
|
||||
end
|
||||
|
||||
defp render_ordered_list(content) do
|
||||
marker_fun = fn index -> "#{index + 1}. " end
|
||||
render_list(content, marker_fun, " ")
|
||||
end
|
||||
|
||||
defp render_list(items, marker_fun, indent) do
|
||||
spaced? = spaced_list_items?(items)
|
||||
item_separator = if(spaced?, do: "\n\n", else: "\n")
|
||||
|
||||
items
|
||||
|> Enum.map(fn {:li, _, content} -> erlang_html_to_md(content) end)
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn {inner, index} ->
|
||||
[first_line | lines] = String.split(inner, "\n")
|
||||
|
||||
first_line = marker_fun.(index) <> first_line
|
||||
|
||||
lines =
|
||||
Enum.map(lines, fn
|
||||
"" -> ""
|
||||
line -> indent <> line
|
||||
end)
|
||||
|
||||
Enum.intersperse([first_line | lines], "\n")
|
||||
end)
|
||||
|> Enum.intersperse(item_separator)
|
||||
end
|
||||
|
||||
defp spaced_list_items?([{:li, _, [{:p, _, _content} | _]} | _items]), do: true
|
||||
defp spaced_list_items?([_ | items]), do: spaced_list_items?(items)
|
||||
defp spaced_list_items?([]), do: false
|
||||
|
||||
defp render_description_list(content) do
|
||||
# Rewrite description list as an unordered list with pseudo heading
|
||||
content
|
||||
|> Enum.chunk_every(2)
|
||||
|> Enum.map(fn [{:dt, _, dt}, {:dd, _, dd}] ->
|
||||
{:li, [], [{:p, [], [{:strong, [], dt}]}, {:p, [], dd}]}
|
||||
end)
|
||||
|> render_unordered_list()
|
||||
end
|
||||
end
|
||||
|
|
@ -1,41 +1,48 @@
|
|||
defmodule Livebook.Completion do
|
||||
defmodule Livebook.Intellisense.Completion do
|
||||
@moduledoc false
|
||||
|
||||
# This module provides basic intellisense completion
|
||||
# suitable for text editors.
|
||||
# This module provides basic completion based on code
|
||||
# and runtime information (binding, environment).
|
||||
#
|
||||
# The implementation is based primarly on `IEx.Autocomplete`.
|
||||
# It also takes insights from `ElixirSense.Providers.Suggestion.Complete`,
|
||||
# which is a very extensive implementation used in the Elixir Language Server.
|
||||
# which is a very extensive implementation used in the
|
||||
# Elixir Language Server.
|
||||
|
||||
@type completion_item :: Livebook.Runtime.completion_item()
|
||||
@type completion_item ::
|
||||
{:variable, name(), value()}
|
||||
| {:map_field, name(), value()}
|
||||
| {:module, name(), doc_content()}
|
||||
| {:function, module(), name(), arity(), doc_content(), list(signature()), spec()}
|
||||
| {:type, module(), name(), arity(), doc_content()}
|
||||
| {:module_attribute, name(), doc_content()}
|
||||
|
||||
# Configures width used for inspect and specs formatting.
|
||||
@line_length 30
|
||||
@type name :: String.t()
|
||||
@type value :: term()
|
||||
@type doc_content :: {format :: String.t(), content :: String.t()} | nil
|
||||
@type signature :: String.t()
|
||||
@type spec :: tuple() | nil
|
||||
|
||||
@doc """
|
||||
Returns a list of completion suggestions for the given `hint`.
|
||||
Returns a list of identifiers matching the given `hint`
|
||||
together with relevant information.
|
||||
|
||||
Uses evaluation binding and environment to expand aliases,
|
||||
Evaluation binding and environment is used to expand aliases,
|
||||
imports, nested maps, etc.
|
||||
|
||||
`hint` may be a single token or line fragment like `if Enum.m`.
|
||||
|
||||
## Options
|
||||
|
||||
* `exact` - whether the hint must match exactly the given
|
||||
identifier. Defaults to `false`, resulting in prefix matching.
|
||||
"""
|
||||
@spec get_completion_items(String.t(), Code.binding(), Macro.Env.t()) :: list(completion_item())
|
||||
def get_completion_items(hint, binding, env) do
|
||||
hint
|
||||
|> complete(%{binding: binding, env: env})
|
||||
|> Enum.sort_by(&completion_item_priority/1)
|
||||
end
|
||||
@spec get_completion_items(String.t(), Code.binding(), Macro.Env.t(), keyword()) ::
|
||||
list(completion_item())
|
||||
def get_completion_items(hint, binding, env, opts \\ []) do
|
||||
matcher = if opts[:exact], do: &Kernel.==/2, else: &String.starts_with?/2
|
||||
|
||||
defp completion_item_priority(completion_item) do
|
||||
{completion_item_kind_priority(completion_item.kind), completion_item.label}
|
||||
end
|
||||
|
||||
@ordered_kinds [:field, :variable, :module, :function, :type]
|
||||
|
||||
defp completion_item_kind_priority(kind) when kind in @ordered_kinds do
|
||||
Enum.find_index(@ordered_kinds, &(&1 == kind))
|
||||
complete(hint, %{binding: binding, env: env, matcher: matcher})
|
||||
end
|
||||
|
||||
defp complete(hint, ctx) do
|
||||
|
|
@ -44,7 +51,7 @@ defmodule Livebook.Completion do
|
|||
complete_alias(List.to_string(alias), ctx)
|
||||
|
||||
{:unquoted_atom, unquoted_atom} ->
|
||||
complete_erlang_module(List.to_string(unquoted_atom))
|
||||
complete_erlang_module(List.to_string(unquoted_atom), ctx)
|
||||
|
||||
{:dot, path, hint} ->
|
||||
complete_dot(path, List.to_string(hint), ctx)
|
||||
|
|
@ -68,7 +75,7 @@ defmodule Livebook.Completion do
|
|||
complete_default(ctx)
|
||||
|
||||
{:module_attribute, attribute} ->
|
||||
complete_module_attribute(List.to_string(attribute))
|
||||
complete_module_attribute(List.to_string(attribute), ctx)
|
||||
|
||||
# :none
|
||||
_ ->
|
||||
|
|
@ -81,13 +88,13 @@ defmodule Livebook.Completion do
|
|||
defp complete_dot(path, hint, ctx) do
|
||||
case expand_dot_path(path, ctx) do
|
||||
{:ok, mod} when is_atom(mod) and hint == "" ->
|
||||
complete_module_member(mod, hint) ++ complete_module(mod, hint)
|
||||
complete_module_member(mod, hint, ctx) ++ complete_module(mod, hint, ctx)
|
||||
|
||||
{:ok, mod} when is_atom(mod) ->
|
||||
complete_module_member(mod, hint)
|
||||
complete_module_member(mod, hint, ctx)
|
||||
|
||||
{:ok, map} when is_map(map) ->
|
||||
complete_map_field(map, hint)
|
||||
complete_map_field(map, hint, ctx)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
|
|
@ -124,16 +131,16 @@ defmodule Livebook.Completion do
|
|||
defp complete_alias(hint, ctx) do
|
||||
case split_at_last_occurrence(hint, ".") do
|
||||
{hint, ""} ->
|
||||
complete_elixir_root_module(hint) ++ complete_env_alias(hint, ctx)
|
||||
complete_elixir_root_module(hint, ctx) ++ complete_env_alias(hint, ctx)
|
||||
|
||||
{alias, hint} ->
|
||||
mod = expand_alias(alias, ctx)
|
||||
complete_module(mod, hint)
|
||||
complete_module(mod, hint, ctx)
|
||||
end
|
||||
end
|
||||
|
||||
defp complete_module_member(mod, hint) do
|
||||
complete_module_function(mod, hint) ++ complete_module_type(mod, hint)
|
||||
defp complete_module_member(mod, hint, ctx) do
|
||||
complete_module_function(mod, hint, ctx) ++ complete_module_type(mod, hint, ctx)
|
||||
end
|
||||
|
||||
defp complete_local_or_var(hint, ctx) do
|
||||
|
|
@ -145,10 +152,10 @@ defmodule Livebook.Completion do
|
|||
ctx.env
|
||||
|> imports_from_env()
|
||||
|> Enum.flat_map(fn {mod, funs} ->
|
||||
complete_module_function(mod, hint, funs)
|
||||
complete_module_function(mod, hint, ctx, funs)
|
||||
end)
|
||||
|
||||
special_forms = complete_module_function(Kernel.SpecialForms, hint)
|
||||
special_forms = complete_module_function(Kernel.SpecialForms, hint, ctx)
|
||||
|
||||
imports ++ special_forms
|
||||
end
|
||||
|
|
@ -156,51 +163,24 @@ defmodule Livebook.Completion do
|
|||
defp complete_variable(hint, ctx) do
|
||||
for {key, value} <- ctx.binding,
|
||||
name = Atom.to_string(key),
|
||||
String.starts_with?(name, hint),
|
||||
do: %{
|
||||
label: name,
|
||||
kind: :variable,
|
||||
detail: "variable",
|
||||
documentation: value_docstr(value),
|
||||
insert_text: name
|
||||
}
|
||||
ctx.matcher.(name, hint),
|
||||
do: {:variable, name, value}
|
||||
end
|
||||
|
||||
defp complete_map_field(map, hint) do
|
||||
defp complete_map_field(map, hint, ctx) do
|
||||
# Note: we need Map.to_list/1 in case this is a struct
|
||||
for {key, value} <- Map.to_list(map),
|
||||
is_atom(key),
|
||||
name = Atom.to_string(key),
|
||||
String.starts_with?(name, hint),
|
||||
do: %{
|
||||
label: name,
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: value_docstr(value),
|
||||
insert_text: name
|
||||
}
|
||||
ctx.matcher.(name, hint),
|
||||
do: {:map_field, name, value}
|
||||
end
|
||||
|
||||
defp value_docstr(value) do
|
||||
"""
|
||||
```
|
||||
#{inspect(value, pretty: true, width: @line_length)}
|
||||
```\
|
||||
"""
|
||||
end
|
||||
|
||||
defp complete_erlang_module(hint) do
|
||||
for mod <- get_matching_modules(hint),
|
||||
defp complete_erlang_module(hint, ctx) do
|
||||
for mod <- get_matching_modules(hint, ctx),
|
||||
usable_as_unquoted_module?(mod),
|
||||
name = Atom.to_string(mod) do
|
||||
%{
|
||||
label: name,
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: mod |> get_module_doc_content() |> format_doc_content(),
|
||||
insert_text: name
|
||||
}
|
||||
end
|
||||
name = ":" <> Atom.to_string(mod),
|
||||
do: {:module, name, get_module_doc_content(mod)}
|
||||
end
|
||||
|
||||
# Converts alias string to module atom with regard to the given env
|
||||
|
|
@ -217,23 +197,16 @@ defmodule Livebook.Completion do
|
|||
defp complete_env_alias(hint, ctx) do
|
||||
for {alias, mod} <- ctx.env.aliases,
|
||||
[name] = Module.split(alias),
|
||||
String.starts_with?(name, hint) do
|
||||
%{
|
||||
label: name,
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: mod |> get_module_doc_content() |> format_doc_content(),
|
||||
insert_text: name
|
||||
}
|
||||
end
|
||||
ctx.matcher.(name, hint),
|
||||
do: {:module, name, get_module_doc_content(mod)}
|
||||
end
|
||||
|
||||
defp complete_module(base_mod, hint) do
|
||||
defp complete_module(base_mod, hint, ctx) do
|
||||
# Note: we specifically don't want further completion
|
||||
# if `base_mod` is an Erlang module.
|
||||
|
||||
if base_mod == Elixir or elixir_module?(base_mod) do
|
||||
complete_elixir_module(base_mod, hint)
|
||||
complete_elixir_module(base_mod, hint, ctx)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
|
@ -243,34 +216,25 @@ defmodule Livebook.Completion do
|
|||
mod |> Atom.to_string() |> String.starts_with?("Elixir.")
|
||||
end
|
||||
|
||||
defp complete_elixir_root_module(hint) do
|
||||
items = complete_elixir_module(Elixir, hint)
|
||||
defp complete_elixir_root_module(hint, ctx) do
|
||||
items = complete_elixir_module(Elixir, hint, ctx)
|
||||
|
||||
# `Elixir` is not a existing module name, but `Elixir.Enum` is,
|
||||
# so if the user types `Eli` the completion should include `Elixir`.
|
||||
if String.starts_with?("Elixir", hint) do
|
||||
[
|
||||
%{
|
||||
label: "Elixir",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: nil,
|
||||
insert_text: "Elixir"
|
||||
}
|
||||
| items
|
||||
]
|
||||
if ctx.matcher.("Elixir", hint) do
|
||||
[{:module, "Elixir", nil} | items]
|
||||
else
|
||||
items
|
||||
end
|
||||
end
|
||||
|
||||
defp complete_elixir_module(base_mod, hint) do
|
||||
defp complete_elixir_module(base_mod, hint, ctx) do
|
||||
# Note: `base_mod` may be `Elixir`, even though it's not a valid module
|
||||
|
||||
match_prefix = "#{base_mod}.#{hint}"
|
||||
depth = match_prefix |> Module.split() |> length()
|
||||
|
||||
for mod <- get_matching_modules(match_prefix),
|
||||
for mod <- get_matching_modules(match_prefix, ctx),
|
||||
parts = Module.split(mod),
|
||||
length(parts) >= depth,
|
||||
name = Enum.at(parts, depth - 1),
|
||||
|
|
@ -281,13 +245,7 @@ defmodule Livebook.Completion do
|
|||
valid_alias_piece?("." <> name),
|
||||
mod = parts |> Enum.take(depth) |> Module.concat(),
|
||||
uniq: true,
|
||||
do: %{
|
||||
label: name,
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: mod |> get_module_doc_content() |> format_doc_content(),
|
||||
insert_text: name
|
||||
}
|
||||
do: {:module, name, get_module_doc_content(mod)}
|
||||
end
|
||||
|
||||
defp valid_alias_piece?(<<?., char, rest::binary>>) when char in ?A..?Z,
|
||||
|
|
@ -309,9 +267,9 @@ defmodule Livebook.Completion do
|
|||
Code.Identifier.classify(mod) != :other
|
||||
end
|
||||
|
||||
defp get_matching_modules(hint) do
|
||||
defp get_matching_modules(hint, ctx) do
|
||||
get_modules()
|
||||
|> Enum.filter(&String.starts_with?(Atom.to_string(&1), hint))
|
||||
|> Enum.filter(&ctx.matcher.(Atom.to_string(&1), hint))
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
|
|
@ -340,7 +298,7 @@ defmodule Livebook.Completion do
|
|||
:ets.match(:ac_tab, {{:loaded, :"$1"}, :_})
|
||||
end
|
||||
|
||||
defp complete_module_function(mod, hint, funs \\ nil) do
|
||||
defp complete_module_function(mod, hint, ctx, funs \\ nil) do
|
||||
if ensure_loaded?(mod) do
|
||||
{format, docs} = get_docs(mod, [:function, :macro])
|
||||
specs = get_specs(mod)
|
||||
|
|
@ -350,24 +308,17 @@ defmodule Livebook.Completion do
|
|||
funs
|
||||
|> Enum.filter(fn {name, _arity} ->
|
||||
name = Atom.to_string(name)
|
||||
String.starts_with?(name, hint)
|
||||
ctx.matcher.(name, hint)
|
||||
end)
|
||||
|> Enum.map(fn {name, arity} ->
|
||||
base_arity = Map.get(funs_with_base_arity, {name, arity}, arity)
|
||||
doc = find_doc(docs, {name, base_arity})
|
||||
spec = find_spec(specs, {name, base_arity})
|
||||
|
||||
docstr = doc |> doc_content(format) |> format_doc_content()
|
||||
signatures = doc |> doc_signatures() |> format_signatures(mod)
|
||||
spec = format_spec(spec)
|
||||
doc_content = doc_content(doc, format)
|
||||
signatures = doc_signatures(doc)
|
||||
|
||||
%{
|
||||
label: "#{name}/#{arity}",
|
||||
kind: :function,
|
||||
detail: signatures,
|
||||
documentation: documentation_join([docstr, spec]),
|
||||
insert_text: Atom.to_string(name)
|
||||
}
|
||||
{:function, mod, Atom.to_string(name), arity, doc_content, signatures, spec}
|
||||
end)
|
||||
else
|
||||
[]
|
||||
|
|
@ -428,78 +379,6 @@ defmodule Livebook.Completion do
|
|||
defp doc_content({_, _, _, %{"en" => docstr}, _}, format), do: {format, docstr}
|
||||
defp doc_content(_doc, _format), do: nil
|
||||
|
||||
defp documentation_join(list) do
|
||||
case Enum.reject(list, &is_nil/1) do
|
||||
[] -> nil
|
||||
parts -> Enum.join(parts, "\n\n")
|
||||
end
|
||||
end
|
||||
|
||||
defp format_signatures([], _mod), do: nil
|
||||
|
||||
defp format_signatures(signatures, mod) do
|
||||
mod_to_prefix(mod) <> Enum.join(signatures, "\n")
|
||||
end
|
||||
|
||||
defp mod_to_prefix(mod) do
|
||||
case Atom.to_string(mod) do
|
||||
"Elixir." <> name -> name <> "."
|
||||
name -> name <> "."
|
||||
end
|
||||
end
|
||||
|
||||
defp format_doc_content(nil), do: nil
|
||||
|
||||
defp format_doc_content({"text/markdown", markdown}) do
|
||||
# Extract just the first paragraph
|
||||
markdown
|
||||
|> String.split("\n\n")
|
||||
|> hd()
|
||||
|> String.trim()
|
||||
end
|
||||
|
||||
defp format_doc_content({"application/erlang+html", erlang_html}) do
|
||||
case erlang_html do
|
||||
# Extract just the first paragraph
|
||||
[{:p, _, inner} | _] ->
|
||||
inner
|
||||
|> text_from_erlang_html()
|
||||
|> String.trim()
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp format_doc_content(_), do: nil
|
||||
|
||||
def text_from_erlang_html(ast) when is_list(ast) do
|
||||
ast
|
||||
|> Enum.map(&text_from_erlang_html/1)
|
||||
|> Enum.join("")
|
||||
end
|
||||
|
||||
def text_from_erlang_html(ast) when is_binary(ast), do: ast
|
||||
def text_from_erlang_html({_, _, ast}), do: text_from_erlang_html(ast)
|
||||
|
||||
defp format_spec(nil), do: nil
|
||||
|
||||
defp format_spec({{name, _arity}, spec_ast_list}) do
|
||||
spec_lines =
|
||||
Enum.map(spec_ast_list, fn spec_ast ->
|
||||
spec =
|
||||
Code.Typespec.spec_to_quoted(name, spec_ast)
|
||||
|> Macro.to_string()
|
||||
|> Code.format_string!(line_length: @line_length)
|
||||
|
||||
["@spec ", spec]
|
||||
end)
|
||||
|
||||
["```", spec_lines, "```"]
|
||||
|> Enum.intersperse("\n")
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
defp exports(mod) do
|
||||
if Code.ensure_loaded?(mod) and function_exported?(mod, :__info__, 1) do
|
||||
mod.__info__(:macros) ++ (mod.__info__(:functions) -- [__info__: 1])
|
||||
|
|
@ -508,26 +387,20 @@ defmodule Livebook.Completion do
|
|||
end
|
||||
end
|
||||
|
||||
defp complete_module_type(mod, hint) do
|
||||
defp complete_module_type(mod, hint, ctx) do
|
||||
{format, docs} = get_docs(mod, [:type])
|
||||
types = get_module_types(mod)
|
||||
|
||||
types
|
||||
|> Enum.filter(fn {name, _arity} ->
|
||||
name = Atom.to_string(name)
|
||||
String.starts_with?(name, hint)
|
||||
ctx.matcher.(name, hint)
|
||||
end)
|
||||
|> Enum.map(fn {name, arity} ->
|
||||
doc = find_doc(docs, {name, arity})
|
||||
docstr = doc |> doc_content(format) |> format_doc_content()
|
||||
doc_content = doc_content(doc, format)
|
||||
|
||||
%{
|
||||
label: "#{name}/#{arity}",
|
||||
kind: :type,
|
||||
detail: "typespec",
|
||||
documentation: docstr,
|
||||
insert_text: Atom.to_string(name)
|
||||
}
|
||||
{:type, mod, Atom.to_string(name), arity, doc_content}
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
@ -559,17 +432,10 @@ defmodule Livebook.Completion do
|
|||
end
|
||||
end
|
||||
|
||||
defp complete_module_attribute(hint) do
|
||||
defp complete_module_attribute(hint, ctx) do
|
||||
for {attribute, info} <- Module.reserved_attributes(),
|
||||
name = Atom.to_string(attribute),
|
||||
String.starts_with?(name, hint) do
|
||||
%{
|
||||
label: name,
|
||||
kind: :variable,
|
||||
detail: "module attribute",
|
||||
documentation: info.doc,
|
||||
insert_text: name
|
||||
}
|
||||
end
|
||||
ctx.matcher.(name, hint),
|
||||
do: {:module_attribute, name, {"text/markdown", info.doc}}
|
||||
end
|
||||
end
|
||||
|
|
@ -31,8 +31,36 @@ defprotocol Livebook.Runtime do
|
|||
@type locator :: {container_ref(), evaluation_ref() | nil}
|
||||
|
||||
@typedoc """
|
||||
A single completion result.
|
||||
Recognised intellisense requests.
|
||||
"""
|
||||
@type intellisense_request ::
|
||||
completion_request()
|
||||
| details_request()
|
||||
| format_request()
|
||||
|
||||
@typedoc """
|
||||
Expected intellisense responses.
|
||||
|
||||
Responding with `nil` indicates there is no relevant reply
|
||||
and effectively aborts the request, so it's suitable for
|
||||
error cases.
|
||||
"""
|
||||
@type intellisense_response ::
|
||||
nil
|
||||
| completion_response()
|
||||
| details_response()
|
||||
| format_response()
|
||||
|
||||
@typedoc """
|
||||
Looks up a list of identifiers that are suitable code
|
||||
completions for the given hint.
|
||||
"""
|
||||
@type completion_request :: {:completion, hint :: String.t()}
|
||||
|
||||
@type completion_response :: %{
|
||||
items: list(completion_item())
|
||||
}
|
||||
|
||||
@type completion_item :: %{
|
||||
label: String.t(),
|
||||
kind: completion_item_kind(),
|
||||
|
|
@ -43,6 +71,28 @@ defprotocol Livebook.Runtime do
|
|||
|
||||
@type completion_item_kind :: :function | :module | :type | :variable | :field
|
||||
|
||||
@typedoc """
|
||||
Looks up more details about an identifier found at `index` in `line`.
|
||||
"""
|
||||
@type details_request :: {:details, line :: String.t(), index :: non_neg_integer()}
|
||||
|
||||
@type details_response :: %{
|
||||
range: %{
|
||||
from: non_neg_integer(),
|
||||
to: non_neg_integer()
|
||||
},
|
||||
contents: list(String.t())
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
Formats the given code snippet.
|
||||
"""
|
||||
@type format_request :: {:format, code :: String.t()}
|
||||
|
||||
@type format_response :: %{
|
||||
code: String.t()
|
||||
}
|
||||
|
||||
@doc """
|
||||
Sets the caller as runtime owner.
|
||||
|
||||
|
|
@ -127,19 +177,20 @@ defprotocol Livebook.Runtime do
|
|||
def drop_container(runtime, container_ref)
|
||||
|
||||
@doc """
|
||||
Asynchronously finds completion items matching the given `hint` text.
|
||||
Asynchronously handles an intellisense request.
|
||||
|
||||
The given `locator` idenfities an evaluation, which bindings
|
||||
and environment are used to provide a more relevant completion
|
||||
results. If there's no appropriate evaluation, `nil` refs can
|
||||
be provided.
|
||||
This part of runtime functionality is used to provide
|
||||
language and context specific intellisense features in
|
||||
the text editor.
|
||||
|
||||
Completion response is sent to the `send_to` process as
|
||||
`{:completion_response, ref, items}`, where `items` is a
|
||||
list of `t:Livebook.Runtime.completion_item/0`.
|
||||
The response is sent to the `send_to` process as
|
||||
`{:intellisense_response, ref, response}`.
|
||||
|
||||
The given `locator` idenfities an evaluation that may be used
|
||||
as context when resolving the request (if relevant).
|
||||
"""
|
||||
@spec request_completion_items(t(), pid(), term(), String.t(), locator()) :: :ok
|
||||
def request_completion_items(runtime, send_to, completion_ref, hint, locator)
|
||||
@spec handle_intellisense(t(), pid(), reference(), intellisense_request(), locator()) :: :ok
|
||||
def handle_intellisense(runtime, send_to, ref, request, locator)
|
||||
|
||||
@doc """
|
||||
Synchronously starts a runtime of the same type with the
|
||||
|
|
|
|||
|
|
@ -61,14 +61,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do
|
|||
ErlDist.RuntimeServer.drop_container(runtime.server_pid, container_ref)
|
||||
end
|
||||
|
||||
def request_completion_items(runtime, send_to, completion_ref, hint, locator) do
|
||||
ErlDist.RuntimeServer.request_completion_items(
|
||||
runtime.server_pid,
|
||||
send_to,
|
||||
completion_ref,
|
||||
hint,
|
||||
locator
|
||||
)
|
||||
def handle_intellisense(runtime, send_to, ref, request, locator) do
|
||||
ErlDist.RuntimeServer.handle_intellisense(runtime.server_pid, send_to, ref, request, locator)
|
||||
end
|
||||
|
||||
def duplicate(runtime) do
|
||||
|
|
|
|||
|
|
@ -89,14 +89,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do
|
|||
ErlDist.RuntimeServer.drop_container(runtime.server_pid, container_ref)
|
||||
end
|
||||
|
||||
def request_completion_items(runtime, send_to, completion_ref, hint, locator) do
|
||||
ErlDist.RuntimeServer.request_completion_items(
|
||||
runtime.server_pid,
|
||||
send_to,
|
||||
completion_ref,
|
||||
hint,
|
||||
locator
|
||||
)
|
||||
def handle_intellisense(runtime, send_to, ref, request, locator) do
|
||||
ErlDist.RuntimeServer.handle_intellisense(runtime.server_pid, send_to, ref, request, locator)
|
||||
end
|
||||
|
||||
def duplicate(_runtime) do
|
||||
|
|
|
|||
|
|
@ -63,14 +63,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do
|
|||
ErlDist.RuntimeServer.drop_container(runtime.server_pid, container_ref)
|
||||
end
|
||||
|
||||
def request_completion_items(runtime, send_to, completion_ref, hint, locator) do
|
||||
ErlDist.RuntimeServer.request_completion_items(
|
||||
runtime.server_pid,
|
||||
send_to,
|
||||
completion_ref,
|
||||
hint,
|
||||
locator
|
||||
)
|
||||
def handle_intellisense(runtime, send_to, ref, request, locator) do
|
||||
ErlDist.RuntimeServer.handle_intellisense(runtime.server_pid, send_to, ref, request, locator)
|
||||
end
|
||||
|
||||
def duplicate(_runtime) do
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ defmodule Livebook.Runtime.ErlDist do
|
|||
Livebook.Evaluator,
|
||||
Livebook.Evaluator.IOProxy,
|
||||
Livebook.Evaluator.DefaultFormatter,
|
||||
Livebook.Completion,
|
||||
Livebook.Intellisense,
|
||||
Livebook.Intellisense.Completion,
|
||||
Livebook.Runtime.ErlDist,
|
||||
Livebook.Runtime.ErlDist.NodeManager,
|
||||
Livebook.Runtime.ErlDist.RuntimeServer,
|
||||
|
|
|
|||
|
|
@ -78,19 +78,24 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Asynchronously sends completion request for the given
|
||||
`hint` text.
|
||||
Asynchronously sends an intellisense request to the server.
|
||||
|
||||
The completion request is forwarded to `Livebook.Evaluator`
|
||||
process that belongs to the given container. If there's no
|
||||
evaluator, there's also no binding and environment, so a
|
||||
generic completion is handled by a temporary process.
|
||||
Completions are forwarded to `Livebook.Evaluator` process
|
||||
that belongs to the given container. If there's no evaluator,
|
||||
there's also no binding and environment, so a generic
|
||||
completion is handled by a temporary process.
|
||||
|
||||
See `Livebook.Runtime` for more details.
|
||||
"""
|
||||
@spec request_completion_items(pid(), pid(), term(), String.t(), Runtime.locator()) :: :ok
|
||||
def request_completion_items(pid, send_to, completion_ref, hint, locator) do
|
||||
GenServer.cast(pid, {:request_completion_items, send_to, completion_ref, hint, locator})
|
||||
@spec handle_intellisense(
|
||||
pid(),
|
||||
pid(),
|
||||
reference(),
|
||||
Runtime.intellisense_request(),
|
||||
Runtime.locator()
|
||||
) :: :ok
|
||||
def handle_intellisense(pid, send_to, ref, request, locator) do
|
||||
GenServer.cast(pid, {:handle_intellisense, send_to, ref, request, locator})
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -206,19 +211,19 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
|||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast(
|
||||
{:request_completion_items, send_to, ref, hint, {container_ref, evaluation_ref}},
|
||||
state
|
||||
) do
|
||||
if evaluator = Map.get(state.evaluators, container_ref) do
|
||||
Evaluator.request_completion_items(evaluator, send_to, ref, hint, evaluation_ref)
|
||||
def handle_cast({:handle_intellisense, send_to, ref, request, locator}, state) do
|
||||
{container_ref, evaluation_ref} = locator
|
||||
evaluator = state.evaluators[container_ref]
|
||||
|
||||
if evaluator != nil and elem(request, 0) not in [:format] do
|
||||
Evaluator.handle_intellisense(evaluator, send_to, ref, request, evaluation_ref)
|
||||
else
|
||||
# Since there's no evaluator, we may as well get the completion items here.
|
||||
# Handle the request in a temporary process using an empty evaluation context
|
||||
Task.Supervisor.start_child(state.completion_supervisor, fn ->
|
||||
binding = []
|
||||
env = :elixir.env_for_eval([])
|
||||
items = Livebook.Completion.get_completion_items(hint, binding, env)
|
||||
send(send_to, {:completion_response, ref, items})
|
||||
response = Livebook.Intellisense.handle_request(request, binding, env)
|
||||
send(send_to, {:intellisense_response, ref, response})
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -142,14 +142,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.MixStandalone do
|
|||
ErlDist.RuntimeServer.drop_container(runtime.server_pid, container_ref)
|
||||
end
|
||||
|
||||
def request_completion_items(runtime, send_to, completion_ref, hint, locator) do
|
||||
ErlDist.RuntimeServer.request_completion_items(
|
||||
runtime.server_pid,
|
||||
send_to,
|
||||
completion_ref,
|
||||
hint,
|
||||
locator
|
||||
)
|
||||
def handle_intellisense(runtime, send_to, ref, request, locator) do
|
||||
ErlDist.RuntimeServer.handle_intellisense(runtime.server_pid, send_to, ref, request, locator)
|
||||
end
|
||||
|
||||
def duplicate(runtime) do
|
||||
|
|
|
|||
|
|
@ -613,23 +613,43 @@ defmodule LivebookWeb.SessionLive do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("completion_request", %{"hint" => hint, "cell_id" => cell_id}, socket) do
|
||||
def handle_event("intellisense_request", %{"cell_id" => cell_id} = params, socket) do
|
||||
request =
|
||||
case params do
|
||||
%{"type" => "completion", "hint" => hint} ->
|
||||
{:completion, hint}
|
||||
|
||||
%{"type" => "details", "line" => line, "index" => index} ->
|
||||
{:details, line, index}
|
||||
|
||||
%{"type" => "format", "code" => code} ->
|
||||
{:format, code}
|
||||
end
|
||||
|
||||
data = socket.private.data
|
||||
|
||||
with {:ok, cell, section} <- Notebook.fetch_cell_and_section(data.notebook, cell_id) do
|
||||
if data.runtime do
|
||||
completion_ref = make_ref()
|
||||
ref = make_ref()
|
||||
prev_locator = Session.find_prev_locator(data.notebook, cell, section)
|
||||
Runtime.request_completion_items(data.runtime, self(), completion_ref, hint, prev_locator)
|
||||
Runtime.handle_intellisense(data.runtime, self(), ref, request, prev_locator)
|
||||
|
||||
{:reply, %{"completion_ref" => inspect(completion_ref)}, socket}
|
||||
{:reply, %{"ref" => inspect(ref)}, socket}
|
||||
else
|
||||
{:reply, %{"completion_ref" => nil},
|
||||
put_flash(
|
||||
socket,
|
||||
:info,
|
||||
"You need to start a runtime (or evaluate a cell) for accurate completion"
|
||||
)}
|
||||
info =
|
||||
case params["type"] do
|
||||
"completion" ->
|
||||
"You need to start a runtime (or evaluate a cell) for code completion"
|
||||
|
||||
"format" ->
|
||||
"You need to start a runtime (or evaluate a cell) to enable code formatting"
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
socket = if info, do: put_flash(socket, :info, info), else: socket
|
||||
{:reply, %{"ref" => nil}, socket}
|
||||
end
|
||||
else
|
||||
_ -> {:noreply, socket}
|
||||
|
|
@ -724,9 +744,9 @@ defmodule LivebookWeb.SessionLive do
|
|||
|> push_redirect(to: Routes.home_path(socket, :page))}
|
||||
end
|
||||
|
||||
def handle_info({:completion_response, ref, items}, socket) do
|
||||
payload = %{"completion_ref" => inspect(ref), "items" => items}
|
||||
{:noreply, push_event(socket, "completion_response", payload)}
|
||||
def handle_info({:intellisense_response, ref, response}, socket) do
|
||||
payload = %{"ref" => inspect(ref), "response" => response}
|
||||
{:noreply, push_event(socket, "intellisense_response", payload)}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
|
|
|
|||
|
|
@ -1,933 +0,0 @@
|
|||
defmodule Livebook.CompletionTest.Utils do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Returns `{binding, env}` resulting from evaluating
|
||||
the given block of code in a fresh context.
|
||||
"""
|
||||
defmacro eval(do: block) do
|
||||
binding = []
|
||||
env = :elixir.env_for_eval([])
|
||||
{_, binding, env} = :elixir.eval_quoted(block, binding, env)
|
||||
|
||||
quote do
|
||||
{unquote(Macro.escape(binding)), unquote(Macro.escape(env))}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Livebook.CompletionTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
import Livebook.CompletionTest.Utils
|
||||
|
||||
alias Livebook.Completion
|
||||
|
||||
test "completion when no hint given" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
length_item = %{
|
||||
label: "length/1",
|
||||
kind: :function,
|
||||
detail: "Kernel.length(list)",
|
||||
documentation: """
|
||||
Returns the length of `list`.
|
||||
|
||||
```
|
||||
@spec length(list()) ::
|
||||
non_neg_integer()
|
||||
```\
|
||||
""",
|
||||
insert_text: "length"
|
||||
}
|
||||
|
||||
assert length_item in Completion.get_completion_items("", binding, env)
|
||||
assert length_item in Completion.get_completion_items("Enum.map(list, ", binding, env)
|
||||
end
|
||||
|
||||
@tag :erl_docs
|
||||
test "Erlang module completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "zlib",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation:
|
||||
"This module provides an API for the zlib library (www.zlib.net). It is used to compress and decompress data. The data format is described by RFC 1950, RFC 1951, and RFC 1952.",
|
||||
insert_text: "zlib"
|
||||
}
|
||||
] = Completion.get_completion_items(":zl", binding, env)
|
||||
end
|
||||
|
||||
test "Erlang module no completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [] = Completion.get_completion_items(":unknown", binding, env)
|
||||
end
|
||||
|
||||
test "Erlang module multiple values completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "user",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: _user_doc,
|
||||
insert_text: "user"
|
||||
},
|
||||
%{
|
||||
label: "user_drv",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: _user_drv_doc,
|
||||
insert_text: "user_drv"
|
||||
},
|
||||
%{
|
||||
label: "user_sup",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: _user_sup_doc,
|
||||
insert_text: "user_sup"
|
||||
}
|
||||
] = Completion.get_completion_items(":user", binding, env)
|
||||
end
|
||||
|
||||
@tag :erl_docs
|
||||
test "Erlang root completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
lists_item = %{
|
||||
label: "lists",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "This module contains functions for list processing.",
|
||||
insert_text: "lists"
|
||||
}
|
||||
|
||||
assert lists_item in Completion.get_completion_items(":", binding, env)
|
||||
assert lists_item in Completion.get_completion_items(" :", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir proxy" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{
|
||||
label: "Elixir",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: nil,
|
||||
insert_text: "Elixir"
|
||||
} in Completion.get_completion_items("E", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir module completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "Enum",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Provides a set of algorithms to work with enumerables.",
|
||||
insert_text: "Enum"
|
||||
},
|
||||
%{
|
||||
label: "Enumerable",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Enumerable protocol used by `Enum` and `Stream` modules.",
|
||||
insert_text: "Enumerable"
|
||||
}
|
||||
] = Completion.get_completion_items("En", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "Enumerable",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Enumerable protocol used by `Enum` and `Stream` modules.",
|
||||
insert_text: "Enumerable"
|
||||
}
|
||||
] = Completion.get_completion_items("Enumera", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir type completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "from/0",
|
||||
kind: :type,
|
||||
detail: "typespec",
|
||||
documentation: "Tuple describing the client of a call request.",
|
||||
insert_text: "from"
|
||||
}
|
||||
] = Completion.get_completion_items("GenServer.fr", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "name/0",
|
||||
kind: :type,
|
||||
detail: "typespec",
|
||||
documentation: _name_doc,
|
||||
insert_text: "name"
|
||||
},
|
||||
%{
|
||||
label: "name_all/0",
|
||||
kind: :type,
|
||||
detail: "typespec",
|
||||
documentation: _name_all_doc,
|
||||
insert_text: "name_all"
|
||||
}
|
||||
] = Completion.get_completion_items(":file.nam", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir completion with self" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "Enumerable",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Enumerable protocol used by `Enum` and `Stream` modules.",
|
||||
insert_text: "Enumerable"
|
||||
}
|
||||
] = Completion.get_completion_items("Enumerable", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir completion on modules from load path" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{
|
||||
label: "Jason",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "A blazing fast JSON parser and generator in pure Elixir.",
|
||||
insert_text: "Jason"
|
||||
} in Completion.get_completion_items("Jas", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir no completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [] = Completion.get_completion_items(".", binding, env)
|
||||
assert [] = Completion.get_completion_items("Xyz", binding, env)
|
||||
assert [] = Completion.get_completion_items("x.Foo", binding, env)
|
||||
assert [] = Completion.get_completion_items("x.Foo.get_by", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir root submodule completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "Access",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Key-based access to data structures.",
|
||||
insert_text: "Access"
|
||||
}
|
||||
] = Completion.get_completion_items("Elixir.Acce", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir submodule completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "ANSI",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Functionality to render ANSI escape sequences.",
|
||||
insert_text: "ANSI"
|
||||
}
|
||||
] = Completion.get_completion_items("IO.AN", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir submodule no completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [] = Completion.get_completion_items("IEx.Xyz", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir function completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "version/0",
|
||||
kind: :function,
|
||||
detail: "System.version()",
|
||||
documentation: """
|
||||
Elixir version information.
|
||||
|
||||
```
|
||||
@spec version() :: String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "version"
|
||||
}
|
||||
] = Completion.get_completion_items("System.ve", binding, env)
|
||||
end
|
||||
|
||||
@tag :erl_docs
|
||||
test "Erlang function completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{
|
||||
label: "gzip/1",
|
||||
kind: :function,
|
||||
detail: "zlib.gzip/1",
|
||||
documentation: """
|
||||
Compresses data with gz headers and checksum.
|
||||
|
||||
```
|
||||
@spec gzip(data) :: compressed
|
||||
when data: iodata(),
|
||||
compressed: binary()
|
||||
```\
|
||||
""",
|
||||
insert_text: "gzip"
|
||||
} in Completion.get_completion_items(":zlib.gz", binding, env)
|
||||
end
|
||||
|
||||
test "function completion with arity" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{
|
||||
label: "concat/1",
|
||||
kind: :function,
|
||||
detail: "Enum.concat(enumerables)",
|
||||
documentation: """
|
||||
Given an enumerable of enumerables, concatenates the `enumerables` into
|
||||
a single list.
|
||||
|
||||
```
|
||||
@spec concat(t()) :: t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "concat"
|
||||
} in Completion.get_completion_items("Enum.concat/", binding, env)
|
||||
end
|
||||
|
||||
test "function completion same name with different arities" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "concat/1",
|
||||
kind: :function,
|
||||
detail: "Enum.concat(enumerables)",
|
||||
documentation: """
|
||||
Given an enumerable of enumerables, concatenates the `enumerables` into
|
||||
a single list.
|
||||
|
||||
```
|
||||
@spec concat(t()) :: t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "concat"
|
||||
},
|
||||
%{
|
||||
label: "concat/2",
|
||||
kind: :function,
|
||||
detail: "Enum.concat(left, right)",
|
||||
documentation: """
|
||||
Concatenates the enumerable on the `right` with the enumerable on the
|
||||
`left`.
|
||||
|
||||
```
|
||||
@spec concat(t(), t()) :: t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "concat"
|
||||
}
|
||||
] = Completion.get_completion_items("Enum.concat", binding, env)
|
||||
end
|
||||
|
||||
test "function completion when has default args then documentation all arities have docs" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "join/1",
|
||||
kind: :function,
|
||||
detail: ~S{Enum.join(enumerable, joiner \\ "")},
|
||||
documentation: """
|
||||
Joins the given `enumerable` into a string using `joiner` as a
|
||||
separator.
|
||||
|
||||
```
|
||||
@spec join(t(), String.t()) ::
|
||||
String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "join"
|
||||
},
|
||||
%{
|
||||
label: "join/2",
|
||||
kind: :function,
|
||||
detail: ~S{Enum.join(enumerable, joiner \\ "")},
|
||||
documentation: """
|
||||
Joins the given `enumerable` into a string using `joiner` as a
|
||||
separator.
|
||||
|
||||
```
|
||||
@spec join(t(), String.t()) ::
|
||||
String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "join"
|
||||
}
|
||||
] = Completion.get_completion_items("Enum.jo", binding, env)
|
||||
end
|
||||
|
||||
test "function completion using a variable bound to a module" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
mod = System
|
||||
end
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "version/0",
|
||||
kind: :function,
|
||||
detail: "System.version()",
|
||||
documentation: """
|
||||
Elixir version information.
|
||||
|
||||
```
|
||||
@spec version() :: String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "version"
|
||||
}
|
||||
] = Completion.get_completion_items("mod.ve", binding, env)
|
||||
end
|
||||
|
||||
test "map atom key completion" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
map = %{
|
||||
foo: 1,
|
||||
bar_1: ~r/pattern/,
|
||||
bar_2: true
|
||||
}
|
||||
end
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "bar_1",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: ~s{```\n~r/pattern/\n```},
|
||||
insert_text: "bar_1"
|
||||
},
|
||||
%{
|
||||
label: "bar_2",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: ~s{```\ntrue\n```},
|
||||
insert_text: "bar_2"
|
||||
},
|
||||
%{
|
||||
label: "foo",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: ~s{```\n1\n```},
|
||||
insert_text: "foo"
|
||||
}
|
||||
] = Completion.get_completion_items("map.", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "foo",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: ~s{```\n1\n```},
|
||||
insert_text: "foo"
|
||||
}
|
||||
] = Completion.get_completion_items("map.f", binding, env)
|
||||
end
|
||||
|
||||
test "nested map atom key completion" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
map = %{
|
||||
nested: %{
|
||||
deeply: %{
|
||||
foo: 1,
|
||||
bar_1: 23,
|
||||
bar_2: 14,
|
||||
mod: System
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "nested",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: """
|
||||
```
|
||||
%{
|
||||
deeply: %{
|
||||
bar_1: 23,
|
||||
bar_2: 14,
|
||||
foo: 1,
|
||||
mod: System
|
||||
}
|
||||
}
|
||||
```\
|
||||
""",
|
||||
insert_text: "nested"
|
||||
}
|
||||
] = Completion.get_completion_items("map.nest", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "foo",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: ~s{```\n1\n```},
|
||||
insert_text: "foo"
|
||||
}
|
||||
] = Completion.get_completion_items("map.nested.deeply.f", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "version/0",
|
||||
kind: :function,
|
||||
detail: "System.version()",
|
||||
documentation: """
|
||||
Elixir version information.
|
||||
|
||||
```
|
||||
@spec version() :: String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "version"
|
||||
}
|
||||
] = Completion.get_completion_items("map.nested.deeply.mod.ve", binding, env)
|
||||
|
||||
assert [] = Completion.get_completion_items("map.non.existent", binding, env)
|
||||
end
|
||||
|
||||
test "map string key completion is not supported" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
map = %{"foo" => 1}
|
||||
end
|
||||
|
||||
assert [] = Completion.get_completion_items("map.f", binding, env)
|
||||
end
|
||||
|
||||
test "autocompletion off a bound variable only works for modules and maps" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
num = 5
|
||||
map = %{nested: %{num: 23}}
|
||||
end
|
||||
|
||||
assert [] = Completion.get_completion_items("num.print", binding, env)
|
||||
assert [] = Completion.get_completion_items("map.nested.num.f", binding, env)
|
||||
end
|
||||
|
||||
test "autocompletion using access syntax does is not supported" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
map = %{nested: %{deeply: %{num: 23}}}
|
||||
end
|
||||
|
||||
assert [] = Completion.get_completion_items("map[:nested][:deeply].n", binding, env)
|
||||
assert [] = Completion.get_completion_items("map[:nested].deeply.n", binding, env)
|
||||
assert [] = Completion.get_completion_items("map.nested.[:deeply].n", binding, env)
|
||||
end
|
||||
|
||||
test "macro completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "is_nil/1",
|
||||
kind: :function,
|
||||
detail: "Kernel.is_nil(term)",
|
||||
documentation: "Returns `true` if `term` is `nil`, `false` otherwise.",
|
||||
insert_text: "is_nil"
|
||||
}
|
||||
] = Completion.get_completion_items("Kernel.is_ni", binding, env)
|
||||
end
|
||||
|
||||
test "special forms completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "quote/2",
|
||||
kind: :function,
|
||||
detail: "Kernel.SpecialForms.quote(opts, block)",
|
||||
documentation: "Gets the representation of any expression.",
|
||||
insert_text: "quote"
|
||||
}
|
||||
] = Completion.get_completion_items("quot", binding, env)
|
||||
end
|
||||
|
||||
test "kernel import completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "put_in/2",
|
||||
kind: :function,
|
||||
detail: "Kernel.put_in(path, value)",
|
||||
documentation: "Puts a value in a nested structure via the given `path`.",
|
||||
insert_text: "put_in"
|
||||
},
|
||||
%{
|
||||
label: "put_in/3",
|
||||
kind: :function,
|
||||
detail: "Kernel.put_in(data, keys, value)",
|
||||
documentation: """
|
||||
Puts a value in a nested structure.
|
||||
|
||||
```
|
||||
@spec put_in(
|
||||
Access.t(),
|
||||
[term(), ...],
|
||||
term()
|
||||
) :: Access.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "put_in"
|
||||
}
|
||||
] = Completion.get_completion_items("put_i", binding, env)
|
||||
end
|
||||
|
||||
test "variable name completion" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
number = 3
|
||||
numbats = ["numbat", "numbat"]
|
||||
nothing = nil
|
||||
end
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "numbats",
|
||||
kind: :variable,
|
||||
detail: "variable",
|
||||
documentation: ~s{```\n["numbat", "numbat"]\n```},
|
||||
insert_text: "numbats"
|
||||
}
|
||||
] = Completion.get_completion_items("numba", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "numbats",
|
||||
kind: :variable,
|
||||
detail: "variable",
|
||||
documentation: ~s{```\n["numbat", "numbat"]\n```},
|
||||
insert_text: "numbats"
|
||||
},
|
||||
%{
|
||||
label: "number",
|
||||
kind: :variable,
|
||||
detail: "variable",
|
||||
documentation: ~s{```\n3\n```},
|
||||
insert_text: "number"
|
||||
}
|
||||
] = Completion.get_completion_items("num", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "nothing",
|
||||
kind: :variable,
|
||||
detail: "variable",
|
||||
documentation: ~s{```\nnil\n```},
|
||||
insert_text: "nothing"
|
||||
},
|
||||
%{label: "node/0"},
|
||||
%{label: "node/1"},
|
||||
%{label: "not/1"}
|
||||
] = Completion.get_completion_items("no", binding, env)
|
||||
end
|
||||
|
||||
test "completion of manually imported functions and macros" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
import Enum
|
||||
import System, only: [version: 0]
|
||||
import Protocol
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "take/2"},
|
||||
%{label: "take_every/2"},
|
||||
%{label: "take_random/2"},
|
||||
%{label: "take_while/2"}
|
||||
] = Completion.get_completion_items("take", binding, env)
|
||||
|
||||
assert %{
|
||||
label: "version/0",
|
||||
kind: :function,
|
||||
detail: "System.version()",
|
||||
documentation: """
|
||||
Elixir version information.
|
||||
|
||||
```
|
||||
@spec version() :: String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "version"
|
||||
} in Completion.get_completion_items("v", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "derive/2"},
|
||||
%{label: "derive/3"}
|
||||
] = Completion.get_completion_items("der", binding, env)
|
||||
end
|
||||
|
||||
test "ignores quoted variables when performing variable completion" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
quote do
|
||||
var!(my_var_1, Elixir) = 1
|
||||
end
|
||||
|
||||
my_var_2 = 2
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "my_var_2"}
|
||||
] = Completion.get_completion_items("my_var", binding, env)
|
||||
end
|
||||
|
||||
test "completion inside expression" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Completion.get_completion_items("1 En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Completion.get_completion_items("foo(En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Completion.get_completion_items("Test En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Completion.get_completion_items("foo(x,En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Completion.get_completion_items("[En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Completion.get_completion_items("{En", binding, env)
|
||||
end
|
||||
|
||||
test "ampersand completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Completion.get_completion_items("&En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "all?/1"},
|
||||
%{label: "all?/2"}
|
||||
] = Completion.get_completion_items("&Enum.al", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "all?/1"},
|
||||
%{label: "all?/2"}
|
||||
] = Completion.get_completion_items("f = &Enum.al", binding, env)
|
||||
end
|
||||
|
||||
test "negation operator completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{label: "is_binary/1"}
|
||||
] = Completion.get_completion_items("!is_bin", binding, env)
|
||||
end
|
||||
|
||||
test "pin operator completion" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
my_variable = 2
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "my_variable"}
|
||||
] = Completion.get_completion_items("^my_va", binding, env)
|
||||
end
|
||||
|
||||
defmodule SublevelTest.LevelA.LevelB do
|
||||
end
|
||||
|
||||
test "Elixir completion sublevel" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{label: "LevelA"}
|
||||
] =
|
||||
Completion.get_completion_items(
|
||||
"Livebook.CompletionTest.SublevelTest.",
|
||||
binding,
|
||||
env
|
||||
)
|
||||
end
|
||||
|
||||
test "complete aliases of Elixir modules" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
alias List, as: MyList
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "MyList"}
|
||||
] = Completion.get_completion_items("MyL", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "to_integer/1"},
|
||||
%{label: "to_integer/2"}
|
||||
] = Completion.get_completion_items("MyList.to_integ", binding, env)
|
||||
end
|
||||
|
||||
@tag :erl_docs
|
||||
test "complete aliases of Erlang modules" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
alias :lists, as: EList
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "EList"}
|
||||
] = Completion.get_completion_items("EL", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "map/2"},
|
||||
%{label: "mapfoldl/3"},
|
||||
%{label: "mapfoldr/3"}
|
||||
] = Completion.get_completion_items("EList.map", binding, env)
|
||||
|
||||
assert %{
|
||||
label: "max/1",
|
||||
kind: :function,
|
||||
detail: "lists.max/1",
|
||||
documentation: """
|
||||
Returns the first element of List that compares greater than or equal to all other elements of List.
|
||||
|
||||
```
|
||||
@spec max(list) :: max
|
||||
when list: [t, ...],
|
||||
max: t,
|
||||
t: term()
|
||||
```\
|
||||
""",
|
||||
insert_text: "max"
|
||||
} in Completion.get_completion_items("EList.", binding, env)
|
||||
|
||||
assert [] = Completion.get_completion_items("EList.Invalid", binding, env)
|
||||
end
|
||||
|
||||
test "completion for functions added when compiled module is reloaded" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
{:module, _, bytecode, _} =
|
||||
defmodule Sample do
|
||||
def foo(), do: 0
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "foo/0"}
|
||||
] = Completion.get_completion_items("Livebook.CompletionTest.Sample.foo", binding, env)
|
||||
|
||||
Code.compiler_options(ignore_module_conflict: true)
|
||||
|
||||
defmodule Sample do
|
||||
def foo(), do: 0
|
||||
def foobar(), do: 0
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "foo/0"},
|
||||
%{label: "foobar/0"}
|
||||
] = Completion.get_completion_items("Livebook.CompletionTest.Sample.foo", binding, env)
|
||||
after
|
||||
Code.compiler_options(ignore_module_conflict: false)
|
||||
:code.purge(Sample)
|
||||
:code.delete(Sample)
|
||||
end
|
||||
|
||||
defmodule MyStruct do
|
||||
defstruct [:my_val]
|
||||
end
|
||||
|
||||
test "completion for struct names" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{label: "MyStruct"}
|
||||
] = Completion.get_completion_items("Livebook.CompletionTest.MyStr", binding, env)
|
||||
end
|
||||
|
||||
test "completion for struct keys" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
struct = %Livebook.CompletionTest.MyStruct{}
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "my_val"}
|
||||
] = Completion.get_completion_items("struct.my", binding, env)
|
||||
end
|
||||
|
||||
test "ignore invalid Elixir module literals" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
defmodule(:"Elixir.Livebook.CompletionTest.Unicodé", do: nil)
|
||||
|
||||
assert [] = Completion.get_completion_items("Livebook.CompletionTest.Unicod", binding, env)
|
||||
after
|
||||
:code.purge(:"Elixir.Livebook.CompletionTest.Unicodé")
|
||||
:code.delete(:"Elixir.Livebook.CompletionTest.Unicodé")
|
||||
end
|
||||
|
||||
test "known Elixir module attributes completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "moduledoc",
|
||||
kind: :variable,
|
||||
detail: "module attribute",
|
||||
documentation: "Provides documentation for the current module.",
|
||||
insert_text: "moduledoc"
|
||||
}
|
||||
] = Completion.get_completion_items("@modu", binding, env)
|
||||
end
|
||||
|
||||
test "handles calls on module attribute" do
|
||||
{binding, env} = eval(do: nil)
|
||||
assert [] = Completion.get_completion_items("@attr.value", binding, env)
|
||||
end
|
||||
end
|
||||
|
|
@ -219,10 +219,11 @@ defmodule Livebook.EvaluatorTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "request_completion_items/5" do
|
||||
describe "handle_intellisense/5 given completion request" do
|
||||
test "sends completion response to the given process", %{evaluator: evaluator} do
|
||||
Evaluator.request_completion_items(evaluator, self(), :comp_ref, "System.ver")
|
||||
assert_receive {:completion_response, :comp_ref, [%{label: "version/0"}]}, 1_000
|
||||
request = {:completion, "System.ver"}
|
||||
Evaluator.handle_intellisense(evaluator, self(), :ref, request)
|
||||
assert_receive {:intellisense_response, :ref, %{items: [%{label: "version/0"}]}}, 1_000
|
||||
end
|
||||
|
||||
test "given evaluation reference uses its bindings and env", %{evaluator: evaluator} do
|
||||
|
|
@ -234,12 +235,14 @@ defmodule Livebook.EvaluatorTest do
|
|||
Evaluator.evaluate_code(evaluator, self(), code, :code_1)
|
||||
assert_receive {:evaluation_response, :code_1, _, %{evaluation_time_ms: _time_ms}}
|
||||
|
||||
Evaluator.request_completion_items(evaluator, self(), :comp_ref, "num", :code_1)
|
||||
assert_receive {:completion_response, :comp_ref, [%{label: "number"}]}, 1_000
|
||||
request = {:completion, "num"}
|
||||
Evaluator.handle_intellisense(evaluator, self(), :ref, request, :code_1)
|
||||
assert_receive {:intellisense_response, :ref, %{items: [%{label: "number"}]}}, 1_000
|
||||
|
||||
Evaluator.request_completion_items(evaluator, self(), :comp_ref, "ANSI.brigh", :code_1)
|
||||
request = {:completion, "ANSI.brigh"}
|
||||
Evaluator.handle_intellisense(evaluator, self(), :ref, request, :code_1)
|
||||
|
||||
assert_receive {:completion_response, :comp_ref, [%{label: "bright/0"}]}, 1_000
|
||||
assert_receive {:intellisense_response, :ref, %{items: [%{label: "bright/0"}]}}, 1_000
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
995
test/livebook/intellisense_test.exs
Normal file
995
test/livebook/intellisense_test.exs
Normal file
|
|
@ -0,0 +1,995 @@
|
|||
defmodule Livebook.IntellisenseTest.Utils do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Returns `{binding, env}` resulting from evaluating
|
||||
the given block of code in a fresh context.
|
||||
"""
|
||||
defmacro eval(do: block) do
|
||||
binding = []
|
||||
env = :elixir.env_for_eval([])
|
||||
{_, binding, env} = :elixir.eval_quoted(block, binding, env)
|
||||
|
||||
quote do
|
||||
{unquote(Macro.escape(binding)), unquote(Macro.escape(env))}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Livebook.IntellisenseTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
import Livebook.IntellisenseTest.Utils
|
||||
|
||||
alias Livebook.Intellisense
|
||||
|
||||
describe "get_completion_items/3" do
|
||||
test "completion when no hint given" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
length_item = %{
|
||||
label: "length/1",
|
||||
kind: :function,
|
||||
detail: "Kernel.length(list)",
|
||||
documentation: """
|
||||
Returns the length of `list`.
|
||||
|
||||
```
|
||||
@spec length(list()) ::
|
||||
non_neg_integer()
|
||||
```\
|
||||
""",
|
||||
insert_text: "length"
|
||||
}
|
||||
|
||||
assert length_item in Intellisense.get_completion_items("", binding, env)
|
||||
assert length_item in Intellisense.get_completion_items("Enum.map(list, ", binding, env)
|
||||
end
|
||||
|
||||
@tag :erl_docs
|
||||
test "Erlang module completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: ":zlib",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation:
|
||||
"This module provides an API for the zlib library ([www.zlib.net](http://www.zlib.net)). It is used to compress and decompress data. The data format is described by [RFC 1950](https://www.ietf.org/rfc/rfc1950.txt), [RFC 1951](https://www.ietf.org/rfc/rfc1951.txt), and [RFC 1952](https://www.ietf.org/rfc/rfc1952.txt).",
|
||||
insert_text: "zlib"
|
||||
}
|
||||
] = Intellisense.get_completion_items(":zl", binding, env)
|
||||
end
|
||||
|
||||
test "Erlang module no completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [] = Intellisense.get_completion_items(":unknown", binding, env)
|
||||
end
|
||||
|
||||
test "Erlang module multiple values completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: ":user",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: _user_doc,
|
||||
insert_text: "user"
|
||||
},
|
||||
%{
|
||||
label: ":user_drv",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: _user_drv_doc,
|
||||
insert_text: "user_drv"
|
||||
},
|
||||
%{
|
||||
label: ":user_sup",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: _user_sup_doc,
|
||||
insert_text: "user_sup"
|
||||
}
|
||||
] = Intellisense.get_completion_items(":user", binding, env)
|
||||
end
|
||||
|
||||
@tag :erl_docs
|
||||
test "Erlang root completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
lists_item = %{
|
||||
label: ":lists",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "This module contains functions for list processing.",
|
||||
insert_text: "lists"
|
||||
}
|
||||
|
||||
assert lists_item in Intellisense.get_completion_items(":", binding, env)
|
||||
assert lists_item in Intellisense.get_completion_items(" :", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir proxy" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{
|
||||
label: "Elixir",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "No documentation available",
|
||||
insert_text: "Elixir"
|
||||
} in Intellisense.get_completion_items("E", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir module completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "Enum",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Provides a set of algorithms to work with enumerables.",
|
||||
insert_text: "Enum"
|
||||
},
|
||||
%{
|
||||
label: "Enumerable",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Enumerable protocol used by `Enum` and `Stream` modules.",
|
||||
insert_text: "Enumerable"
|
||||
}
|
||||
] = Intellisense.get_completion_items("En", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "Enumerable",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Enumerable protocol used by `Enum` and `Stream` modules.",
|
||||
insert_text: "Enumerable"
|
||||
}
|
||||
] = Intellisense.get_completion_items("Enumera", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir type completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "from/0",
|
||||
kind: :type,
|
||||
detail: "typespec",
|
||||
documentation: "Tuple describing the client of a call request.",
|
||||
insert_text: "from"
|
||||
}
|
||||
] = Intellisense.get_completion_items("GenServer.fr", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "name/0",
|
||||
kind: :type,
|
||||
detail: "typespec",
|
||||
documentation: _name_doc,
|
||||
insert_text: "name"
|
||||
},
|
||||
%{
|
||||
label: "name_all/0",
|
||||
kind: :type,
|
||||
detail: "typespec",
|
||||
documentation: _name_all_doc,
|
||||
insert_text: "name_all"
|
||||
}
|
||||
] = Intellisense.get_completion_items(":file.nam", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir completion with self" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "Enumerable",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Enumerable protocol used by `Enum` and `Stream` modules.",
|
||||
insert_text: "Enumerable"
|
||||
}
|
||||
] = Intellisense.get_completion_items("Enumerable", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir completion on modules from load path" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{
|
||||
label: "Jason",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "A blazing fast JSON parser and generator in pure Elixir.",
|
||||
insert_text: "Jason"
|
||||
} in Intellisense.get_completion_items("Jas", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir no completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [] = Intellisense.get_completion_items(".", binding, env)
|
||||
assert [] = Intellisense.get_completion_items("Xyz", binding, env)
|
||||
assert [] = Intellisense.get_completion_items("x.Foo", binding, env)
|
||||
assert [] = Intellisense.get_completion_items("x.Foo.get_by", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir root submodule completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "Access",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Key-based access to data structures.",
|
||||
insert_text: "Access"
|
||||
}
|
||||
] = Intellisense.get_completion_items("Elixir.Acce", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir submodule completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "ANSI",
|
||||
kind: :module,
|
||||
detail: "module",
|
||||
documentation: "Functionality to render ANSI escape sequences.",
|
||||
insert_text: "ANSI"
|
||||
}
|
||||
] = Intellisense.get_completion_items("IO.AN", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir submodule no completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [] = Intellisense.get_completion_items("IEx.Xyz", binding, env)
|
||||
end
|
||||
|
||||
test "Elixir function completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "version/0",
|
||||
kind: :function,
|
||||
detail: "System.version()",
|
||||
documentation: """
|
||||
Elixir version information.
|
||||
|
||||
```
|
||||
@spec version() :: String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "version"
|
||||
}
|
||||
] = Intellisense.get_completion_items("System.ve", binding, env)
|
||||
end
|
||||
|
||||
@tag :erl_docs
|
||||
test "Erlang function completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{
|
||||
label: "gzip/1",
|
||||
kind: :function,
|
||||
detail: ":zlib.gzip/1",
|
||||
documentation: """
|
||||
Compresses data with gz headers and checksum.
|
||||
|
||||
```
|
||||
@spec gzip(data) :: compressed
|
||||
when data: iodata(),
|
||||
compressed: binary()
|
||||
```\
|
||||
""",
|
||||
insert_text: "gzip"
|
||||
} in Intellisense.get_completion_items(":zlib.gz", binding, env)
|
||||
end
|
||||
|
||||
test "function completion with arity" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{
|
||||
label: "concat/1",
|
||||
kind: :function,
|
||||
detail: "Enum.concat(enumerables)",
|
||||
documentation: """
|
||||
Given an enumerable of enumerables, concatenates the `enumerables` into
|
||||
a single list.
|
||||
|
||||
```
|
||||
@spec concat(t()) :: t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "concat"
|
||||
} in Intellisense.get_completion_items("Enum.concat/", binding, env)
|
||||
end
|
||||
|
||||
test "function completion same name with different arities" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "concat/1",
|
||||
kind: :function,
|
||||
detail: "Enum.concat(enumerables)",
|
||||
documentation: """
|
||||
Given an enumerable of enumerables, concatenates the `enumerables` into
|
||||
a single list.
|
||||
|
||||
```
|
||||
@spec concat(t()) :: t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "concat"
|
||||
},
|
||||
%{
|
||||
label: "concat/2",
|
||||
kind: :function,
|
||||
detail: "Enum.concat(left, right)",
|
||||
documentation: """
|
||||
Concatenates the enumerable on the `right` with the enumerable on the
|
||||
`left`.
|
||||
|
||||
```
|
||||
@spec concat(t(), t()) :: t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "concat"
|
||||
}
|
||||
] = Intellisense.get_completion_items("Enum.concat", binding, env)
|
||||
end
|
||||
|
||||
test "function completion when has default args then documentation all arities have docs" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "join/1",
|
||||
kind: :function,
|
||||
detail: ~S{Enum.join(enumerable, joiner \\ "")},
|
||||
documentation: """
|
||||
Joins the given `enumerable` into a string using `joiner` as a
|
||||
separator.
|
||||
|
||||
```
|
||||
@spec join(t(), String.t()) ::
|
||||
String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "join"
|
||||
},
|
||||
%{
|
||||
label: "join/2",
|
||||
kind: :function,
|
||||
detail: ~S{Enum.join(enumerable, joiner \\ "")},
|
||||
documentation: """
|
||||
Joins the given `enumerable` into a string using `joiner` as a
|
||||
separator.
|
||||
|
||||
```
|
||||
@spec join(t(), String.t()) ::
|
||||
String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "join"
|
||||
}
|
||||
] = Intellisense.get_completion_items("Enum.jo", binding, env)
|
||||
end
|
||||
|
||||
test "function completion using a variable bound to a module" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
mod = System
|
||||
end
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "version/0",
|
||||
kind: :function,
|
||||
detail: "System.version()",
|
||||
documentation: """
|
||||
Elixir version information.
|
||||
|
||||
```
|
||||
@spec version() :: String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "version"
|
||||
}
|
||||
] = Intellisense.get_completion_items("mod.ve", binding, env)
|
||||
end
|
||||
|
||||
test "map atom key completion" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
map = %{
|
||||
foo: 1,
|
||||
bar_1: ~r/pattern/,
|
||||
bar_2: true
|
||||
}
|
||||
end
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "bar_1",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: ~s{```\n~r/pattern/\n```},
|
||||
insert_text: "bar_1"
|
||||
},
|
||||
%{
|
||||
label: "bar_2",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: ~s{```\ntrue\n```},
|
||||
insert_text: "bar_2"
|
||||
},
|
||||
%{
|
||||
label: "foo",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: ~s{```\n1\n```},
|
||||
insert_text: "foo"
|
||||
}
|
||||
] = Intellisense.get_completion_items("map.", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "foo",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: ~s{```\n1\n```},
|
||||
insert_text: "foo"
|
||||
}
|
||||
] = Intellisense.get_completion_items("map.f", binding, env)
|
||||
end
|
||||
|
||||
test "nested map atom key completion" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
map = %{
|
||||
nested: %{
|
||||
deeply: %{
|
||||
foo: 1,
|
||||
bar_1: 23,
|
||||
bar_2: 14,
|
||||
mod: System
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "nested",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: """
|
||||
```
|
||||
%{
|
||||
deeply: %{
|
||||
bar_1: 23,
|
||||
bar_2: 14,
|
||||
foo: 1,
|
||||
mod: System
|
||||
}
|
||||
}
|
||||
```\
|
||||
""",
|
||||
insert_text: "nested"
|
||||
}
|
||||
] = Intellisense.get_completion_items("map.nest", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "foo",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: ~s{```\n1\n```},
|
||||
insert_text: "foo"
|
||||
}
|
||||
] = Intellisense.get_completion_items("map.nested.deeply.f", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "version/0",
|
||||
kind: :function,
|
||||
detail: "System.version()",
|
||||
documentation: """
|
||||
Elixir version information.
|
||||
|
||||
```
|
||||
@spec version() :: String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "version"
|
||||
}
|
||||
] = Intellisense.get_completion_items("map.nested.deeply.mod.ve", binding, env)
|
||||
|
||||
assert [] = Intellisense.get_completion_items("map.non.existent", binding, env)
|
||||
end
|
||||
|
||||
test "map string key completion is not supported" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
map = %{"foo" => 1}
|
||||
end
|
||||
|
||||
assert [] = Intellisense.get_completion_items("map.f", binding, env)
|
||||
end
|
||||
|
||||
test "autocompletion off a bound variable only works for modules and maps" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
num = 5
|
||||
map = %{nested: %{num: 23}}
|
||||
end
|
||||
|
||||
assert [] = Intellisense.get_completion_items("num.print", binding, env)
|
||||
assert [] = Intellisense.get_completion_items("map.nested.num.f", binding, env)
|
||||
end
|
||||
|
||||
test "autocompletion using access syntax does is not supported" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
map = %{nested: %{deeply: %{num: 23}}}
|
||||
end
|
||||
|
||||
assert [] = Intellisense.get_completion_items("map[:nested][:deeply].n", binding, env)
|
||||
assert [] = Intellisense.get_completion_items("map[:nested].deeply.n", binding, env)
|
||||
assert [] = Intellisense.get_completion_items("map.nested.[:deeply].n", binding, env)
|
||||
end
|
||||
|
||||
test "macro completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "is_nil/1",
|
||||
kind: :function,
|
||||
detail: "Kernel.is_nil(term)",
|
||||
documentation: "Returns `true` if `term` is `nil`, `false` otherwise.",
|
||||
insert_text: "is_nil"
|
||||
}
|
||||
] = Intellisense.get_completion_items("Kernel.is_ni", binding, env)
|
||||
end
|
||||
|
||||
test "special forms completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "quote/2",
|
||||
kind: :function,
|
||||
detail: "Kernel.SpecialForms.quote(opts, block)",
|
||||
documentation: "Gets the representation of any expression.",
|
||||
insert_text: "quote"
|
||||
}
|
||||
] = Intellisense.get_completion_items("quot", binding, env)
|
||||
end
|
||||
|
||||
test "kernel import completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "put_in/2",
|
||||
kind: :function,
|
||||
detail: "Kernel.put_in(path, value)",
|
||||
documentation: "Puts a value in a nested structure via the given `path`.",
|
||||
insert_text: "put_in"
|
||||
},
|
||||
%{
|
||||
label: "put_in/3",
|
||||
kind: :function,
|
||||
detail: "Kernel.put_in(data, keys, value)",
|
||||
documentation: """
|
||||
Puts a value in a nested structure.
|
||||
|
||||
```
|
||||
@spec put_in(
|
||||
Access.t(),
|
||||
[term(), ...],
|
||||
term()
|
||||
) :: Access.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "put_in"
|
||||
}
|
||||
] = Intellisense.get_completion_items("put_i", binding, env)
|
||||
end
|
||||
|
||||
test "variable name completion" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
number = 3
|
||||
numbats = ["numbat", "numbat"]
|
||||
nothing = nil
|
||||
end
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "numbats",
|
||||
kind: :variable,
|
||||
detail: "variable",
|
||||
documentation: ~s{```\n["numbat", "numbat"]\n```},
|
||||
insert_text: "numbats"
|
||||
}
|
||||
] = Intellisense.get_completion_items("numba", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "numbats",
|
||||
kind: :variable,
|
||||
detail: "variable",
|
||||
documentation: ~s{```\n["numbat", "numbat"]\n```},
|
||||
insert_text: "numbats"
|
||||
},
|
||||
%{
|
||||
label: "number",
|
||||
kind: :variable,
|
||||
detail: "variable",
|
||||
documentation: ~s{```\n3\n```},
|
||||
insert_text: "number"
|
||||
}
|
||||
] = Intellisense.get_completion_items("num", binding, env)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "nothing",
|
||||
kind: :variable,
|
||||
detail: "variable",
|
||||
documentation: ~s{```\nnil\n```},
|
||||
insert_text: "nothing"
|
||||
},
|
||||
%{label: "node/0"},
|
||||
%{label: "node/1"},
|
||||
%{label: "not/1"}
|
||||
] = Intellisense.get_completion_items("no", binding, env)
|
||||
end
|
||||
|
||||
test "completion of manually imported functions and macros" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
import Enum
|
||||
import System, only: [version: 0]
|
||||
import Protocol
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "take/2"},
|
||||
%{label: "take_every/2"},
|
||||
%{label: "take_random/2"},
|
||||
%{label: "take_while/2"}
|
||||
] = Intellisense.get_completion_items("take", binding, env)
|
||||
|
||||
assert %{
|
||||
label: "version/0",
|
||||
kind: :function,
|
||||
detail: "System.version()",
|
||||
documentation: """
|
||||
Elixir version information.
|
||||
|
||||
```
|
||||
@spec version() :: String.t()
|
||||
```\
|
||||
""",
|
||||
insert_text: "version"
|
||||
} in Intellisense.get_completion_items("v", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "derive/2"},
|
||||
%{label: "derive/3"}
|
||||
] = Intellisense.get_completion_items("der", binding, env)
|
||||
end
|
||||
|
||||
test "ignores quoted variables when performing variable completion" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
quote do
|
||||
var!(my_var_1, Elixir) = 1
|
||||
end
|
||||
|
||||
my_var_2 = 2
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "my_var_2"}
|
||||
] = Intellisense.get_completion_items("my_var", binding, env)
|
||||
end
|
||||
|
||||
test "completion inside expression" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Intellisense.get_completion_items("1 En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Intellisense.get_completion_items("foo(En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Intellisense.get_completion_items("Test En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Intellisense.get_completion_items("foo(x,En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Intellisense.get_completion_items("[En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Intellisense.get_completion_items("{En", binding, env)
|
||||
end
|
||||
|
||||
test "ampersand completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{label: "Enum"},
|
||||
%{label: "Enumerable"}
|
||||
] = Intellisense.get_completion_items("&En", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "all?/1"},
|
||||
%{label: "all?/2"}
|
||||
] = Intellisense.get_completion_items("&Enum.al", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "all?/1"},
|
||||
%{label: "all?/2"}
|
||||
] = Intellisense.get_completion_items("f = &Enum.al", binding, env)
|
||||
end
|
||||
|
||||
test "negation operator completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{label: "is_binary/1"}
|
||||
] = Intellisense.get_completion_items("!is_bin", binding, env)
|
||||
end
|
||||
|
||||
test "pin operator completion" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
my_variable = 2
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "my_variable"}
|
||||
] = Intellisense.get_completion_items("^my_va", binding, env)
|
||||
end
|
||||
|
||||
defmodule SublevelTest.LevelA.LevelB do
|
||||
end
|
||||
|
||||
test "Elixir completion sublevel" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{label: "LevelA"}
|
||||
] =
|
||||
Intellisense.get_completion_items(
|
||||
"Livebook.IntellisenseTest.SublevelTest.",
|
||||
binding,
|
||||
env
|
||||
)
|
||||
end
|
||||
|
||||
test "complete aliases of Elixir modules" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
alias List, as: MyList
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "MyList"}
|
||||
] = Intellisense.get_completion_items("MyL", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "to_integer/1"},
|
||||
%{label: "to_integer/2"}
|
||||
] = Intellisense.get_completion_items("MyList.to_integ", binding, env)
|
||||
end
|
||||
|
||||
@tag :erl_docs
|
||||
test "complete aliases of Erlang modules" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
alias :lists, as: EList
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "EList"}
|
||||
] = Intellisense.get_completion_items("EL", binding, env)
|
||||
|
||||
assert [
|
||||
%{label: "map/2"},
|
||||
%{label: "mapfoldl/3"},
|
||||
%{label: "mapfoldr/3"}
|
||||
] = Intellisense.get_completion_items("EList.map", binding, env)
|
||||
|
||||
assert %{
|
||||
label: "max/1",
|
||||
kind: :function,
|
||||
detail: ":lists.max/1",
|
||||
documentation: """
|
||||
Returns the first element of `List` that compares greater than or equal to all other elements of `List`.
|
||||
|
||||
```
|
||||
@spec max(list) :: max
|
||||
when list: [t, ...],
|
||||
max: t,
|
||||
t: term()
|
||||
```\
|
||||
""",
|
||||
insert_text: "max"
|
||||
} in Intellisense.get_completion_items("EList.", binding, env)
|
||||
|
||||
assert [] = Intellisense.get_completion_items("EList.Invalid", binding, env)
|
||||
end
|
||||
|
||||
test "completion for functions added when compiled module is reloaded" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
{:module, _, bytecode, _} =
|
||||
defmodule Sample do
|
||||
def foo(), do: 0
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "foo/0"}
|
||||
] =
|
||||
Intellisense.get_completion_items(
|
||||
"Livebook.IntellisenseTest.Sample.foo",
|
||||
binding,
|
||||
env
|
||||
)
|
||||
|
||||
Code.compiler_options(ignore_module_conflict: true)
|
||||
|
||||
defmodule Sample do
|
||||
def foo(), do: 0
|
||||
def foobar(), do: 0
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "foo/0"},
|
||||
%{label: "foobar/0"}
|
||||
] =
|
||||
Intellisense.get_completion_items(
|
||||
"Livebook.IntellisenseTest.Sample.foo",
|
||||
binding,
|
||||
env
|
||||
)
|
||||
after
|
||||
Code.compiler_options(ignore_module_conflict: false)
|
||||
:code.purge(Sample)
|
||||
:code.delete(Sample)
|
||||
end
|
||||
|
||||
defmodule MyStruct do
|
||||
defstruct [:my_val]
|
||||
end
|
||||
|
||||
test "completion for struct names" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{label: "MyStruct"}
|
||||
] =
|
||||
Intellisense.get_completion_items("Livebook.IntellisenseTest.MyStr", binding, env)
|
||||
end
|
||||
|
||||
test "completion for struct keys" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
struct = %Livebook.IntellisenseTest.MyStruct{}
|
||||
end
|
||||
|
||||
assert [
|
||||
%{label: "my_val"}
|
||||
] = Intellisense.get_completion_items("struct.my", binding, env)
|
||||
end
|
||||
|
||||
test "ignore invalid Elixir module literals" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
defmodule(:"Elixir.Livebook.IntellisenseTest.Unicodé", do: nil)
|
||||
|
||||
assert [] =
|
||||
Intellisense.get_completion_items("Livebook.IntellisenseTest.Unicod", binding, env)
|
||||
after
|
||||
:code.purge(:"Elixir.Livebook.IntellisenseTest.Unicodé")
|
||||
:code.delete(:"Elixir.Livebook.IntellisenseTest.Unicodé")
|
||||
end
|
||||
|
||||
test "known Elixir module attributes completion" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "moduledoc",
|
||||
kind: :variable,
|
||||
detail: "module attribute",
|
||||
documentation: "Provides documentation for the current module.",
|
||||
insert_text: "moduledoc"
|
||||
}
|
||||
] = Intellisense.get_completion_items("@modu", binding, env)
|
||||
end
|
||||
|
||||
test "handles calls on module attribute" do
|
||||
{binding, env} = eval(do: nil)
|
||||
assert [] = Intellisense.get_completion_items("@attr.value", binding, env)
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_details/3" do
|
||||
test "returns nil if there are no matches" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert nil == Intellisense.get_details("Unknown.unknown()", 2, binding, env)
|
||||
end
|
||||
|
||||
test "returns subject range" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{range: %{from: 0, to: 17}} =
|
||||
Intellisense.get_details("Integer.to_string(10)", 15, binding, env)
|
||||
|
||||
assert %{range: %{from: 0, to: 7}} =
|
||||
Intellisense.get_details("Integer.to_string(10)", 2, binding, env)
|
||||
end
|
||||
|
||||
test "does not return duplicate details for functions with default arguments" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{contents: [_]} =
|
||||
Intellisense.get_details("Integer.to_string(10)", 15, binding, env)
|
||||
end
|
||||
|
||||
test "returns details only for exactly matching identifiers" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert nil == Intellisense.get_details("Enum.ma", 6, binding, env)
|
||||
end
|
||||
|
||||
test "returns full docs" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{contents: [content]} = Intellisense.get_details("Enum.map", 6, binding, env)
|
||||
assert content =~ "## Examples"
|
||||
end
|
||||
|
||||
test "returns full Erlang docs" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert %{contents: [file]} = Intellisense.get_details(":file.read()", 2, binding, env)
|
||||
assert file =~ "## Performance"
|
||||
|
||||
assert %{contents: [file_read]} = Intellisense.get_details(":file.read()", 8, binding, env)
|
||||
assert file_read =~ "Typical error reasons:"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -130,20 +130,40 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "request_completion_items/6" do
|
||||
describe "handle_intellisense/5 given completion request" do
|
||||
test "provides basic completion when no evaluation reference is given", %{pid: pid} do
|
||||
RuntimeServer.request_completion_items(pid, self(), :comp_ref, "System.ver", {:c1, nil})
|
||||
request = {:completion, "System.ver"}
|
||||
RuntimeServer.handle_intellisense(pid, self(), :ref, request, {:c1, nil})
|
||||
|
||||
assert_receive {:completion_response, :comp_ref, [%{label: "version/0"}]}
|
||||
assert_receive {:intellisense_response, :ref, %{items: [%{label: "version/0"}]}}
|
||||
end
|
||||
|
||||
test "provides extended completion when previous evaluation reference is given", %{pid: pid} do
|
||||
RuntimeServer.evaluate_code(pid, "number = 10", {:c1, :e1}, {:c1, nil})
|
||||
assert_receive {:evaluation_response, :e1, _, %{evaluation_time_ms: _time_ms}}
|
||||
|
||||
RuntimeServer.request_completion_items(pid, self(), :comp_ref, "num", {:c1, :e1})
|
||||
request = {:completion, "num"}
|
||||
RuntimeServer.handle_intellisense(pid, self(), :ref, request, {:c1, :e1})
|
||||
|
||||
assert_receive {:completion_response, :comp_ref, [%{label: "number"}]}
|
||||
assert_receive {:intellisense_response, :ref, %{items: [%{label: "number"}]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle_intellisense/5 given details request" do
|
||||
test "responds with identifier details", %{pid: pid} do
|
||||
request = {:details, "System.version", 10}
|
||||
RuntimeServer.handle_intellisense(pid, self(), :ref, request, {:c1, nil})
|
||||
|
||||
assert_receive {:intellisense_response, :ref, %{range: %{from: 0, to: 14}, contents: [_]}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle_intellisense/5 given format request" do
|
||||
test "responds with a formatted code", %{pid: pid} do
|
||||
request = {:format, "System.version"}
|
||||
RuntimeServer.handle_intellisense(pid, self(), :ref, request, {:c1, nil})
|
||||
|
||||
assert_receive {:intellisense_response, :ref, %{code: "System.version()"}}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -240,9 +240,13 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
view
|
||||
|> element("#session")
|
||||
|> render_hook("completion_request", %{"cell_id" => cell_id, "hint" => "System.ver"})
|
||||
|> render_hook("intellisense_request", %{
|
||||
"cell_id" => cell_id,
|
||||
"type" => "completion",
|
||||
"hint" => "System.ver"
|
||||
})
|
||||
|
||||
assert_reply view, %{"completion_ref" => nil}
|
||||
assert_reply view, %{"ref" => nil}
|
||||
end
|
||||
|
||||
test "replies with completion reference and then sends asynchronous response",
|
||||
|
|
@ -257,14 +261,18 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
view
|
||||
|> element("#session")
|
||||
|> render_hook("completion_request", %{"cell_id" => cell_id, "hint" => "System.ver"})
|
||||
|> render_hook("intellisense_request", %{
|
||||
"cell_id" => cell_id,
|
||||
"type" => "completion",
|
||||
"hint" => "System.ver"
|
||||
})
|
||||
|
||||
assert_reply view, %{"completion_ref" => ref}
|
||||
assert_reply view, %{"ref" => ref}
|
||||
assert ref != nil
|
||||
|
||||
assert_push_event(view, "completion_response", %{
|
||||
"completion_ref" => ^ref,
|
||||
"items" => [%{label: "version/0"}]
|
||||
assert_push_event(view, "intellisense_response", %{
|
||||
"ref" => ^ref,
|
||||
"response" => %{items: [%{label: "version/0"}]}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ defmodule Livebook.Runtime.NoopRuntime do
|
|||
def evaluate_code(_, _, _, _, _ \\ []), do: :ok
|
||||
def forget_evaluation(_, _), do: :ok
|
||||
def drop_container(_, _), do: :ok
|
||||
def request_completion_items(_, _, _, _, _), do: :ok
|
||||
def handle_intellisense(_, _, _, _, _), do: :ok
|
||||
def duplicate(_), do: {:ok, Livebook.Runtime.NoopRuntime.new()}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue