mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-08 20:46:16 +08:00
List module definitions in the sections side panel (#2760)
This commit is contained in:
parent
eca2dc1143
commit
ad0db1832b
15 changed files with 215 additions and 46 deletions
|
@ -373,7 +373,7 @@ const Session = {
|
|||
}
|
||||
} else if (keyBuffer.tryMatch(["e", "s"])) {
|
||||
this.queueFocusedSectionEvaluation();
|
||||
} else if (keyBuffer.tryMatch(["s", "s"])) {
|
||||
} else if (keyBuffer.tryMatch(["s", "o"])) {
|
||||
this.toggleSectionsList();
|
||||
} else if (keyBuffer.tryMatch(["s", "e"])) {
|
||||
this.toggleSecretsList();
|
||||
|
@ -579,11 +579,23 @@ const Session = {
|
|||
*/
|
||||
handleSectionsListClick(event) {
|
||||
const sectionButton = event.target.closest(`[data-el-sections-list-item]`);
|
||||
|
||||
if (sectionButton) {
|
||||
const sectionId = sectionButton.getAttribute("data-section-id");
|
||||
const section = this.getSectionById(sectionId);
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -550,7 +550,7 @@ defmodule Livebook.Intellisense do
|
|||
path = Path.join(context.ebin_path, "#{module}.beam")
|
||||
|
||||
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: to_string(file), line: line}
|
||||
else
|
||||
|
|
|
@ -185,7 +185,7 @@ defmodule Livebook.Intellisense.Docs do
|
|||
|
||||
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, {:module, module}) do
|
||||
|
@ -221,8 +221,6 @@ defmodule Livebook.Intellisense.Docs do
|
|||
end
|
||||
|
||||
defp beam_lib_chunks(path, key) do
|
||||
path = String.to_charlist(path)
|
||||
|
||||
case :beam_lib.chunks(path, [key]) do
|
||||
{:ok, {_, [{^key, value}]}} -> {:ok, value}
|
||||
_ -> :error
|
||||
|
|
|
@ -470,12 +470,15 @@ defprotocol Livebook.Runtime do
|
|||
dependencies between evaluations and avoids unnecessary reevaluations.
|
||||
"""
|
||||
@type evaluation_response_metadata :: %{
|
||||
interrupted: boolean(),
|
||||
errored: boolean(),
|
||||
evaluation_time_ms: non_neg_integer(),
|
||||
code_markers: list(code_marker()),
|
||||
memory_usage: runtime_memory(),
|
||||
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 """
|
||||
|
|
|
@ -439,7 +439,7 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
|
||||
%{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
|
||||
{:ok, value, binding, env} ->
|
||||
context_id = random_long_id()
|
||||
|
@ -454,8 +454,10 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
{identifiers_used, identifiers_defined} =
|
||||
identifier_dependencies(new_context, tracer_info, context)
|
||||
|
||||
identifier_definitions = definitions(new_context, tracer_info)
|
||||
|
||||
result = {:ok, value}
|
||||
{new_context, result, identifiers_used, identifiers_defined}
|
||||
{new_context, result, identifiers_used, identifiers_defined, identifier_definitions}
|
||||
|
||||
{:error, kind, error, stacktrace} ->
|
||||
for {module, _} <- tracer_info.modules_defined do
|
||||
|
@ -465,9 +467,10 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
result = {:error, kind, error, stacktrace}
|
||||
identifiers_used = :unknown
|
||||
identifiers_defined = %{}
|
||||
identifier_definitions = []
|
||||
# Empty context
|
||||
new_context = initial_context()
|
||||
{new_context, result, identifiers_used, identifiers_defined}
|
||||
{new_context, result, identifiers_used, identifiers_defined, identifier_definitions}
|
||||
end
|
||||
|
||||
if ebin_path() do
|
||||
|
@ -475,7 +478,6 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
end
|
||||
|
||||
state = put_context(state, ref, new_context)
|
||||
|
||||
output = Evaluator.Formatter.format_result(result, language)
|
||||
|
||||
metadata = %{
|
||||
|
@ -485,7 +487,8 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
memory_usage: memory(),
|
||||
code_markers: code_markers,
|
||||
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})
|
||||
|
@ -890,7 +893,7 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
into: identifiers_used
|
||||
|
||||
identifiers_defined =
|
||||
for {module, _vars} <- tracer_info.modules_defined,
|
||||
for {module, _line_vars} <- tracer_info.modules_defined,
|
||||
version = module.__info__(:md5),
|
||||
do: {{:module, module}, version},
|
||||
into: identifiers_defined
|
||||
|
@ -968,7 +971,7 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
# Note that :prune_binding removes variables used by modules
|
||||
# (unless used outside), so we get those from the tracer
|
||||
module_used_vars =
|
||||
for {_module, vars} <- tracer_info.modules_defined,
|
||||
for {_module, {_line, vars}} <- tracer_info.modules_defined,
|
||||
var <- vars,
|
||||
into: MapSet.new(),
|
||||
do: var
|
||||
|
@ -1038,4 +1041,16 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
defp ebin_path() do
|
||||
Process.get(@ebin_path_key)
|
||||
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
|
||||
|
|
|
@ -146,7 +146,7 @@ defmodule Livebook.Runtime.Evaluator.IOProxy do
|
|||
state = update_in(state.tracer_info, &Evaluator.Tracer.apply_updates(&1, updates))
|
||||
|
||||
modules_defined =
|
||||
for {:module_defined, module, _vars} <- updates,
|
||||
for {:module_defined, module, _vars, _line} <- updates,
|
||||
into: state.modules_defined,
|
||||
do: module
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ defmodule Livebook.Runtime.Evaluator.Tracer do
|
|||
module = env.module
|
||||
vars = Map.keys(env.versioned_vars)
|
||||
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))
|
||||
end
|
||||
|
||||
defp apply_update(info, {:module_defined, module, vars}) do
|
||||
put_in(info.modules_defined[module], vars)
|
||||
defp apply_update(info, {:module_defined, module, vars, line}) do
|
||||
put_in(info.modules_defined[module], {line, vars})
|
||||
end
|
||||
|
||||
defp apply_update(info, {:alias_used, alias}) do
|
||||
|
|
|
@ -118,6 +118,8 @@ defmodule Livebook.Session.Data do
|
|||
new_bound_to_inputs: %{input_id() => input_hash()},
|
||||
identifiers_used: list(identifier :: term()) | :unknown,
|
||||
identifiers_defined: %{(identifier :: term()) => version :: term()},
|
||||
identifier_definitions:
|
||||
list(%{label: String.t(), file: String.t(), line: pos_integer()}),
|
||||
data: t()
|
||||
}
|
||||
|
||||
|
@ -1401,7 +1403,8 @@ defmodule Livebook.Session.Data do
|
|||
identifiers_defined: metadata.identifiers_defined,
|
||||
bound_to_inputs: eval_info.new_bound_to_inputs,
|
||||
evaluation_end: DateTime.utc_now(),
|
||||
code_markers: metadata.code_markers
|
||||
code_markers: metadata.code_markers,
|
||||
identifier_definitions: metadata.identifier_definitions
|
||||
}
|
||||
end)
|
||||
|> update_cell_evaluation_snapshot(cell, section)
|
||||
|
@ -2380,6 +2383,7 @@ defmodule Livebook.Session.Data do
|
|||
data: nil,
|
||||
code_markers: [],
|
||||
doctest_reports: %{},
|
||||
identifier_definitions: [],
|
||||
reevaluates_automatically: false
|
||||
}
|
||||
end
|
||||
|
|
|
@ -1796,7 +1796,8 @@ defmodule LivebookWeb.SessionLive do
|
|||
id: section.id,
|
||||
name: section.name,
|
||||
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,
|
||||
clients:
|
||||
|
@ -1853,6 +1854,14 @@ defmodule LivebookWeb.SessionLive do
|
|||
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
|
||||
cells =
|
||||
data.notebook
|
||||
|
|
|
@ -331,8 +331,8 @@ defmodule LivebookWeb.SessionLive.Render do
|
|||
<%!-- Local functionality --%>
|
||||
|
||||
<.button_item
|
||||
icon="booklet-fill"
|
||||
label="Sections (ss)"
|
||||
icon="node-tree"
|
||||
label="Outline (so)"
|
||||
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"
|
||||
data-el-side-panel
|
||||
>
|
||||
<div class="flex grow" data-el-sections-list>
|
||||
<.sections_list data_view={@data_view} />
|
||||
<div data-el-sections-list>
|
||||
<.outline_list data_view={@data_view} />
|
||||
</div>
|
||||
<div data-el-clients-list>
|
||||
<.clients_list data_view={@data_view} client_id={@client_id} />
|
||||
|
@ -497,25 +497,26 @@ defmodule LivebookWeb.SessionLive.Render do
|
|||
"""
|
||||
end
|
||||
|
||||
defp sections_list(assigns) do
|
||||
defp outline_list(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col grow">
|
||||
<h3 class="uppercase text-sm font-semibold text-gray-500">
|
||||
Sections
|
||||
Outline
|
||||
</h3>
|
||||
<div class="flex flex-col mt-4 space-y-4">
|
||||
<div :for={section_item <- @data_view.sections_items} class="flex items-center">
|
||||
<button
|
||||
class="grow flex items-center text-gray-500 hover:text-gray-900 text-left"
|
||||
data-el-sections-list-item
|
||||
data-section-id={section_item.id}
|
||||
>
|
||||
<span class="flex items-center space-x-1">
|
||||
<div :for={section_item <- @data_view.sections_items} class="flex flex-col">
|
||||
<div class="flex justify-between items-center">
|
||||
<button
|
||||
class="grow flex items-center gap-1 text-gray-500 hover:text-gray-900 text-left"
|
||||
data-el-sections-list-item
|
||||
data-section-id={section_item.id}
|
||||
>
|
||||
<.remix_icon icon="font-size" class="text-lg font-normal leading-none" />
|
||||
<span><%= section_item.name %></span>
|
||||
<%!--
|
||||
Note: the container has overflow-y auto, so we cannot set overflow-x visible,
|
||||
consequently we show the tooltip wrapped to a fixed number of characters
|
||||
--%>
|
||||
Note: the container has overflow-y auto, so we cannot set overflow-x visible,
|
||||
consequently we show the tooltip wrapped to a fixed number of characters
|
||||
--%>
|
||||
<span
|
||||
:if={section_item.parent}
|
||||
{branching_tooltip_attrs(section_item.name, section_item.parent.name)}
|
||||
|
@ -525,12 +526,30 @@ defmodule LivebookWeb.SessionLive.Render do
|
|||
class="text-lg font-normal leading-none flip-horizontally"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<.section_status
|
||||
status={elem(section_item.status, 0)}
|
||||
cell_id={elem(section_item.status, 1)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<.section_status
|
||||
status={elem(section_item.status, 0)}
|
||||
cell_id={elem(section_item.status, 1)}
|
||||
/>
|
||||
</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>
|
||||
<button
|
||||
|
@ -558,7 +577,7 @@ defmodule LivebookWeb.SessionLive.Render do
|
|||
wrapped_name = Livebook.Utils.wrap_line("”" <> parent_name <> "”", 16)
|
||||
label = "Branches from\n#{wrapped_name}"
|
||||
|
||||
[class: "tooltip #{direction}", data_tooltip: label]
|
||||
[class: "tooltip #{direction}", "data-tooltip": label]
|
||||
end
|
||||
|
||||
defp clients_list(assigns) do
|
||||
|
|
|
@ -259,9 +259,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
}}
|
||||
end
|
||||
|
||||
test "returns additional metadata when there is a module compilation error", %{
|
||||
evaluator: evaluator
|
||||
} do
|
||||
test "returns additional metadata when there is a module compilation error",
|
||||
%{evaluator: evaluator} do
|
||||
code = """
|
||||
defmodule Livebook.Runtime.EvaluatorTest.Invalid do
|
||||
x
|
||||
|
@ -504,6 +503,50 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
|
||||
refute Code.ensure_loaded?(Livebook.Runtime.EvaluatorTest.Exited)
|
||||
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
|
||||
|
||||
describe "doctests" do
|
||||
|
|
|
@ -25,6 +25,7 @@ defmodule Livebook.Session.DataTest do
|
|||
defp eval_meta(opts \\ []) do
|
||||
uses = opts[:uses] || []
|
||||
defines = opts[:defines] || %{}
|
||||
identifier_definitions = opts[:identifier_definitions] || []
|
||||
errored = Keyword.get(opts, :errored, false)
|
||||
interrupted = Keyword.get(opts, :interrupted, false)
|
||||
|
||||
|
@ -34,6 +35,7 @@ defmodule Livebook.Session.DataTest do
|
|||
evaluation_time_ms: 10,
|
||||
identifiers_used: uses,
|
||||
identifiers_defined: defines,
|
||||
identifier_definitions: identifier_definitions,
|
||||
code_markers: []
|
||||
}
|
||||
end
|
||||
|
|
|
@ -17,6 +17,7 @@ defmodule Livebook.SessionTest do
|
|||
evaluation_time_ms: 10,
|
||||
identifiers_used: [],
|
||||
identifiers_defined: %{},
|
||||
identifier_definitions: [],
|
||||
code_markers: []
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
%{id: id} = insert_deployment_group(mode: :online, hub_id: hub.id)
|
||||
id = to_string(id)
|
||||
hub_id = 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
|
||||
|> element("#hub-deployment-group-#{id} [aria-label=\"apps deployed\"]")
|
||||
|
|
|
@ -2632,4 +2632,66 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
assert File.read!(dockerfile_path) =~ "COPY notebook.livemd /apps"
|
||||
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
|
||||
|
|
Loading…
Add table
Reference in a new issue