Made a Source Code Inspector, useful in big projects or large teams

Here’s the code, so others can play around with it, till the Phoenix team adds it to Phoenix 2.0!

defmodule DerpyToolsWeb.Nav do
import Phoenix.LiveView
use Phoenix.Component

def on_mount(:default, _params, _session, socket) do
  {:cont,
   socket
   |> attach_hook(:inspect_source, :handle_event, &handle_event/3)}
end

defp handle_event("inspect-source", %{"file" => file, "line" => line}, socket) do
  System.cmd("code", ["--goto", "#{file}:#{line}"])

  {:halt, socket}
end

defp handle_event(_, _, socket), do: {:cont, socket}
scope "/", DerpyToolsWeb do
  pipe_through :browser

  live_session :no_log_in_required,
    on_mount: [DerpyToolsWeb.Nav] do
    live "/", HomePageLive
    ...
  end
end

Usage

<h2
  id="test-div"
  data-file={__ENV__.file}
  data-line={__ENV__.line}
  phx-hook={Mix.env() == :dev && "SourceInspector"}
  class="..."
>
    Hover over this!
</h2>

JavaScript Side

source_inspector.js

import { computePosition, flip, offset, arrow } from "../vendor/floating-ui";

const SourceInspector = {
  mounted() {
    if (!this.el.dataset) {
      console.log("Please pass in file & line data attributes!");
      return;
    }

    const globalTooltip = document.querySelector("#inspector-tooltip");

    let tooltip = globalTooltip.cloneNode(true);
    tooltip.setAttribute("id", `inspect-${this.el.id}`);
    const inspectSourceBtn = tooltip.querySelector("#source-btn");
    const arrowElement = tooltip.querySelector("#arrow");

    this.el.addEventListener("mouseenter", () => {
      const { file, line } = this.el.dataset;

      this.el.appendChild(tooltip);

      tooltip.classList.remove("hidden");
      tooltip.classList.add("flex");

      placeTooltip(this.el, tooltip, arrowElement);

      this.el.classList.add(
        "rounded-lg",
        "outline",
        "outline-offset-4",
        "outline-pink-500"
      );

      inspectSourceBtn.setAttribute("phx-value-file", file);
      inspectSourceBtn.setAttribute("phx-value-line", line);
    });
    this.el.addEventListener("mouseleave", (e) => {
      handleMouseLeave(this.el, tooltip);
    });
  },
};

function handleMouseLeave(target, tooltip) {
  target.classList.remove(
    "rounded-lg",
    "outline",
    "outline-offset-4",
    "outline-pink-500"
  );

  tooltip.classList.add("hidden");
  tooltip.classList.remove("flex");
}

function placeTooltip(target, tooltip, arrowElement) {
  computePosition(target, tooltip, {
    placement: "top",
    middleware: [
      flip(),
      offset(8),
      arrow({
        element: arrowElement,
      }),
    ],
  }).then(({ x, y, placement, middlewareData }) => {
    Object.assign(tooltip.style, {
      left: `${x}px`,
      top: `${y}px`,
    });

    const { x: arrowX, y: arrowY } = middlewareData.arrow;

    const staticSide = {
      top: "bottom",
      right: "left",
      bottom: "top",
      left: "right",
    }[placement.split("-")[0]];

    Object.assign(arrowElement.style, {
      left: arrowX != null ? `${arrowX}px` : "",
      top: arrowY != null ? `${arrowY}px` : "",
      right: "",
      bottom: "",
      [staticSide]: "-10px",
    });
  });
}

export default SourceInspector;

app.js

import SourceInspector from "./source_inspector";

let liveSocket = new LiveSocket("/live", Socket, {
  params: {
    _csrf_token: csrfToken,
  },
  hooks: {
    SourceInspector,
  },
});

N.B. Don’t forget to add this in the environment variable:
export ELIXIR_EDITOR="code --goto __FILE__:__LINE__"

This way, the Beam instance will know to open the VS Code editor!


P.S. I used float-ui, which is the next iteration of Popper.js, for the tooltip.

Just download the ESM files from JS Delivr, i.e.

6 Likes