@@ -76,8 +80,8 @@ defmodule FirehoseWeb.MicroprintsLive do
highlighted_line={@highlighted_line}
/>
- <%= if @expanded_path == path and source do %> <.source_viewer
- content={source}
+ <%= if @expanded_path == path and @source_content do %> <.source_viewer
+ source_content={@source_content}
highlighted_line={@highlighted_line}
language="elixir"
file_path={path}
@@ -111,11 +115,14 @@ defmodule FirehoseWeb.MicroprintsLive do
_ -> nil
end
- {:noreply,
- socket
- |> assign(:highlighted_path, path)
- |> assign(:highlighted_line, highlighted)
- |> assign(:expanded_path, expanded)}
+ socket =
+ socket
+ |> assign(:highlighted_path, path)
+ |> assign(:highlighted_line, highlighted)
+ |> assign(:expanded_path, expanded)
+ |> assign(:source_content, nil)
+
+ {:noreply, push_patch(socket, to: build_params(socket))}
end
@impl true
@@ -126,11 +133,75 @@ defmodule FirehoseWeb.MicroprintsLive do
_ -> path
end
- {:noreply, assign(socket, :expanded_path, expanded)}
+ source_content =
+ case expanded do
+ nil -> nil
+ ^path ->
+ item = Enum.find(socket.assigns.microprints, &(&1.path == path))
+ if item && item.source_dir do
+ abs_path = resolve_source_path(item.source_dir, path)
+ read_source(abs_path)
+ else
+ nil
+ end
+ end
+
+ socket = socket |> assign(:expanded_path, expanded) |> assign(:source_content, source_content)
+
+ {:noreply, push_patch(socket, to: build_params(socket))}
end
# Private helpers
+ defp restore_state_from_params(socket, params) do
+ expanded_path = params["expanded"]
+ highlighted_path = params["highlighted"]
+ highlighted_line = if params["line"], do: String.to_integer(params["line"]), else: nil
+
+ source_content =
+ if expanded_path do
+ item = Enum.find(socket.assigns.microprints, &(&1.path == expanded_path))
+
+ if item && item.source_dir do
+ abs_path = resolve_source_path(item.source_dir, expanded_path)
+ read_source(abs_path)
+ end
+ end
+
+ socket
+ |> assign(:expanded_path, expanded_path)
+ |> assign(:source_content, source_content)
+ |> assign(:highlighted_path, highlighted_path)
+ |> assign(:highlighted_line, highlighted_line)
+ end
+
+ defp build_params(socket) do
+ params = %{}
+
+ params =
+ if socket.assigns.expanded_path do
+ Map.put(params, "expanded", socket.assigns.expanded_path)
+ else
+ params
+ end
+
+ params =
+ if socket.assigns.highlighted_path do
+ Map.put(params, "highlighted", socket.assigns.highlighted_path)
+ else
+ params
+ end
+
+ params =
+ if socket.assigns.highlighted_line do
+ params |> Map.put("line", socket.assigns.highlighted_line)
+ else
+ params
+ end
+
+ "/microprints" <> if params != %{}, do: "?" <> URI.encode_query(params), else: ""
+ end
+
defp sort_by_mtime(files) do
app_root = Mix.Project.project_file() |> Path.dirname()
@@ -143,16 +214,39 @@ defmodule FirehoseWeb.MicroprintsLive do
|> Enum.map(fn {file, _mtime} -> file end)
end
+ defp sort_by_mtime_with_dir(tuples) do
+ app_root = Mix.Project.project_file() |> Path.dirname()
+
+ tuples
+ |> Enum.map(fn {path, dir} ->
+ abs_path = Path.join(app_root, path)
+ {{path, dir}, File.stat!(abs_path).mtime}
+ end)
+ |> Enum.sort_by(fn {{_path, _dir}, mtime} -> mtime end, :desc)
+ |> Enum.map(fn {{path, dir}, _mtime} -> {path, dir} end)
+ end
+
@doc false
def scan_source_files do
@source_dirs
|> Enum.flat_map(&collect_elixir_files/1)
- |> Enum.uniq()
+ |> Enum.uniq_by(&elem(&1, 0))
+ |> Enum.map(&elem(&1, 0))
|> Enum.sort()
|> sort_by_mtime()
|> Enum.take(2)
end
+ @doc false
+ def scan_source_files_with_dir do
+ @source_dirs
+ |> Enum.flat_map(&collect_elixir_files/1)
+ |> Enum.uniq_by(&elem(&1, 0))
+ |> Enum.sort_by(&elem(&1, 0))
+ |> sort_by_mtime_with_dir()
+ |> Enum.take(2)
+ end
+
defp resolve_absolute_paths(files) do
app_root = Mix.Project.project_file() |> Path.dirname()
@@ -181,26 +275,26 @@ defmodule FirehoseWeb.MicroprintsLive do
|> Path.wildcard()
|> Enum.filter(&File.regular?(&1))
|> Enum.reject(&excluded_path?/1)
- |> Enum.map(&format_path(&1, monorepo_root, dir))
+ |> Enum.map(&format_path_and_dir(&1, monorepo_root, dir))
false ->
[]
end
end
- defp format_path(path, monorepo_root, dir) do
+ defp format_path_and_dir(path, monorepo_root, dir) do
relative = Path.relative_to(path, monorepo_root)
- case dir do
- "app" ->
- String.replace_prefix(relative, "app/", "")
+ formatted =
+ case dir do
+ "app" -> String.replace_prefix(relative, "app/", "")
+ _ -> "../" <> relative
+ end
- _ ->
- "../" <> relative
- end
+ {formatted, dir}
end
- defp process_file(rel_path) do
+ defp process_file({rel_path, source_dir}) do
abs_path = resolve_absolute_paths([rel_path]) |> List.first()
case MicroprintCache.get_microprint(abs_path) do
@@ -211,13 +305,10 @@ defmodule FirehoseWeb.MicroprintsLive do
|> Enum.with_index(1)
|> Enum.map(fn {line, num} -> Map.put(line, :line_number, num) end)
- # Read source code for expand/contract
- source = read_source(abs_path)
-
%{
path: rel_path,
- microprint: Map.put(microprint, :lines, lines_with_numbers),
- source: source
+ source_dir: source_dir,
+ microprint: Map.put(microprint, :lines, lines_with_numbers)
}
{:error, reason} ->
@@ -225,6 +316,13 @@ defmodule FirehoseWeb.MicroprintsLive do
end
end
+ defp resolve_source_path(source_dir, rel_path) do
+ app_root = Mix.Project.project_file() |> Path.dirname()
+ monorepo_root = app_root |> Path.dirname()
+ base = Path.join(monorepo_root, source_dir)
+ Path.join(base, rel_path)
+ end
+
defp read_source(abs_path) do
case File.read(abs_path) do
{:ok, content} ->
@@ -255,7 +353,7 @@ defmodule FirehoseWeb.MicroprintsLive do
data-language={@language}
style="background: var(--sv-bg); color: var(--sv-text);"
>
-
"-pre"} phx-update="ignore" class="m-0 p-2"><%= @content %>
+
"-pre"} phx-update="ignore" class="m-0 p-2"><%= @source_content %>
"""
end
diff --git a/app/test/firehose_web/live/microprints_live_test.exs b/app/test/firehose_web/live/microprints_live_test.exs
index be9130c..e195ef3 100644
--- a/app/test/firehose_web/live/microprints_live_test.exs
+++ b/app/test/firehose_web/live/microprints_live_test.exs
@@ -139,4 +139,42 @@ defmodule FirehoseWeb.MicroprintsLiveTest do
"file B should show Collapse button when expanded"
end
end
+
+ describe "URL state persistence" do
+ test "expanded path is restored from query param on mount", %{conn: conn} do
+ files = MicroprintsLive.scan_source_files()
+ file_a = List.first(files)
+
+ {:ok, view, _html} = live(conn, ~p"/microprints?expanded=#{file_a}")
+
+ html = render(view)
+ assert html =~ "Collapse",
+ "Expanded file should show Collapse button when restored from URL param"
+ assert html =~ ~s(phx-value-path="#{file_a}")
+ end
+
+ test "unknown expanded path in URL is ignored gracefully", %{conn: conn} do
+ {:ok, view, _html} = live(conn, ~p"/microprints?expanded=nonexistent.ex")
+
+ html = render(view)
+ refute html =~ "Collapse",
+ "Nonexistent expanded path should not show Collapse"
+ end
+
+ test "expand updates URL with expanded param", %{conn: conn} do
+ files = MicroprintsLive.scan_source_files()
+ file_a = List.first(files)
+
+ {:ok, view, _html} = live(conn, ~p"/microprints")
+
+ view
+ |> element("button[phx-value-path=\"#{file_a}\"]", "Expand")
+ |> render_click()
+
+ # URL should contain expanded param after clicking Expand
+ html = render(view)
+ assert html =~ "Collapse",
+ "File should remain expanded after push_patch updates URL"
+ end
+ end
end
diff --git a/expand-collapse-fixed.html b/expand-collapse-fixed.html
index 81ea143..e06c7e8 100644
--- a/expand-collapse-fixed.html
+++ b/expand-collapse-fixed.html
@@ -1158,7 +1158,7 @@