Skip to content

Instantly share code, notes, and snippets.

@jtmuller5
Created February 6, 2026 15:41
Show Gist options
  • Select an option

  • Save jtmuller5/401460039199e7886bb85f732b18bf0f to your computer and use it in GitHub Desktop.

Select an option

Save jtmuller5/401460039199e7886bb85f732b18bf0f to your computer and use it in GitHub Desktop.
Click-to-component plugin for React Grab, designed for TanStack Start
import { useEffect } from "react";
/*
Add the following to your root component, e.g., in __root.tsx:
<ReactGrab editor="vscode" projectRoot="/Users/you/project" />
*/
type Editor = "vscode" | "vscode-insiders" | "cursor" | (string & {});
interface ClickToComponentOptions {
editor?: Editor;
pathModifier?: (path: string) => string;
altClick?: boolean;
}
// Get React fiber from DOM element
function getFiberFromElement(element: Element): Fiber | null {
for (const key of Object.keys(element)) {
if (
key.startsWith("__reactFiber$") ||
key.startsWith("__reactInternalInstance$")
) {
return (element as unknown as Record<string, Fiber>)[key];
}
}
return null;
}
interface Fiber {
return: Fiber | null;
_debugSource?: { fileName: string; lineNumber: number };
_debugOwner?: Fiber;
_debugStack?: Error;
type?: { name?: string } | string;
}
// Parse stack trace from _debugStack to get source location
// Stack frames can be URLs like: http://localhost:7777/_build/@fs/Users/.../file.tsx:6:39
function parseDebugStack(
stack: Error | undefined,
): { filePath: string; lineNumber: number } | null {
if (!stack?.stack) return null;
const lines = stack.stack.split("\n");
for (const line of lines) {
// Match URL format: http://localhost:.../@fs/path/to/file.tsx:line:col
// The /@fs/ prefix indicates a real filesystem path
const fsMatch = line.match(/@fs(\/[^:]+):(\d+):\d+/);
if (fsMatch) {
return { filePath: fsMatch[1], lineNumber: parseInt(fsMatch[2], 10) };
}
// Match URL format with /_build/src/: http://localhost:.../_build/src/file.tsx:line:col
const buildMatch = line.match(/\/_build\/(src\/[^:]+):(\d+):\d+/);
if (buildMatch) {
return {
filePath: buildMatch[1],
lineNumber: parseInt(buildMatch[2], 10),
};
}
// Match traditional format: "at ComponentName (file.tsx:123:45)"
const traditionalMatch = line.match(
/at\s+(?:\S+\s+)?\(?([^:\s()]+):(\d+):\d+\)?/,
);
if (traditionalMatch && !traditionalMatch[1].includes("node_modules")) {
return {
filePath: traditionalMatch[1],
lineNumber: parseInt(traditionalMatch[2], 10),
};
}
}
return null;
}
// Check if path is a library file (not our project)
function isLibraryPath(path: string): boolean {
return (
path.includes("node_modules") ||
path.startsWith("/@") ||
/(?:\.\.\/)+@/.test(path) ||
path.includes("@tanstack") ||
path.includes("@vinxi") ||
path.includes("react-dom") ||
path.includes("react/")
);
}
// Walk fiber tree to find first project component source
function findProjectSource(
element: Element,
): { filePath: string; lineNumber: number } | null {
let fiber = getFiberFromElement(element);
const visited = new Set<Fiber>();
while (fiber) {
if (visited.has(fiber)) break;
visited.add(fiber);
// React 19 uses _debugStack instead of _debugSource
// Try _debugStack first (React 19), then _debugSource (React 18)
let filePath: string | undefined;
let lineNumber: number | undefined;
if (fiber._debugStack) {
const parsed = parseDebugStack(fiber._debugStack);
if (parsed) {
filePath = parsed.filePath;
lineNumber = parsed.lineNumber;
}
} else if (fiber._debugSource) {
filePath = fiber._debugSource.fileName;
lineNumber = fiber._debugSource.lineNumber;
}
if (filePath && !isLibraryPath(filePath)) {
return { filePath, lineNumber: lineNumber ?? 1 };
}
// Walk up via _debugOwner first (component that rendered this),
// then fall back to return (parent fiber)
fiber = fiber._debugOwner ?? fiber.return;
}
return null;
}
const createClickToComponentPlugin = (
options: ClickToComponentOptions = {},
) => {
const { editor = "vscode-insiders", pathModifier, altClick = true } = options;
const buildUrl = (filePath: string, line?: number) => {
const path = pathModifier ? pathModifier(filePath) : filePath;
if (!path) return null; // Skip if pathModifier returns empty
const fullPath = `${path}:${line ?? 1}:1`;
return fullPath[0] === "/"
? `${editor}://file${fullPath}`
: `${editor}://file/${fullPath}`;
};
const openFile = (filePath: string, lineNumber?: number) => {
const url = buildUrl(filePath, lineNumber);
if (url) window.location.assign(url);
};
return {
name: "click-to-component",
hooks: {
onOpenFile: (filePath: string, lineNumber?: number) => {
openFile(filePath, lineNumber);
return true;
},
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setup: (_api: any) => {
if (!altClick) return;
let isAltHeld = false;
let target: HTMLElement | null = null;
const onKeyDown = (e: KeyboardEvent) => {
if (e.altKey) isAltHeld = true;
};
const onKeyUp = () => {
isAltHeld = false;
target?.removeAttribute("data-ctc");
target = null;
};
const onMouseMove = (e: MouseEvent) => {
if (!isAltHeld || !(e.target instanceof HTMLElement)) return;
target?.removeAttribute("data-ctc");
target = e.target;
target.setAttribute("data-ctc", "");
};
const onClick = (e: MouseEvent) => {
if (!isAltHeld || !target) return;
e.preventDefault();
e.stopPropagation();
const currentTarget = target;
// Use our custom fiber walking to skip library components
const source = findProjectSource(currentTarget);
if (source?.filePath) {
openFile(source.filePath, source.lineNumber);
}
isAltHeld = false;
currentTarget.removeAttribute("data-ctc");
target = null;
};
const style = document.createElement("style");
style.textContent = `[data-ctc] { outline: -webkit-focus-ring-color auto 1px !important; cursor: pointer !important; }`;
document.head.appendChild(style);
window.addEventListener("keydown", onKeyDown);
window.addEventListener("keyup", onKeyUp);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("click", onClick, { capture: true });
return {
cleanup: () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("keyup", onKeyUp);
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("click", onClick, { capture: true });
style.remove();
},
};
},
};
};
type ClickToComponentPlugin = ReturnType<typeof createClickToComponentPlugin>;
interface ReactGrabProps {
/** Editor to open files in. Default: "cursor" */
editor?: Editor;
/** Project root path. Auto-detected from Vite if not provided */
projectRoot?: string;
}
export function ReactGrab({
editor = "vscode-insiders",
projectRoot,
}: ReactGrabProps = {}) {
useEffect(() => {
if (import.meta.env.PROD) return;
let cleanup: (() => void) | undefined;
void import("react-grab").then((m) => {
const api = m.getGlobalApi();
if (!api) return;
api.setEnabled(true);
const handleKeyDown = (event: KeyboardEvent) => {
if (event.metaKey && event.key === " ") {
event.preventDefault();
event.stopPropagation();
setTimeout(() => {
if (api.isActive()) {
api.deactivate();
} else {
api.activate();
}
}, 0);
}
};
document.addEventListener("keydown", handleKeyDown, true);
cleanup = () =>
document.removeEventListener("keydown", handleKeyDown, true);
(
window as {
__REACT_GRAB__?: {
registerPlugin: (plugin: ClickToComponentPlugin) => void;
};
}
).__REACT_GRAB__?.registerPlugin(
createClickToComponentPlugin({
editor,
pathModifier: (path) => {
// TanStack Start/Vinxi builds to /_build/src/...
// Strip that prefix to get the real source path
if (path.startsWith("/_build/")) {
path = path.slice("/_build/".length);
}
// If we have a project root and path is relative, make it absolute
if (projectRoot) {
if (path.startsWith("src/")) {
return `${projectRoot}/${path}`;
}
if (path.startsWith("/src/")) {
return `${projectRoot}${path}`;
}
}
// Skip library source files - not useful to navigate to
// Handles: /@tanstack/..., /node_modules/..., ../../../../@tanstack/...
if (
path.startsWith("/@") ||
path.startsWith("/node_modules/") ||
/(?:\.\.\/)+@/.test(path)
) {
return "";
}
// If path is already absolute, use it directly
if (path.startsWith("/")) {
return path;
}
// Relative path with project root
if (projectRoot) {
return `${projectRoot}/${path}`;
}
return path;
},
}),
);
});
return () => cleanup?.();
}, [editor, projectRoot]);
return null;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment