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.