diff --git a/assets/js/hooks/session.js b/assets/js/hooks/session.js index d2f3529fb..072ec41b8 100644 --- a/assets/js/hooks/session.js +++ b/assets/js/hooks/session.js @@ -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); + } }, /** diff --git a/lib/livebook/intellisense.ex b/lib/livebook/intellisense.ex index 043b17fa5..5ecf4abf9 100644 --- a/lib/livebook/intellisense.ex +++ b/lib/livebook/intellisense.ex @@ -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 diff --git a/lib/livebook/intellisense/docs.ex b/lib/livebook/intellisense/docs.ex index e0f8a9a0b..01915a2c0 100644 --- a/lib/livebook/intellisense/docs.ex +++ b/lib/livebook/intellisense/docs.ex @@ -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 diff --git a/lib/livebook/runtime.ex b/lib/livebook/runtime.ex index 83ddf0f2b..17a22de94 100644 --- a/lib/livebook/runtime.ex +++ b/lib/livebook/runtime.ex @@ -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 """ diff --git a/lib/livebook/runtime/evaluator.ex b/lib/livebook/runtime/evaluator.ex index a0b8e20a5..518e90c7a 100644 --- a/lib/livebook/runtime/evaluator.ex +++ b/lib/livebook/runtime/evaluator.ex @@ -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 diff --git a/lib/livebook/runtime/evaluator/io_proxy.ex b/lib/livebook/runtime/evaluator/io_proxy.ex index b768b2844..152662753 100644 --- a/lib/livebook/runtime/evaluator/io_proxy.ex +++ b/lib/livebook/runtime/evaluator/io_proxy.ex @@ -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 diff --git a/lib/livebook/runtime/evaluator/tracer.ex b/lib/livebook/runtime/evaluator/tracer.ex index 3325c5861..969e2f5d8 100644 --- a/lib/livebook/runtime/evaluator/tracer.ex +++ b/lib/livebook/runtime/evaluator/tracer.ex @@ -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 diff --git a/lib/livebook/session/data.ex b/lib/livebook/session/data.ex index b96060b91..83dd7caed 100644 --- a/lib/livebook/session/data.ex +++ b/lib/livebook/session/data.ex @@ -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 diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index f4fa3778e..88d177526 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -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 diff --git a/lib/livebook_web/live/session_live/render.ex b/lib/livebook_web/live/session_live/render.ex index 8e0df3653..3ea83840d 100644 --- a/lib/livebook_web/live/session_live/render.ex +++ b/lib/livebook_web/live/session_live/render.ex @@ -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 > -