List module definitions in the sections side panel (#2760)

This commit is contained in:
Alexandre de Souza 2024-08-23 18:28:33 -03:00 committed by GitHub
parent eca2dc1143
commit ad0db1832b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 215 additions and 46 deletions

View file

@ -373,7 +373,7 @@ const Session = {
} }
} else if (keyBuffer.tryMatch(["e", "s"])) { } else if (keyBuffer.tryMatch(["e", "s"])) {
this.queueFocusedSectionEvaluation(); this.queueFocusedSectionEvaluation();
} else if (keyBuffer.tryMatch(["s", "s"])) { } else if (keyBuffer.tryMatch(["s", "o"])) {
this.toggleSectionsList(); this.toggleSectionsList();
} else if (keyBuffer.tryMatch(["s", "e"])) { } else if (keyBuffer.tryMatch(["s", "e"])) {
this.toggleSecretsList(); this.toggleSecretsList();
@ -579,11 +579,23 @@ const Session = {
*/ */
handleSectionsListClick(event) { handleSectionsListClick(event) {
const sectionButton = event.target.closest(`[data-el-sections-list-item]`); const sectionButton = event.target.closest(`[data-el-sections-list-item]`);
if (sectionButton) { if (sectionButton) {
const sectionId = sectionButton.getAttribute("data-section-id"); const sectionId = sectionButton.getAttribute("data-section-id");
const section = this.getSectionById(sectionId); const section = this.getSectionById(sectionId);
section.scrollIntoView({ behavior: "instant", block: "start" }); section.scrollIntoView({ behavior: "instant", block: "start" });
} }
const sectionDefinitionButton = event.target.closest(
`[data-el-sections-list-definition-item]`,
);
if (sectionDefinitionButton) {
const file = sectionDefinitionButton.getAttribute("data-file");
const line = sectionDefinitionButton.getAttribute("data-line");
this.jumpToLine(file, line);
}
}, },
/** /**

View file

@ -550,7 +550,7 @@ defmodule Livebook.Intellisense do
path = Path.join(context.ebin_path, "#{module}.beam") path = Path.join(context.ebin_path, "#{module}.beam")
with true <- File.exists?(path), with true <- File.exists?(path),
{:ok, line} <- Docs.locate_definition(path, identifier) do {:ok, line} <- Docs.locate_definition(String.to_charlist(path), identifier) do
file = module.module_info(:compile)[:source] file = module.module_info(:compile)[:source]
%{file: to_string(file), line: line} %{file: to_string(file), line: line}
else else

View file

@ -185,7 +185,7 @@ defmodule Livebook.Intellisense.Docs do
The function returns the line where the identifier is located. The function returns the line where the identifier is located.
""" """
@spec locate_definition(String.t(), definition()) :: {:ok, pos_integer()} | :error @spec locate_definition(list() | binary(), definition()) :: {:ok, pos_integer()} | :error
def locate_definition(path, identifier) def locate_definition(path, identifier)
def locate_definition(path, {:module, module}) do def locate_definition(path, {:module, module}) do
@ -221,8 +221,6 @@ defmodule Livebook.Intellisense.Docs do
end end
defp beam_lib_chunks(path, key) do defp beam_lib_chunks(path, key) do
path = String.to_charlist(path)
case :beam_lib.chunks(path, [key]) do case :beam_lib.chunks(path, [key]) do
{:ok, {_, [{^key, value}]}} -> {:ok, value} {:ok, {_, [{^key, value}]}} -> {:ok, value}
_ -> :error _ -> :error

View file

@ -470,12 +470,15 @@ defprotocol Livebook.Runtime do
dependencies between evaluations and avoids unnecessary reevaluations. dependencies between evaluations and avoids unnecessary reevaluations.
""" """
@type evaluation_response_metadata :: %{ @type evaluation_response_metadata :: %{
interrupted: boolean(),
errored: boolean(), errored: boolean(),
evaluation_time_ms: non_neg_integer(), evaluation_time_ms: non_neg_integer(),
code_markers: list(code_marker()), code_markers: list(code_marker()),
memory_usage: runtime_memory(), memory_usage: runtime_memory(),
identifiers_used: list(identifier :: term()) | :unknown, identifiers_used: list(identifier :: term()) | :unknown,
identifiers_defined: %{(identifier :: term()) => version :: term()} identifiers_defined: %{(identifier :: term()) => version :: term()},
identifier_definitions:
list(%{label: String.t(), file: String.t(), line: pos_integer()})
} }
@typedoc """ @typedoc """

View file

@ -439,7 +439,7 @@ defmodule Livebook.Runtime.Evaluator do
%{tracer_info: tracer_info} = Evaluator.IOProxy.after_evaluation(state.io_proxy) %{tracer_info: tracer_info} = Evaluator.IOProxy.after_evaluation(state.io_proxy)
{new_context, result, identifiers_used, identifiers_defined} = {new_context, result, identifiers_used, identifiers_defined, identifier_definitions} =
case eval_result do case eval_result do
{:ok, value, binding, env} -> {:ok, value, binding, env} ->
context_id = random_long_id() context_id = random_long_id()
@ -454,8 +454,10 @@ defmodule Livebook.Runtime.Evaluator do
{identifiers_used, identifiers_defined} = {identifiers_used, identifiers_defined} =
identifier_dependencies(new_context, tracer_info, context) identifier_dependencies(new_context, tracer_info, context)
identifier_definitions = definitions(new_context, tracer_info)
result = {:ok, value} result = {:ok, value}
{new_context, result, identifiers_used, identifiers_defined} {new_context, result, identifiers_used, identifiers_defined, identifier_definitions}
{:error, kind, error, stacktrace} -> {:error, kind, error, stacktrace} ->
for {module, _} <- tracer_info.modules_defined do for {module, _} <- tracer_info.modules_defined do
@ -465,9 +467,10 @@ defmodule Livebook.Runtime.Evaluator do
result = {:error, kind, error, stacktrace} result = {:error, kind, error, stacktrace}
identifiers_used = :unknown identifiers_used = :unknown
identifiers_defined = %{} identifiers_defined = %{}
identifier_definitions = []
# Empty context # Empty context
new_context = initial_context() new_context = initial_context()
{new_context, result, identifiers_used, identifiers_defined} {new_context, result, identifiers_used, identifiers_defined, identifier_definitions}
end end
if ebin_path() do if ebin_path() do
@ -475,7 +478,6 @@ defmodule Livebook.Runtime.Evaluator do
end end
state = put_context(state, ref, new_context) state = put_context(state, ref, new_context)
output = Evaluator.Formatter.format_result(result, language) output = Evaluator.Formatter.format_result(result, language)
metadata = %{ metadata = %{
@ -485,7 +487,8 @@ defmodule Livebook.Runtime.Evaluator do
memory_usage: memory(), memory_usage: memory(),
code_markers: code_markers, code_markers: code_markers,
identifiers_used: identifiers_used, identifiers_used: identifiers_used,
identifiers_defined: identifiers_defined identifiers_defined: identifiers_defined,
identifier_definitions: identifier_definitions
} }
send(state.send_to, {:runtime_evaluation_response, ref, output, metadata}) send(state.send_to, {:runtime_evaluation_response, ref, output, metadata})
@ -890,7 +893,7 @@ defmodule Livebook.Runtime.Evaluator do
into: identifiers_used into: identifiers_used
identifiers_defined = identifiers_defined =
for {module, _vars} <- tracer_info.modules_defined, for {module, _line_vars} <- tracer_info.modules_defined,
version = module.__info__(:md5), version = module.__info__(:md5),
do: {{:module, module}, version}, do: {{:module, module}, version},
into: identifiers_defined into: identifiers_defined
@ -968,7 +971,7 @@ defmodule Livebook.Runtime.Evaluator do
# Note that :prune_binding removes variables used by modules # Note that :prune_binding removes variables used by modules
# (unless used outside), so we get those from the tracer # (unless used outside), so we get those from the tracer
module_used_vars = module_used_vars =
for {_module, vars} <- tracer_info.modules_defined, for {_module, {_line, vars}} <- tracer_info.modules_defined,
var <- vars, var <- vars,
into: MapSet.new(), into: MapSet.new(),
do: var do: var
@ -1038,4 +1041,16 @@ defmodule Livebook.Runtime.Evaluator do
defp ebin_path() do defp ebin_path() do
Process.get(@ebin_path_key) Process.get(@ebin_path_key)
end end
defp definitions(context, tracer_info) do
for {module, {line, _vars}} <- tracer_info.modules_defined,
do: %{label: module_name(module), file: context.env.file, line: line}
end
defp module_name(module) do
case Atom.to_string(module) do
"Elixir." <> name -> name
name -> name
end
end
end end

View file

@ -146,7 +146,7 @@ defmodule Livebook.Runtime.Evaluator.IOProxy do
state = update_in(state.tracer_info, &Evaluator.Tracer.apply_updates(&1, updates)) state = update_in(state.tracer_info, &Evaluator.Tracer.apply_updates(&1, updates))
modules_defined = modules_defined =
for {:module_defined, module, _vars} <- updates, for {:module_defined, module, _vars, _line} <- updates,
into: state.modules_defined, into: state.modules_defined,
do: module do: module

View file

@ -93,7 +93,7 @@ defmodule Livebook.Runtime.Evaluator.Tracer do
module = env.module module = env.module
vars = Map.keys(env.versioned_vars) vars = Map.keys(env.versioned_vars)
Evaluator.write_module!(module, bytecode) Evaluator.write_module!(module, bytecode)
[{:module_defined, module, vars}, {:alias_used, module}] [{:module_defined, module, vars, env.line}, {:alias_used, module}]
_ -> _ ->
[] []
@ -112,8 +112,8 @@ defmodule Livebook.Runtime.Evaluator.Tracer do
update_in(info.modules_used, &MapSet.put(&1, module)) update_in(info.modules_used, &MapSet.put(&1, module))
end end
defp apply_update(info, {:module_defined, module, vars}) do defp apply_update(info, {:module_defined, module, vars, line}) do
put_in(info.modules_defined[module], vars) put_in(info.modules_defined[module], {line, vars})
end end
defp apply_update(info, {:alias_used, alias}) do defp apply_update(info, {:alias_used, alias}) do

View file

@ -118,6 +118,8 @@ defmodule Livebook.Session.Data do
new_bound_to_inputs: %{input_id() => input_hash()}, new_bound_to_inputs: %{input_id() => input_hash()},
identifiers_used: list(identifier :: term()) | :unknown, identifiers_used: list(identifier :: term()) | :unknown,
identifiers_defined: %{(identifier :: term()) => version :: term()}, identifiers_defined: %{(identifier :: term()) => version :: term()},
identifier_definitions:
list(%{label: String.t(), file: String.t(), line: pos_integer()}),
data: t() data: t()
} }
@ -1401,7 +1403,8 @@ defmodule Livebook.Session.Data do
identifiers_defined: metadata.identifiers_defined, identifiers_defined: metadata.identifiers_defined,
bound_to_inputs: eval_info.new_bound_to_inputs, bound_to_inputs: eval_info.new_bound_to_inputs,
evaluation_end: DateTime.utc_now(), evaluation_end: DateTime.utc_now(),
code_markers: metadata.code_markers code_markers: metadata.code_markers,
identifier_definitions: metadata.identifier_definitions
} }
end) end)
|> update_cell_evaluation_snapshot(cell, section) |> update_cell_evaluation_snapshot(cell, section)
@ -2380,6 +2383,7 @@ defmodule Livebook.Session.Data do
data: nil, data: nil,
code_markers: [], code_markers: [],
doctest_reports: %{}, doctest_reports: %{},
identifier_definitions: [],
reevaluates_automatically: false reevaluates_automatically: false
} }
end end

View file

@ -1796,7 +1796,8 @@ defmodule LivebookWeb.SessionLive do
id: section.id, id: section.id,
name: section.name, name: section.name,
parent: parent_section_view(section.parent_id, data), parent: parent_section_view(section.parent_id, data),
status: cells_status(section.cells, data) status: cells_status(section.cells, data),
identifier_definitions: cells_identifier_definitions(section.cells, data)
} }
end, end,
clients: clients:
@ -1853,6 +1854,14 @@ defmodule LivebookWeb.SessionLive do
end end
end end
defp cells_identifier_definitions(cells, data) do
for %Cell.Code{} = cell <- cells,
Cell.evaluable?(cell),
info = data.cell_infos[cell.id].eval,
definition <- info.identifier_definitions,
do: definition
end
defp global_status(data) do defp global_status(data) do
cells = cells =
data.notebook data.notebook

View file

@ -331,8 +331,8 @@ defmodule LivebookWeb.SessionLive.Render do
<%!-- Local functionality --%> <%!-- Local functionality --%>
<.button_item <.button_item
icon="booklet-fill" icon="node-tree"
label="Sections (ss)" label="Outline (so)"
button_attrs={["data-el-sections-list-toggle": true]} button_attrs={["data-el-sections-list-toggle": true]}
/> />
@ -419,8 +419,8 @@ defmodule LivebookWeb.SessionLive.Render do
class="flex flex-col h-full w-full max-w-xs absolute z-30 top-0 left-[64px] overflow-y-auto shadow-xl md:static md:shadow-none bg-gray-50 border-r border-gray-100 px-6 pt-16 md:py-8" class="flex flex-col h-full w-full max-w-xs absolute z-30 top-0 left-[64px] overflow-y-auto shadow-xl md:static md:shadow-none bg-gray-50 border-r border-gray-100 px-6 pt-16 md:py-8"
data-el-side-panel data-el-side-panel
> >
<div class="flex grow" data-el-sections-list> <div data-el-sections-list>
<.sections_list data_view={@data_view} /> <.outline_list data_view={@data_view} />
</div> </div>
<div data-el-clients-list> <div data-el-clients-list>
<.clients_list data_view={@data_view} client_id={@client_id} /> <.clients_list data_view={@data_view} client_id={@client_id} />
@ -497,20 +497,21 @@ defmodule LivebookWeb.SessionLive.Render do
""" """
end end
defp sections_list(assigns) do defp outline_list(assigns) do
~H""" ~H"""
<div class="flex flex-col grow"> <div class="flex flex-col grow">
<h3 class="uppercase text-sm font-semibold text-gray-500"> <h3 class="uppercase text-sm font-semibold text-gray-500">
Sections Outline
</h3> </h3>
<div class="flex flex-col mt-4 space-y-4"> <div class="flex flex-col mt-4 space-y-4">
<div :for={section_item <- @data_view.sections_items} class="flex items-center"> <div :for={section_item <- @data_view.sections_items} class="flex flex-col">
<div class="flex justify-between items-center">
<button <button
class="grow flex items-center text-gray-500 hover:text-gray-900 text-left" class="grow flex items-center gap-1 text-gray-500 hover:text-gray-900 text-left"
data-el-sections-list-item data-el-sections-list-item
data-section-id={section_item.id} data-section-id={section_item.id}
> >
<span class="flex items-center space-x-1"> <.remix_icon icon="font-size" class="text-lg font-normal leading-none" />
<span><%= section_item.name %></span> <span><%= section_item.name %></span>
<%!-- <%!--
Note: the container has overflow-y auto, so we cannot set overflow-x visible, Note: the container has overflow-y auto, so we cannot set overflow-x visible,
@ -525,13 +526,31 @@ defmodule LivebookWeb.SessionLive.Render do
class="text-lg font-normal leading-none flip-horizontally" class="text-lg font-normal leading-none flip-horizontally"
/> />
</span> </span>
</span>
</button> </button>
<.section_status <.section_status
status={elem(section_item.status, 0)} status={elem(section_item.status, 0)}
cell_id={elem(section_item.status, 1)} cell_id={elem(section_item.status, 1)}
/> />
</div> </div>
<ul :if={section_item.identifier_definitions != []} class="mt-2 ml-5 list-none items-center">
<li :for={definition <- section_item.identifier_definitions}>
<button
class="flex items-center max-w-full text-gray-500 hover:text-gray-900 text-sm gap-1"
data-el-sections-list-definition-item
data-file={definition.file}
data-line={definition.line}
title={definition.label}
>
<.remix_icon icon="braces-line" class="font-normal" />
<span class="font-mono truncate">
<%= definition.label %>
</span>
</button>
</li>
</ul>
</div>
</div> </div>
<button <button
class="inline-flex items-center justify-center p-8 py-1 mt-8 space-x-2 text-sm font-medium text-gray-500 border border-gray-400 border-dashed rounded-xl hover:bg-gray-100" class="inline-flex items-center justify-center p-8 py-1 mt-8 space-x-2 text-sm font-medium text-gray-500 border border-gray-400 border-dashed rounded-xl hover:bg-gray-100"
@ -558,7 +577,7 @@ defmodule LivebookWeb.SessionLive.Render do
wrapped_name = Livebook.Utils.wrap_line("" <> parent_name <> "", 16) wrapped_name = Livebook.Utils.wrap_line("" <> parent_name <> "", 16)
label = "Branches from\n#{wrapped_name}" label = "Branches from\n#{wrapped_name}"
[class: "tooltip #{direction}", data_tooltip: label] [class: "tooltip #{direction}", "data-tooltip": label]
end end
defp clients_list(assigns) do defp clients_list(assigns) do

View file

@ -259,9 +259,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
}} }}
end end
test "returns additional metadata when there is a module compilation error", %{ test "returns additional metadata when there is a module compilation error",
evaluator: evaluator %{evaluator: evaluator} do
} do
code = """ code = """
defmodule Livebook.Runtime.EvaluatorTest.Invalid do defmodule Livebook.Runtime.EvaluatorTest.Invalid do
x x
@ -504,6 +503,50 @@ defmodule Livebook.Runtime.EvaluatorTest do
refute Code.ensure_loaded?(Livebook.Runtime.EvaluatorTest.Exited) refute Code.ensure_loaded?(Livebook.Runtime.EvaluatorTest.Exited)
end end
@tag :with_ebin_path
test "returns identifier definitions", %{evaluator: evaluator} do
Code.put_compiler_option(:debug_info, true)
code = ~S'''
defmodule Livebook.Runtime.EvaluatorTest.ModuleDef1 do
def fun(), do: :ok
end
defmodule Livebook.Runtime.EvaluatorTest.ModuleDef2 do
defmodule Foo do
defstruct [:name]
end
end
'''
file = "file.livemd#cell_id:123456789"
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: file)
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(_),
metadata() = metadata}
assert metadata.identifier_definitions == [
%{
label: "Livebook.Runtime.EvaluatorTest.ModuleDef1",
line: 1,
file: file
},
%{
label: "Livebook.Runtime.EvaluatorTest.ModuleDef2",
line: 5,
file: file
},
%{
label: "Livebook.Runtime.EvaluatorTest.ModuleDef2.Foo",
line: 6,
file: file
}
]
after
Code.put_compiler_option(:debug_info, false)
end
end end
describe "doctests" do describe "doctests" do

View file

@ -25,6 +25,7 @@ defmodule Livebook.Session.DataTest do
defp eval_meta(opts \\ []) do defp eval_meta(opts \\ []) do
uses = opts[:uses] || [] uses = opts[:uses] || []
defines = opts[:defines] || %{} defines = opts[:defines] || %{}
identifier_definitions = opts[:identifier_definitions] || []
errored = Keyword.get(opts, :errored, false) errored = Keyword.get(opts, :errored, false)
interrupted = Keyword.get(opts, :interrupted, false) interrupted = Keyword.get(opts, :interrupted, false)
@ -34,6 +35,7 @@ defmodule Livebook.Session.DataTest do
evaluation_time_ms: 10, evaluation_time_ms: 10,
identifiers_used: uses, identifiers_used: uses,
identifiers_defined: defines, identifiers_defined: defines,
identifier_definitions: identifier_definitions,
code_markers: [] code_markers: []
} }
end end

View file

@ -17,6 +17,7 @@ defmodule Livebook.SessionTest do
evaluation_time_ms: 10, evaluation_time_ms: 10,
identifiers_used: [], identifiers_used: [],
identifiers_defined: %{}, identifiers_defined: %{},
identifier_definitions: [],
code_markers: [] code_markers: []
} }

View file

@ -246,10 +246,11 @@ defmodule LivebookWeb.Integration.Hub.DeploymentGroupTest do
test "shows the app deployed count", %{conn: conn, hub: hub, tmp_dir: tmp_dir} do test "shows the app deployed count", %{conn: conn, hub: hub, tmp_dir: tmp_dir} do
%{id: id} = insert_deployment_group(mode: :online, hub_id: hub.id) %{id: id} = insert_deployment_group(mode: :online, hub_id: hub.id)
id = to_string(id) id = to_string(id)
hub_id = hub.id
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
refute_received {:app_deployment_started, %{deployment_group_id: ^id}} refute_received {:app_deployment_started, %{deployment_group_id: ^id, hub_id: ^hub_id}}
assert view assert view
|> element("#hub-deployment-group-#{id} [aria-label=\"apps deployed\"]") |> element("#hub-deployment-group-#{id} [aria-label=\"apps deployed\"]")

View file

@ -2632,4 +2632,66 @@ defmodule LivebookWeb.SessionLiveTest do
assert File.read!(dockerfile_path) =~ "COPY notebook.livemd /apps" assert File.read!(dockerfile_path) =~ "COPY notebook.livemd /apps"
end end
end end
test "defined modules under sections", %{conn: conn, session: session} do
Code.put_compiler_option(:debug_info, true)
Session.subscribe(session.id)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
refute render(view) =~ "LivebookWeb.SessionLiveTest.MyBigModuleName"
cell_id =
insert_text_cell(session.pid, insert_section(session.pid), :code, ~S'''
defmodule LivebookWeb.SessionLiveTest.MyBigModuleName do
def bar, do: :baz
end
''')
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
assert has_element?(
view,
"[data-el-sections-list-definition-item] span",
"LivebookWeb.SessionLiveTest.MyBigModuleName"
)
assert render(view) =~
~s'data-file="#cell:#{cell_id}" data-line="1" title="LivebookWeb.SessionLiveTest.MyBigModuleName"'
second_cell_id =
insert_text_cell(session.pid, insert_section(session.pid), :code, ~S'''
defmodule LivebookWeb.SessionLiveTest.AnotherModule do
def bar, do: :baz
end
defmodule LivebookWeb.SessionLiveTest.Foo do
def bar, do: :baz
end
''')
Session.queue_cell_evaluation(session.pid, second_cell_id)
assert_receive {:operation, {:add_cell_evaluation_response, _, ^second_cell_id, _, _}}
assert has_element?(
view,
"[data-el-sections-list-definition-item] span",
"LivebookWeb.SessionLiveTest.AnotherModule"
)
assert has_element?(
view,
"[data-el-sections-list-definition-item] span",
"LivebookWeb.SessionLiveTest.Foo"
)
assert render(view) =~
~s'data-file="#cell:#{second_cell_id}" data-line="1" title="LivebookWeb.SessionLiveTest.AnotherModule"'
assert render(view) =~
~s'data-file="#cell:#{second_cell_id}" data-line="5" title="LivebookWeb.SessionLiveTest.Foo"'
after
Code.put_compiler_option(:debug_info, false)
end
end end