mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-13 16:34:45 +08:00
Improve completion (#747)
* Add keywords to completion * Fix signature request caching for call without parentheses * Don't insert parentheses for def* macros * Don't trigger missing runtime message when auto completion is enabled * Don't insert parentheses for keyword macros * Improve completion of env macros * Apply review comments * Update locals without parentheses * Apply suggestions from code review Co-authored-by: José Valim <jose.valim@dashbit.co> * Format Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
parent
4b5ea87b3d
commit
d909272746
7 changed files with 201 additions and 31 deletions
|
@ -250,6 +250,7 @@ class LiveEditor {
|
||||||
|
|
||||||
return this.__asyncIntellisenseRequest("completion", {
|
return this.__asyncIntellisenseRequest("completion", {
|
||||||
hint: lineUntilCursor,
|
hint: lineUntilCursor,
|
||||||
|
editor_auto_completion: settings.editor_auto_completion,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const suggestions = completionItemsToSuggestions(
|
const suggestions = completionItemsToSuggestions(
|
||||||
|
@ -311,8 +312,11 @@ class LiveEditor {
|
||||||
const lineUntilCursor = lines[lineIdx].slice(0, position.column - 1);
|
const lineUntilCursor = lines[lineIdx].slice(0, position.column - 1);
|
||||||
const codeUntilCursor = [...prevLines, lineUntilCursor].join("\n");
|
const codeUntilCursor = [...prevLines, lineUntilCursor].join("\n");
|
||||||
|
|
||||||
// Remove trailing characters that don't affect the signature
|
const codeUntilLastStop = codeUntilCursor
|
||||||
const codeUntilLastStop = codeUntilCursor.replace(/[^(),]*?$/, "");
|
// Remove trailing characters that don't affect the signature
|
||||||
|
.replace(/[^(),\s]*?$/, "")
|
||||||
|
// Remove whitespace before delimiter
|
||||||
|
.replace(/([(),])\s*$/, "$1");
|
||||||
|
|
||||||
// Cache subsequent requests for the same prefix, so that we don't
|
// Cache subsequent requests for the same prefix, so that we don't
|
||||||
// make unnecessary requests
|
// make unnecessary requests
|
||||||
|
@ -464,6 +468,8 @@ function parseItemKind(kind) {
|
||||||
return monaco.languages.CompletionItemKind.Variable;
|
return monaco.languages.CompletionItemKind.Variable;
|
||||||
case "field":
|
case "field":
|
||||||
return monaco.languages.CompletionItemKind.Field;
|
return monaco.languages.CompletionItemKind.Field;
|
||||||
|
case "keyword":
|
||||||
|
return monaco.languages.CompletionItemKind.Keyword;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,13 +114,14 @@ defmodule Livebook.Intellisense do
|
||||||
IdentifierMatcher.completion_identifiers(hint, binding, env)
|
IdentifierMatcher.completion_identifiers(hint, binding, env)
|
||||||
|> Enum.filter(&include_in_completion?/1)
|
|> Enum.filter(&include_in_completion?/1)
|
||||||
|> Enum.map(&format_completion_item/1)
|
|> Enum.map(&format_completion_item/1)
|
||||||
|
|> Enum.concat(extra_completion_items(hint))
|
||||||
|> Enum.sort_by(&completion_item_priority/1)
|
|> Enum.sort_by(&completion_item_priority/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp include_in_completion?({:module, _module, _display_name, :hidden}), do: false
|
defp include_in_completion?({:module, _module, _display_name, :hidden}), do: false
|
||||||
|
|
||||||
defp include_in_completion?(
|
defp include_in_completion?(
|
||||||
{:function, _module, _name, _arity, _display_name, :hidden, _signatures, _specs}
|
{:function, _module, _name, _arity, _type, _display_name, :hidden, _signatures, _specs}
|
||||||
),
|
),
|
||||||
do: false
|
do: false
|
||||||
|
|
||||||
|
@ -168,7 +169,7 @@ defmodule Livebook.Intellisense do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp format_completion_item(
|
defp format_completion_item(
|
||||||
{:function, module, name, arity, display_name, documentation, signatures, specs}
|
{:function, module, name, arity, type, display_name, documentation, signatures, specs}
|
||||||
),
|
),
|
||||||
do: %{
|
do: %{
|
||||||
label: "#{display_name}/#{arity}",
|
label: "#{display_name}/#{arity}",
|
||||||
|
@ -181,11 +182,24 @@ defmodule Livebook.Intellisense do
|
||||||
]),
|
]),
|
||||||
insert_text:
|
insert_text:
|
||||||
cond do
|
cond do
|
||||||
String.starts_with?(display_name, "~") -> display_name
|
type == :macro and keyword_macro?(name) ->
|
||||||
Macro.operator?(name, arity) -> display_name
|
"#{display_name} "
|
||||||
# A snippet with cursor in parentheses
|
|
||||||
arity == 0 -> "#{display_name}()"
|
type == :macro and env_macro?(name) ->
|
||||||
true -> "#{display_name}($0)"
|
display_name
|
||||||
|
|
||||||
|
String.starts_with?(display_name, "~") ->
|
||||||
|
display_name
|
||||||
|
|
||||||
|
Macro.operator?(name, arity) ->
|
||||||
|
display_name
|
||||||
|
|
||||||
|
arity == 0 ->
|
||||||
|
"#{display_name}()"
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# A snippet with cursor in parentheses
|
||||||
|
"#{display_name}($0)"
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +221,87 @@ defmodule Livebook.Intellisense do
|
||||||
insert_text: name
|
insert_text: name
|
||||||
}
|
}
|
||||||
|
|
||||||
@ordered_kinds [:field, :variable, :module, :struct, :interface, :function, :type]
|
defp keyword_macro?(name) do
|
||||||
|
def? = name |> Atom.to_string() |> String.starts_with?("def")
|
||||||
|
|
||||||
|
def? or
|
||||||
|
name in [
|
||||||
|
# Special forms
|
||||||
|
:alias,
|
||||||
|
:case,
|
||||||
|
:cond,
|
||||||
|
:for,
|
||||||
|
:fn,
|
||||||
|
:import,
|
||||||
|
:quote,
|
||||||
|
:receive,
|
||||||
|
:require,
|
||||||
|
:try,
|
||||||
|
:with,
|
||||||
|
|
||||||
|
# Kernel
|
||||||
|
:destructure,
|
||||||
|
:raise,
|
||||||
|
:reraise,
|
||||||
|
:if,
|
||||||
|
:unless,
|
||||||
|
:use
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp env_macro?(name) do
|
||||||
|
name in [:__ENV__, :__MODULE__, :__DIR__, :__STACKTRACE__, :__CALLER__]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extra_completion_items(hint) do
|
||||||
|
items = [
|
||||||
|
%{
|
||||||
|
label: "do",
|
||||||
|
kind: :keyword,
|
||||||
|
detail: "do-end block",
|
||||||
|
documentation: nil,
|
||||||
|
insert_text: "do\n $0\nend"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
label: "true",
|
||||||
|
kind: :keyword,
|
||||||
|
detail: "boolean",
|
||||||
|
documentation: nil,
|
||||||
|
insert_text: "true"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
label: "false",
|
||||||
|
kind: :keyword,
|
||||||
|
detail: "boolean",
|
||||||
|
documentation: nil,
|
||||||
|
insert_text: "false"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
label: "nil",
|
||||||
|
kind: :keyword,
|
||||||
|
detail: "special atom",
|
||||||
|
documentation: nil,
|
||||||
|
insert_text: "nil"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
label: "when",
|
||||||
|
kind: :keyword,
|
||||||
|
detail: "guard operator",
|
||||||
|
documentation: nil,
|
||||||
|
insert_text: "when"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
last_word = hint |> String.split(~r/\s/) |> List.last()
|
||||||
|
|
||||||
|
if last_word == "" do
|
||||||
|
[]
|
||||||
|
else
|
||||||
|
Enum.filter(items, &String.starts_with?(&1.label, last_word))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@ordered_kinds [:keyword, :field, :variable, :module, :struct, :interface, :function, :type]
|
||||||
|
|
||||||
defp completion_item_priority(%{kind: :struct, detail: "exception"} = completion_item) do
|
defp completion_item_priority(%{kind: :struct, detail: "exception"} = completion_item) do
|
||||||
{length(@ordered_kinds), completion_item.label}
|
{length(@ordered_kinds), completion_item.label}
|
||||||
|
@ -263,7 +357,7 @@ defmodule Livebook.Intellisense do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp format_details_item(
|
defp format_details_item(
|
||||||
{:function, module, name, _arity, _display_name, documentation, signatures, specs}
|
{:function, module, name, _arity, _type, _display_name, documentation, signatures, specs}
|
||||||
) do
|
) do
|
||||||
join_with_divider([
|
join_with_divider([
|
||||||
format_signatures(signatures, module) |> code(),
|
format_signatures(signatures, module) |> code(),
|
||||||
|
|
|
@ -22,14 +22,15 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
{:variable, name(), value()}
|
{:variable, name(), value()}
|
||||||
| {:map_field, name(), value()}
|
| {:map_field, name(), value()}
|
||||||
| {:module, module(), display_name(), Docs.documentation()}
|
| {:module, module(), display_name(), Docs.documentation()}
|
||||||
| {:function, module(), name(), arity(), display_name(), Docs.documentation(),
|
| {:function, module(), name(), arity(), function_type(), display_name(),
|
||||||
list(Docs.signature()), list(Docs.spec())}
|
Docs.documentation(), list(Docs.signature()), list(Docs.spec())}
|
||||||
| {:type, module(), name(), arity(), Docs.documentation()}
|
| {:type, module(), name(), arity(), Docs.documentation()}
|
||||||
| {:module_attribute, name(), Docs.documentation()}
|
| {:module_attribute, name(), Docs.documentation()}
|
||||||
|
|
||||||
@type name :: atom()
|
@type name :: atom()
|
||||||
@type display_name :: String.t()
|
@type display_name :: String.t()
|
||||||
@type value :: term()
|
@type value :: term()
|
||||||
|
@type function_type :: :function | :macro
|
||||||
|
|
||||||
@exact_matcher &Kernel.==/2
|
@exact_matcher &Kernel.==/2
|
||||||
@prefix_matcher &String.starts_with?/2
|
@prefix_matcher &String.starts_with?/2
|
||||||
|
@ -241,10 +242,13 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp match_sigil(hint, ctx) do
|
defp match_sigil(hint, ctx) do
|
||||||
for {:function, module, name, arity, "sigil_" <> sigil_name, documentation, signatures, specs} <-
|
for {:function, module, name, arity, type, "sigil_" <> sigil_name, documentation, signatures,
|
||||||
|
specs} <-
|
||||||
match_local("sigil_", %{ctx | matcher: @prefix_matcher}),
|
match_local("sigil_", %{ctx | matcher: @prefix_matcher}),
|
||||||
ctx.matcher.(sigil_name, hint),
|
ctx.matcher.(sigil_name, hint),
|
||||||
do: {:function, module, name, arity, "~" <> sigil_name, documentation, signatures, specs}
|
do:
|
||||||
|
{:function, module, name, arity, type, "~" <> sigil_name, documentation, signatures,
|
||||||
|
specs}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp match_erlang_module(hint, ctx) do
|
defp match_erlang_module(hint, ctx) do
|
||||||
|
@ -376,24 +380,26 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
funs = funs || exports(mod)
|
funs = funs || exports(mod)
|
||||||
|
|
||||||
matching_funs =
|
matching_funs =
|
||||||
Enum.filter(funs, fn {name, _arity} ->
|
Enum.filter(funs, fn {name, _arity, _type} ->
|
||||||
name = Atom.to_string(name)
|
name = Atom.to_string(name)
|
||||||
ctx.matcher.(name, hint)
|
ctx.matcher.(name, hint)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
doc_items =
|
doc_items =
|
||||||
Livebook.Intellisense.Docs.lookup_module_members(mod, matching_funs,
|
Livebook.Intellisense.Docs.lookup_module_members(
|
||||||
|
mod,
|
||||||
|
Enum.map(matching_funs, &Tuple.delete_at(&1, 2)),
|
||||||
kinds: [:function, :macro]
|
kinds: [:function, :macro]
|
||||||
)
|
)
|
||||||
|
|
||||||
Enum.map(matching_funs, fn {name, arity} ->
|
Enum.map(matching_funs, fn {name, arity, type} ->
|
||||||
doc_item =
|
doc_item =
|
||||||
Enum.find(doc_items, %{documentation: nil, signatures: [], specs: []}, fn doc_item ->
|
Enum.find(doc_items, %{documentation: nil, signatures: [], specs: []}, fn doc_item ->
|
||||||
doc_item.name == name && doc_item.arity == arity
|
doc_item.name == name && doc_item.arity == arity
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:function, mod, name, arity, Atom.to_string(name), doc_item && doc_item.documentation,
|
{:function, mod, name, arity, type, Atom.to_string(name),
|
||||||
doc_item.signatures, doc_item.specs}
|
doc_item && doc_item.documentation, doc_item.signatures, doc_item.specs}
|
||||||
end)
|
end)
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
|
@ -402,12 +408,19 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
|
|
||||||
defp exports(mod) do
|
defp exports(mod) do
|
||||||
if Code.ensure_loaded?(mod) and function_exported?(mod, :__info__, 1) do
|
if Code.ensure_loaded?(mod) and function_exported?(mod, :__info__, 1) do
|
||||||
mod.__info__(:macros) ++ (mod.__info__(:functions) -- [__info__: 1])
|
macros = mod.__info__(:macros)
|
||||||
|
functions = mod.__info__(:functions) -- [__info__: 1]
|
||||||
|
append_funs_type(macros, :macro) ++ append_funs_type(functions, :function)
|
||||||
else
|
else
|
||||||
mod.module_info(:exports) -- [module_info: 0, module_info: 1]
|
functions = mod.module_info(:exports) -- [module_info: 0, module_info: 1]
|
||||||
|
append_funs_type(functions, :function)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp append_funs_type(funs, type) do
|
||||||
|
Enum.map(funs, &Tuple.append(&1, type))
|
||||||
|
end
|
||||||
|
|
||||||
defp match_module_type(mod, hint, ctx) do
|
defp match_module_type(mod, hint, ctx) do
|
||||||
types = get_module_types(mod)
|
types = get_module_types(mod)
|
||||||
|
|
||||||
|
@ -444,7 +457,14 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
defp ensure_loaded?(Elixir), do: false
|
defp ensure_loaded?(Elixir), do: false
|
||||||
defp ensure_loaded?(mod), do: Code.ensure_loaded?(mod)
|
defp ensure_loaded?(mod), do: Code.ensure_loaded?(mod)
|
||||||
|
|
||||||
defp imports_from_env(env), do: env.functions ++ env.macros
|
defp imports_from_env(env) do
|
||||||
|
Enum.map(env.functions, fn {mod, funs} ->
|
||||||
|
{mod, append_funs_type(funs, :function)}
|
||||||
|
end) ++
|
||||||
|
Enum.map(env.macros, fn {mod, funs} ->
|
||||||
|
{mod, append_funs_type(funs, :macro)}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp split_at_last_occurrence(string, pattern) do
|
defp split_at_last_occurrence(string, pattern) do
|
||||||
case :binary.matches(string, pattern) do
|
case :binary.matches(string, pattern) do
|
||||||
|
|
|
@ -72,7 +72,7 @@ defprotocol Livebook.Runtime do
|
||||||
}
|
}
|
||||||
|
|
||||||
@type completion_item_kind ::
|
@type completion_item_kind ::
|
||||||
:function | :module | :struct | :interface | :type | :variable | :field
|
:function | :module | :struct | :interface | :type | :variable | :field | :keyword
|
||||||
|
|
||||||
@typedoc """
|
@typedoc """
|
||||||
Looks up more details about an identifier found in `column` in `line`.
|
Looks up more details about an identifier found in `column` in `line`.
|
||||||
|
|
|
@ -784,14 +784,14 @@ defmodule LivebookWeb.SessionLive do
|
||||||
{:reply, %{"ref" => inspect(ref)}, socket}
|
{:reply, %{"ref" => inspect(ref)}, socket}
|
||||||
else
|
else
|
||||||
info =
|
info =
|
||||||
case params["type"] do
|
cond do
|
||||||
"completion" ->
|
params["type"] == "completion" and not params["editor_auto_completion"] ->
|
||||||
"You need to start a runtime (or evaluate a cell) for code completion"
|
"You need to start a runtime (or evaluate a cell) for code completion"
|
||||||
|
|
||||||
"format" ->
|
params["type"] == "format" ->
|
||||||
"You need to start a runtime (or evaluate a cell) to enable code formatting"
|
"You need to start a runtime (or evaluate a cell) to enable code formatting"
|
||||||
|
|
||||||
_ ->
|
true ->
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -738,7 +738,7 @@ defmodule Livebook.IntellisenseTest do
|
||||||
kind: :function,
|
kind: :function,
|
||||||
detail: "Kernel.SpecialForms.quote(opts, block)",
|
detail: "Kernel.SpecialForms.quote(opts, block)",
|
||||||
documentation: "Gets the representation of any expression.",
|
documentation: "Gets the representation of any expression.",
|
||||||
insert_text: "quote($0)"
|
insert_text: "quote "
|
||||||
}
|
}
|
||||||
] = Intellisense.get_completion_items("quot", binding, env)
|
] = Intellisense.get_completion_items("quot", binding, env)
|
||||||
end
|
end
|
||||||
|
@ -1110,6 +1110,54 @@ defmodule Livebook.IntellisenseTest do
|
||||||
{binding, env} = eval(do: nil)
|
{binding, env} = eval(do: nil)
|
||||||
assert [] = Intellisense.get_completion_items("@attr.value", binding, env)
|
assert [] = Intellisense.get_completion_items("@attr.value", binding, env)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "includes language keywords" do
|
||||||
|
{binding, env} = eval(do: nil)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
label: "do",
|
||||||
|
kind: :keyword,
|
||||||
|
detail: "do-end block",
|
||||||
|
documentation: nil,
|
||||||
|
insert_text: "do\n $0\nend"
|
||||||
|
}
|
||||||
|
| _
|
||||||
|
] = Intellisense.get_completion_items("do", binding, env)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes space instead of parentheses for def* macros" do
|
||||||
|
{binding, env} = eval(do: nil)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
label: "defmodule/2",
|
||||||
|
insert_text: "defmodule "
|
||||||
|
}
|
||||||
|
] = Intellisense.get_completion_items("defmodu", binding, env)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes space instead of parentheses for keyword macros" do
|
||||||
|
{binding, env} = eval(do: nil)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
label: "import/2",
|
||||||
|
insert_text: "import "
|
||||||
|
}
|
||||||
|
] = Intellisense.get_completion_items("impor", binding, env)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes doesn't include space nor parentheses for macros like __ENV__" do
|
||||||
|
{binding, env} = eval(do: nil)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
label: "__ENV__/0",
|
||||||
|
insert_text: "__ENV__"
|
||||||
|
}
|
||||||
|
] = Intellisense.get_completion_items("__EN", binding, env)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "get_details/3" do
|
describe "get_details/3" do
|
||||||
|
|
|
@ -348,7 +348,8 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
|> render_hook("intellisense_request", %{
|
|> render_hook("intellisense_request", %{
|
||||||
"cell_id" => cell_id,
|
"cell_id" => cell_id,
|
||||||
"type" => "completion",
|
"type" => "completion",
|
||||||
"hint" => "System.ver"
|
"hint" => "System.ver",
|
||||||
|
"editor_auto_completion" => false
|
||||||
})
|
})
|
||||||
|
|
||||||
assert_reply view, %{"ref" => nil}
|
assert_reply view, %{"ref" => nil}
|
||||||
|
@ -369,7 +370,8 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
|> render_hook("intellisense_request", %{
|
|> render_hook("intellisense_request", %{
|
||||||
"cell_id" => cell_id,
|
"cell_id" => cell_id,
|
||||||
"type" => "completion",
|
"type" => "completion",
|
||||||
"hint" => "System.ver"
|
"hint" => "System.ver",
|
||||||
|
"editor_auto_completion" => false
|
||||||
})
|
})
|
||||||
|
|
||||||
assert_reply view, %{"ref" => ref}
|
assert_reply view, %{"ref" => ref}
|
||||||
|
|
Loading…
Add table
Reference in a new issue